1 #this is so kludgy i'd be embarassed if it wasn't cybercash's fault
3 use vars qw($paymentserversecret $paymentserverport $paymentserverhost);
8 use vars qw( @ISA $conf $lpr $processor $xaction $E_NoErr $invoice_from
17 use Business::CreditCard;
18 use FS::UID qw( getotaker dbh );
19 use FS::Record qw( qsearchs qsearch );
22 use FS::cust_bill_pkg;
25 use FS::cust_pay_batch;
26 use FS::part_referral;
27 use FS::cust_main_county;
29 use FS::cust_main_invoice;
30 use FS::prepay_credit;
32 @ISA = qw( FS::Record );
37 #ask FS::UID to run this stuff for us later
38 $FS::UID::callback{'FS::cust_main'} = sub {
40 $lpr = $conf->config('lpr');
41 $invoice_from = $conf->config('invoice_from');
42 $smtpmachine = $conf->config('smtpmachine');
44 if ( $conf->exists('cybercash3.2') ) {
46 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
47 require CCMckDirectLib3_2;
49 require CCMckErrno3_2;
50 #qw(MCKGetErrorMessage $E_NoErr);
51 import CCMckErrno3_2 qw($E_NoErr);
54 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
55 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
56 if ( $status != $E_NoErr ) {
57 warn "CCMckLib3_2::InitConfig error:\n";
58 foreach my $key (keys %CCMckLib3_2::Config) {
59 warn " $key => $CCMckLib3_2::Config{$key}\n"
61 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
62 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
64 $processor='cybercash3.2';
65 } elsif ( $conf->exists('cybercash2') ) {
68 ( $main::paymentserverhost,
69 $main::paymentserverport,
70 $main::paymentserversecret,
72 ) = $conf->config('cybercash2');
73 $processor='cybercash2';
79 FS::cust_main - Object methods for cust_main records
85 $record = new FS::cust_main \%hash;
86 $record = new FS::cust_main { 'column' => 'value' };
88 $error = $record->insert;
90 $error = $new_record->replace($old_record);
92 $error = $record->delete;
94 $error = $record->check;
96 @cust_pkg = $record->all_pkgs;
98 @cust_pkg = $record->ncancelled_pkgs;
100 $error = $record->bill;
101 $error = $record->bill %options;
102 $error = $record->bill 'time' => $time;
104 $error = $record->collect;
105 $error = $record->collect %options;
106 $error = $record->collect 'invoice_time' => $time,
107 'batch_card' => 'yes',
108 'report_badcard' => 'yes',
113 An FS::cust_main object represents a customer. FS::cust_main inherits from
114 FS::Record. The following fields are currently supported:
118 =item custnum - primary key (assigned automatically for new customers)
120 =item agentnum - agent (see L<FS::agent>)
122 =item refnum - referral (see L<FS::part_referral>)
128 =item ss - social security number (optional)
130 =item company - (optional)
134 =item address2 - (optional)
138 =item county - (optional, see L<FS::cust_main_county>)
140 =item state - (see L<FS::cust_main_county>)
144 =item country - (see L<FS::cust_main_county>)
146 =item daytime - phone (optional)
148 =item night - phone (optional)
150 =item fax - phone (optional)
152 =item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
154 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
156 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
158 =item payname - name on card or billing name
160 =item tax - tax exempt, empty or `Y'
162 =item otaker - order taker (assigned automatically, see L<FS::UID>)
172 Creates a new customer. To add the customer to the database, see L<"insert">.
174 Note that this stores the hash reference, not a distinct copy of the hash it
175 points to. You can ask the object for a copy with the I<hash> method.
179 sub table { 'cust_main'; }
183 Adds this customer to the database. If there is an error, returns the error,
184 otherwise returns false.
186 There is a special insert mode in which you pass a data structure to the insert
187 method containing FS::cust_pkg and FS::svc_I<tablename> objects. When
188 running under a transactional database, all records are inserted atomicly, or
189 the transaction is rolled back. There should be a better explanation of this,
190 but until then, here's an example:
193 tie %hash, 'Tie::RefHash'; #this part is important
195 $cust_pkg => [ $svc_acct ],
197 $cust_main->insert( \%hash );
204 local $SIG{HUP} = 'IGNORE';
205 local $SIG{INT} = 'IGNORE';
206 local $SIG{QUIT} = 'IGNORE';
207 local $SIG{TERM} = 'IGNORE';
208 local $SIG{TSTP} = 'IGNORE';
209 local $SIG{PIPE} = 'IGNORE';
211 my $oldAutoCommit = $FS::UID::AutoCommit;
212 local $FS::UID::AutoCommit = 0;
217 if ( $self->payby eq 'PREPAY' ) {
218 $self->payby('BILL');
219 my $prepay_credit = qsearchs(
221 { 'identifier' => $self->payinfo },
225 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
226 unless $prepay_credit;
227 $amount = $prepay_credit->amount;
228 $seconds = $prepay_credit->seconds;
229 my $error = $prepay_credit->delete;
231 $dbh->rollback if $oldAutoCommit;
236 my $error = $self->SUPER::insert;
238 $dbh->rollback if $oldAutoCommit;
243 my $cust_pkgs = shift;
244 foreach my $cust_pkg ( keys %$cust_pkgs ) {
245 $cust_pkg->custnum( $self->custnum );
246 $error = $cust_pkg->insert;
248 $dbh->rollback if $oldAutoCommit;
251 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
252 $svc_something->pkgnum( $cust_pkg->pkgnum );
253 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
254 $svc_something->seconds( $svc_something->seconds + $seconds );
257 $error = $svc_something->insert;
259 $dbh->rollback if $oldAutoCommit;
267 $dbh->rollback if $oldAutoCommit;
268 return "No svc_acct record to apply pre-paid time";
272 my $cust_credit = new FS::cust_credit {
273 'custnum' => $self->custnum,
276 $error = $cust_credit->insert;
278 $dbh->rollback if $oldAutoCommit;
283 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
288 =item delete NEW_CUSTNUM
290 This deletes the customer. If there is an error, returns the error, otherwise
293 This will completely remove all traces of the customer record. This is not
294 what you want when a customer cancels service; for that, cancel all of the
295 customer's packages (see L<FS::cust_pkg/cancel>).
297 If the customer has any packages, you need to pass a new (valid) customer
298 number for those packages to be transferred to.
300 You can't delete a customer with invoices (see L<FS::cust_bill>),
301 or credits (see L<FS::cust_credit>).
308 local $SIG{HUP} = 'IGNORE';
309 local $SIG{INT} = 'IGNORE';
310 local $SIG{QUIT} = 'IGNORE';
311 local $SIG{TERM} = 'IGNORE';
312 local $SIG{TSTP} = 'IGNORE';
313 local $SIG{PIPE} = 'IGNORE';
315 my $oldAutoCommit = $FS::UID::AutoCommit;
316 local $FS::UID::AutoCommit = 0;
319 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
320 $dbh->rollback if $oldAutoCommit;
321 return "Can't delete a customer with invoices";
323 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
324 $dbh->rollback if $oldAutoCommit;
325 return "Can't delete a customer with credits";
328 my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
330 my $new_custnum = shift;
331 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
332 $dbh->rollback if $oldAutoCommit;
333 return "Invalid new customer number: $new_custnum";
335 foreach my $cust_pkg ( @cust_pkg ) {
336 my %hash = $cust_pkg->hash;
337 $hash{'custnum'} = $new_custnum;
338 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
339 my $error = $new_cust_pkg->replace($cust_pkg);
341 $dbh->rollback if $oldAutoCommit;
346 foreach my $cust_main_invoice (
347 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
349 my $error = $cust_main_invoice->delete;
351 $dbh->rollback if $oldAutoCommit;
356 my $error = $self->SUPER::delete;
358 $dbh->rollback if $oldAutoCommit;
362 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
367 =item replace OLD_RECORD
369 Replaces the OLD_RECORD with this one in the database. If there is an error,
370 returns the error, otherwise returns false.
374 Checks all fields to make sure this is a valid customer record. If there is
375 an error, returns the error, otherwise returns false. Called by the insert
384 $self->ut_numbern('custnum')
385 || $self->ut_number('agentnum')
386 || $self->ut_number('refnum')
387 || $self->ut_textn('company')
388 || $self->ut_text('address1')
389 || $self->ut_textn('address2')
390 || $self->ut_text('city')
391 || $self->ut_textn('county')
392 || $self->ut_textn('state')
394 return $error if $error;
396 return "Unknown agent"
397 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
399 return "Unknown referral"
400 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
402 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/
403 or return "Illegal last name: ". $self->getfield('last');
404 $self->setfield('last',$1);
406 $self->first =~ /^([\w \,\.\-\']+)$/
407 or return "Illegal first name: ". $self->first;
410 if ( $self->ss eq '' ) {
415 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
416 or return "Illegal social security number: ". $self->ss;
417 $self->ss("$1-$2-$3");
420 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
422 unless ( qsearchs('cust_main_county', {
423 'country' => $self->country,
426 return "Unknown state/county/country: ".
427 $self->state. "/". $self->county. "/". $self->country
428 unless qsearchs('cust_main_county',{
429 'state' => $self->state,
430 'county' => $self->county,
431 'country' => $self->country,
436 $self->ut_phonen('daytime', $self->country)
437 || $self->ut_phonen('night', $self->country)
438 || $self->ut_phonen('fax', $self->country)
440 return $error if $error;
442 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
443 or return "Illegal zip: ". $self->zip;
446 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
447 or return "Illegal payby: ". $self->payby;
450 if ( $self->payby eq 'CARD' ) {
452 my $payinfo = $self->payinfo;
454 $payinfo =~ /^(\d{13,16})$/
455 or return "Illegal credit card number: ". $self->payinfo;
457 $self->payinfo($payinfo);
459 or return "Illegal credit card number: ". $self->payinfo;
460 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
462 } elsif ( $self->payby eq 'BILL' ) {
464 $error = $self->ut_textn('payinfo');
465 return "Illegal P.O. number: ". $self->payinfo if $error;
467 } elsif ( $self->payby eq 'COMP' ) {
469 $error = $self->ut_textn('payinfo');
470 return "Illegal comp account issuer: ". $self->payinfo if $error;
472 } elsif ( $self->payby eq 'PREPAY' ) {
474 my $payinfo = $self->payinfo;
475 $payinfo =~ s/\W//g; #anything else would just confuse things
476 $self->payinfo($payinfo);
477 $error = $self->ut_alpha('payinfo');
478 return "Illegal prepayment identifier: ". $self->payinfo if $error;
479 return "Unknown prepayment identifier"
480 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
484 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
485 return "Expriation date required"
486 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
489 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
490 or return "Illegal expiration date: ". $self->paydate;
491 if ( length($2) == 4 ) {
492 $self->paydate("$2-$1-01");
493 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
494 $self->paydate("19$2-$1-01");
496 $self->paydate("20$2-$1-01");
500 if ( $self->payname eq '' ) {
501 $self->payname( $self->first. " ". $self->getfield('last') );
503 $self->payname =~ /^([\w \,\.\-\']+)$/
504 or return "Illegal billing name: ". $self->payname;
508 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
511 $self->otaker(getotaker);
518 Returns all packages (see L<FS::cust_pkg>) for this customer.
524 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
527 =item ncancelled_pkgs
529 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
533 sub ncancelled_pkgs {
535 @{ [ # force list context
536 qsearch( 'cust_pkg', {
537 'custnum' => $self->custnum,
540 qsearch( 'cust_pkg', {
541 'custnum' => $self->custnum,
549 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
550 conjunction with the collect method.
552 The only currently available option is `time', which bills the customer as if
553 it were that time. It is specified as a UNIX timestamp; see
554 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
557 If there is an error, returns the error, otherwise returns false.
562 my( $self, %options ) = @_;
563 my $time = $options{'time'} || time;
568 local $SIG{HUP} = 'IGNORE';
569 local $SIG{INT} = 'IGNORE';
570 local $SIG{QUIT} = 'IGNORE';
571 local $SIG{TERM} = 'IGNORE';
572 local $SIG{TSTP} = 'IGNORE';
573 local $SIG{PIPE} = 'IGNORE';
575 my $oldAutoCommit = $FS::UID::AutoCommit;
576 local $FS::UID::AutoCommit = 0;
579 # find the packages which are due for billing, find out how much they are
580 # & generate invoice database.
582 my( $total_setup, $total_recur ) = ( 0, 0 );
585 foreach my $cust_pkg (
586 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
589 next if $cust_pkg->getfield('cancel');
591 #? to avoid use of uninitialized value errors... ?
592 $cust_pkg->setfield('bill', '')
593 unless defined($cust_pkg->bill);
595 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
597 #so we don't modify cust_pkg record unnecessarily
598 my $cust_pkg_mod_flag = 0;
599 my %hash = $cust_pkg->hash;
600 my $old_cust_pkg = new FS::cust_pkg \%hash;
604 unless ( $cust_pkg->setup ) {
605 my $setup_prog = $part_pkg->getfield('setup');
607 #$cpt->permit(); #what is necessary?
608 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
609 $setup = $cpt->reval($setup_prog);
610 unless ( defined($setup) ) {
611 warn "Error reval-ing part_pkg->setup pkgpart ",
612 $part_pkg->pkgpart, ": $@";
614 $cust_pkg->setfield('setup',$time);
615 $cust_pkg_mod_flag=1;
622 if ( $part_pkg->getfield('freq') > 0 &&
623 ! $cust_pkg->getfield('susp') &&
624 ( $cust_pkg->getfield('bill') || 0 ) < $time
626 my $recur_prog = $part_pkg->getfield('recur');
628 #$cpt->permit(); #what is necessary?
629 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
630 $recur = $cpt->reval($recur_prog);
631 unless ( defined($recur) ) {
632 warn "Error reval-ing part_pkg->recur pkgpart ",
633 $part_pkg->pkgpart, ": $@";
635 #change this bit to use Date::Manip? CAREFUL with timezones (see
636 # mailing list archive)
637 #$sdate=$cust_pkg->bill || time;
638 #$sdate=$cust_pkg->bill || $time;
639 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
640 my ($sec,$min,$hour,$mday,$mon,$year) =
641 (localtime($sdate) )[0,1,2,3,4,5];
642 $mon += $part_pkg->getfield('freq');
643 until ( $mon < 12 ) { $mon -= 12; $year++; }
644 $cust_pkg->setfield('bill',
645 timelocal($sec,$min,$hour,$mday,$mon,$year));
646 $cust_pkg_mod_flag = 1;
650 warn "setup is undefined" unless defined($setup);
651 warn "recur is undefined" unless defined($recur);
652 warn "cust_pkg bill is undefined" unless defined($cust_pkg->bill);
654 if ( $cust_pkg_mod_flag ) {
655 $error=$cust_pkg->replace($old_cust_pkg);
656 if ( $error ) { #just in case
657 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
659 $setup = sprintf( "%.2f", $setup );
660 $recur = sprintf( "%.2f", $recur );
661 my $cust_bill_pkg = new FS::cust_bill_pkg ({
662 'pkgnum' => $cust_pkg->pkgnum,
666 'edate' => $cust_pkg->bill,
668 push @cust_bill_pkg, $cust_bill_pkg;
669 $total_setup += $setup;
670 $total_recur += $recur;
676 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
678 unless ( @cust_bill_pkg ) {
679 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
683 unless ( $self->getfield('tax') =~ /Y/i
684 || $self->getfield('payby') eq 'COMP'
686 my $cust_main_county = qsearchs('cust_main_county',{
687 'state' => $self->state,
688 'county' => $self->county,
689 'country' => $self->country,
691 my $tax = sprintf( "%.2f",
692 $charged * ( $cust_main_county->getfield('tax') / 100 )
694 $charged = sprintf( "%.2f", $charged+$tax );
696 my $cust_bill_pkg = new FS::cust_bill_pkg ({
703 push @cust_bill_pkg, $cust_bill_pkg;
706 my $cust_bill = new FS::cust_bill ( {
707 'custnum' => $self->getfield('custnum'),
709 'charged' => $charged,
711 $error = $cust_bill->insert;
713 $dbh->rollback if $oldAutoCommit;
714 return "$error for customer #". $self->custnum;
717 my $invnum = $cust_bill->invnum;
719 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
720 $cust_bill_pkg->setfield( 'invnum', $invnum );
721 $error = $cust_bill_pkg->insert;
722 #shouldn't happen, but how else tohandle this?
724 $dbh->rollback if $oldAutoCommit;
725 return "$error for customer #". $self->custnum;
729 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
733 =item collect OPTIONS
735 (Attempt to) collect money for this customer's outstanding invoices (see
736 L<FS::cust_bill>). Usually used after the bill method.
738 Depending on the value of `payby', this may print an invoice (`BILL'), charge
739 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
741 If there is an error, returns the error, otherwise returns false.
743 Currently available options are:
745 invoice_time - Use this time when deciding when to print invoices and
746 late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse>
747 for conversion functions.
749 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
750 default, cards are processed immediately, which will generate an error if
751 CyberCash is not installed.
753 report_badcard - Set this true if you want bad card transactions to
754 return an error. By default, they don't.
759 my( $self, %options ) = @_;
760 my $invoice_time = $options{'invoice_time'} || time;
763 local $SIG{HUP} = 'IGNORE';
764 local $SIG{INT} = 'IGNORE';
765 local $SIG{QUIT} = 'IGNORE';
766 local $SIG{TERM} = 'IGNORE';
767 local $SIG{TSTP} = 'IGNORE';
768 local $SIG{PIPE} = 'IGNORE';
770 my $oldAutoCommit = $FS::UID::AutoCommit;
771 local $FS::UID::AutoCommit = 0;
774 my $total_owed = $self->balance;
775 warn "collect: total owed $total_owed " if $Debug;
776 unless ( $total_owed > 0 ) { #redundant?????
777 $dbh->rollback if $oldAutoCommit;
781 foreach my $cust_bill (
782 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
785 #this has to be before next's
786 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
790 $total_owed = sprintf( "%.2f", $total_owed - $amount );
792 next unless $cust_bill->owed > 0;
794 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
796 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)" if $Debug;
798 next unless $amount > 0;
800 if ( $self->payby eq 'BILL' ) {
803 my $since = $invoice_time - ( $cust_bill->_date || 0 );
804 #warn "$invoice_time ", $cust_bill->_date, " $since";
805 if ( $since >= 0 #don't print future invoices
806 && ( $cust_bill->printed * 2592000 ) <= $since
809 #my @print_text = $cust_bill->print_text; #( date )
810 my @invoicing_list = $self->invoicing_list;
811 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
812 $ENV{SMTPHOSTS} = $smtpmachine;
813 $ENV{MAILADDRESS} = $invoice_from;
814 my $header = new Mail::Header ( [
815 "From: $invoice_from",
816 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
817 "Sender: $invoice_from",
818 "Reply-To: $invoice_from",
819 "Date: ". time2str("%a, %d %b %Y %X %z", time),
822 my $message = new Mail::Internet (
824 'Body' => [ $cust_bill->print_text ], #( date)
826 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
828 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
829 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
830 print LPR $cust_bill->print_text; #( date )
832 or die $! ? "Error closing $lpr: $!"
833 : "Exit status $? from $lpr";
836 my %hash = $cust_bill->hash;
838 my $new_cust_bill = new FS::cust_bill(\%hash);
839 my $error = $new_cust_bill->replace($cust_bill);
840 warn "Error updating $cust_bill->printed: $error" if $error;
844 } elsif ( $self->payby eq 'COMP' ) {
845 my $cust_pay = new FS::cust_pay ( {
846 'invnum' => $cust_bill->invnum,
850 'payinfo' => $self->payinfo,
853 my $error = $cust_pay->insert;
855 $dbh->rollback if $oldAutoCommit;
856 return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
860 } elsif ( $self->payby eq 'CARD' ) {
862 if ( $options{'batch_card'} ne 'yes' ) {
864 unless ( $processor ) {
865 $dbh->rollback if $oldAutoCommit;
866 return "Real time card processing not enabled!";
869 if ( $processor =~ /^cybercash/ ) {
871 #fix exp. date for cybercash
872 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
873 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
876 my $paybatch = $cust_bill->invnum.
877 '-' . time2str("%y%m%d%H%M%S", time);
879 my $payname = $self->payname ||
880 $self->getfield('first'). ' '. $self->getfield('last');
882 my $address = $self->address1;
883 $address .= ", ". $self->address2 if $self->address2;
885 my $country = 'USA' if $self->country eq 'US';
887 my @full_xaction = ( $xaction,
888 'Order-ID' => $paybatch,
889 'Amount' => "usd $amount",
890 'Card-Number' => $self->getfield('payinfo'),
891 'Card-Name' => $payname,
892 'Card-Address' => $address,
893 'Card-City' => $self->getfield('city'),
894 'Card-State' => $self->getfield('state'),
895 'Card-Zip' => $self->getfield('zip'),
896 'Card-Country' => $country,
901 if ( $processor eq 'cybercash2' ) {
902 $^W=0; #CCLib isn't -w safe, ugh!
903 %result = &CCLib::sendmserver(@full_xaction);
905 } elsif ( $processor eq 'cybercash3.2' ) {
906 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
908 $dbh->rollback if $oldAutoCommit;
909 return "Unknown real-time processor $processor";
912 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
913 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
914 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
915 my $cust_pay = new FS::cust_pay ( {
916 'invnum' => $cust_bill->invnum,
920 'payinfo' => $self->payinfo,
921 'paybatch' => "$processor:$paybatch",
923 my $error = $cust_pay->insert;
925 # gah, even with transactions.
926 $dbh->commit if $oldAutoCommit; #well.
927 my $e = 'WARNING: Card debited but database not updated - '.
928 'error applying payment, invnum #' . $cust_bill->invnum.
929 " (CyberCash Order-ID $paybatch): $error";
933 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
934 || $options{'report_badcard'} ) {
935 $dbh->commit if $oldAutoCommit;
936 return 'Cybercash error, invnum #' .
937 $cust_bill->invnum. ':'. $result{'MErrMsg'};
939 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
944 $dbh->rollback if $oldAutoCommit;
945 return "Unknown real-time processor $processor\n";
950 my $cust_pay_batch = new FS::cust_pay_batch ( {
951 'invnum' => $cust_bill->getfield('invnum'),
952 'custnum' => $self->getfield('custnum'),
953 'last' => $self->getfield('last'),
954 'first' => $self->getfield('first'),
955 'address1' => $self->getfield('address1'),
956 'address2' => $self->getfield('address2'),
957 'city' => $self->getfield('city'),
958 'state' => $self->getfield('state'),
959 'zip' => $self->getfield('zip'),
960 'country' => $self->getfield('country'),
962 'cardnum' => $self->getfield('payinfo'),
963 'exp' => $self->getfield('paydate'),
964 'payname' => $self->getfield('payname'),
967 my $error = $cust_pay_batch->insert;
969 $dbh->rollback if $oldAutoCommit;
970 return "Error adding to cust_pay_batch: $error";
976 $dbh->rollback if $oldAutoCommit;
977 return "Unknown payment type ". $self->payby;
981 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
988 Returns the total owed for this customer on all invoices
989 (see L<FS::cust_bill>).
996 foreach my $cust_bill ( qsearch('cust_bill', {
997 'custnum' => $self->custnum,
999 $total_bill += $cust_bill->owed;
1001 sprintf( "%.2f", $total_bill );
1004 =item total_credited
1006 Returns the total credits (see L<FS::cust_credit>) for this customer.
1010 sub total_credited {
1012 my $total_credit = 0;
1013 foreach my $cust_credit ( qsearch('cust_credit', {
1014 'custnum' => $self->custnum,
1016 $total_credit += $cust_credit->credited;
1018 sprintf( "%.2f", $total_credit );
1023 Returns the balance for this customer (total owed minus total credited).
1029 sprintf( "%.2f", $self->total_owed - $self->total_credited );
1032 =item invoicing_list [ ARRAYREF ]
1034 If an arguement is given, sets these email addresses as invoice recipients
1035 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1036 (except as warnings), so use check_invoicing_list first.
1038 Returns a list of email addresses (with svcnum entries expanded).
1040 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1041 check it without disturbing anything by passing nothing.
1043 This interface may change in the future.
1047 sub invoicing_list {
1048 my( $self, $arrayref ) = @_;
1050 my @cust_main_invoice;
1051 if ( $self->custnum ) {
1052 @cust_main_invoice =
1053 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1055 @cust_main_invoice = ();
1057 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1058 #warn $cust_main_invoice->destnum;
1059 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1060 #warn $cust_main_invoice->destnum;
1061 my $error = $cust_main_invoice->delete;
1062 warn $error if $error;
1065 if ( $self->custnum ) {
1066 @cust_main_invoice =
1067 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1069 @cust_main_invoice = ();
1071 foreach my $address ( @{$arrayref} ) {
1072 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1073 my $cust_main_invoice = new FS::cust_main_invoice ( {
1074 'custnum' => $self->custnum,
1077 my $error = $cust_main_invoice->insert;
1078 warn $error if $error;
1082 if ( $self->custnum ) {
1084 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1090 =item check_invoicing_list ARRAYREF
1092 Checks these arguements as valid input for the invoicing_list method. If there
1093 is an error, returns the error, otherwise returns false.
1097 sub check_invoicing_list {
1098 my( $self, $arrayref ) = @_;
1099 foreach my $address ( @{$arrayref} ) {
1100 my $cust_main_invoice = new FS::cust_main_invoice ( {
1101 'custnum' => $self->custnum,
1104 my $error = $self->custnum
1105 ? $cust_main_invoice->check
1106 : $cust_main_invoice->checkdest
1108 return $error if $error;
1117 $Id: cust_main.pm,v 1.11 2001-04-09 23:05:15 ivan Exp $
1123 The delete method should possibly take an FS::cust_main object reference
1124 instead of a scalar customer number.
1126 Bill and collect options should probably be passed as references instead of a
1129 CyberCash v2 forces us to define some variables in package main.
1131 There should probably be a configuration file with a list of allowed credit
1134 CyberCash is the only processor.
1136 No multiple currency support (probably a larger project than just this module).
1140 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1141 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1142 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1143 L<FS::UID>, schema.html from the base documentation.