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 ],
198 $cust_main->insert( \%hash );
206 local $SIG{HUP} = 'IGNORE';
207 local $SIG{INT} = 'IGNORE';
208 local $SIG{QUIT} = 'IGNORE';
209 local $SIG{TERM} = 'IGNORE';
210 local $SIG{TSTP} = 'IGNORE';
211 local $SIG{PIPE} = 'IGNORE';
213 my $oldAutoCommit = $FS::UID::AutoCommit;
214 local $FS::UID::AutoCommit = 0;
219 if ( $self->payby eq 'PREPAY' ) {
220 $self->payby('BILL');
221 my $prepay_credit = qsearchs(
223 { 'identifier' => $self->payinfo },
227 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
228 unless $prepay_credit;
229 $amount = $prepay_credit->amount;
230 $seconds = $prepay_credit->seconds;
231 my $error = $prepay_credit->delete;
233 $dbh->rollback if $oldAutoCommit;
238 my $error = $self->SUPER::insert;
240 $dbh->rollback if $oldAutoCommit;
245 my $cust_pkgs = shift @param;
246 foreach my $cust_pkg ( keys %$cust_pkgs ) {
247 $cust_pkg->custnum( $self->custnum );
248 $error = $cust_pkg->insert;
250 $dbh->rollback if $oldAutoCommit;
253 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
254 $svc_something->pkgnum( $cust_pkg->pkgnum );
255 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
256 $svc_something->seconds( $svc_something->seconds + $seconds );
259 $error = $svc_something->insert;
261 $dbh->rollback if $oldAutoCommit;
269 $dbh->rollback if $oldAutoCommit;
270 return "No svc_acct record to apply pre-paid time";
274 my $cust_credit = new FS::cust_credit {
275 'custnum' => $self->custnum,
278 $error = $cust_credit->insert;
280 $dbh->rollback if $oldAutoCommit;
285 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
290 =item delete NEW_CUSTNUM
292 This deletes the customer. If there is an error, returns the error, otherwise
295 This will completely remove all traces of the customer record. This is not
296 what you want when a customer cancels service; for that, cancel all of the
297 customer's packages (see L<FS::cust_pkg/cancel>).
299 If the customer has any packages, you need to pass a new (valid) customer
300 number for those packages to be transferred to.
302 You can't delete a customer with invoices (see L<FS::cust_bill>),
303 or credits (see L<FS::cust_credit>).
310 local $SIG{HUP} = 'IGNORE';
311 local $SIG{INT} = 'IGNORE';
312 local $SIG{QUIT} = 'IGNORE';
313 local $SIG{TERM} = 'IGNORE';
314 local $SIG{TSTP} = 'IGNORE';
315 local $SIG{PIPE} = 'IGNORE';
317 my $oldAutoCommit = $FS::UID::AutoCommit;
318 local $FS::UID::AutoCommit = 0;
321 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
322 $dbh->rollback if $oldAutoCommit;
323 return "Can't delete a customer with invoices";
325 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
326 $dbh->rollback if $oldAutoCommit;
327 return "Can't delete a customer with credits";
330 my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
332 my $new_custnum = shift;
333 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
334 $dbh->rollback if $oldAutoCommit;
335 return "Invalid new customer number: $new_custnum";
337 foreach my $cust_pkg ( @cust_pkg ) {
338 my %hash = $cust_pkg->hash;
339 $hash{'custnum'} = $new_custnum;
340 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
341 my $error = $new_cust_pkg->replace($cust_pkg);
343 $dbh->rollback if $oldAutoCommit;
348 foreach my $cust_main_invoice (
349 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
351 my $error = $cust_main_invoice->delete;
353 $dbh->rollback if $oldAutoCommit;
358 my $error = $self->SUPER::delete;
360 $dbh->rollback if $oldAutoCommit;
364 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
369 =item replace OLD_RECORD
371 Replaces the OLD_RECORD with this one in the database. If there is an error,
372 returns the error, otherwise returns false.
376 Checks all fields to make sure this is a valid customer record. If there is
377 an error, returns the error, otherwise returns false. Called by the insert
386 $self->ut_numbern('custnum')
387 || $self->ut_number('agentnum')
388 || $self->ut_number('refnum')
389 || $self->ut_textn('company')
390 || $self->ut_text('address1')
391 || $self->ut_textn('address2')
392 || $self->ut_text('city')
393 || $self->ut_textn('county')
394 || $self->ut_textn('state')
396 #barf. need message catalogs. i18n. etc.
397 $error .= "Please select a referral."
398 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
399 return $error if $error;
401 return "Unknown agent"
402 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
404 return "Unknown referral"
405 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
407 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/
408 or return "Illegal last name: ". $self->getfield('last');
409 $self->setfield('last',$1);
411 $self->first =~ /^([\w \,\.\-\']+)$/
412 or return "Illegal first name: ". $self->first;
415 if ( $self->ss eq '' ) {
420 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
421 or return "Illegal social security number: ". $self->ss;
422 $self->ss("$1-$2-$3");
425 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
427 unless ( qsearchs('cust_main_county', {
428 'country' => $self->country,
431 return "Unknown state/county/country: ".
432 $self->state. "/". $self->county. "/". $self->country
433 unless qsearchs('cust_main_county',{
434 'state' => $self->state,
435 'county' => $self->county,
436 'country' => $self->country,
441 $self->ut_phonen('daytime', $self->country)
442 || $self->ut_phonen('night', $self->country)
443 || $self->ut_phonen('fax', $self->country)
445 return $error if $error;
447 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
448 or return "Illegal zip: ". $self->zip;
451 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
452 or return "Illegal payby: ". $self->payby;
455 if ( $self->payby eq 'CARD' ) {
457 my $payinfo = $self->payinfo;
459 $payinfo =~ /^(\d{13,16})$/
460 or return "Illegal credit card number: ". $self->payinfo;
462 $self->payinfo($payinfo);
464 or return "Illegal credit card number: ". $self->payinfo;
465 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
467 } elsif ( $self->payby eq 'BILL' ) {
469 $error = $self->ut_textn('payinfo');
470 return "Illegal P.O. number: ". $self->payinfo if $error;
472 } elsif ( $self->payby eq 'COMP' ) {
474 $error = $self->ut_textn('payinfo');
475 return "Illegal comp account issuer: ". $self->payinfo if $error;
477 } elsif ( $self->payby eq 'PREPAY' ) {
479 my $payinfo = $self->payinfo;
480 $payinfo =~ s/\W//g; #anything else would just confuse things
481 $self->payinfo($payinfo);
482 $error = $self->ut_alpha('payinfo');
483 return "Illegal prepayment identifier: ". $self->payinfo if $error;
484 return "Unknown prepayment identifier"
485 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
489 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
490 return "Expriation date required"
491 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
494 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
495 or return "Illegal expiration date: ". $self->paydate;
496 if ( length($2) == 4 ) {
497 $self->paydate("$2-$1-01");
498 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
499 $self->paydate("19$2-$1-01");
501 $self->paydate("20$2-$1-01");
505 if ( $self->payname eq '' ) {
506 $self->payname( $self->first. " ". $self->getfield('last') );
508 $self->payname =~ /^([\w \,\.\-\']+)$/
509 or return "Illegal billing name: ". $self->payname;
513 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
516 $self->otaker(getotaker);
523 Returns all packages (see L<FS::cust_pkg>) for this customer.
529 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
532 =item ncancelled_pkgs
534 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
538 sub ncancelled_pkgs {
540 @{ [ # force list context
541 qsearch( 'cust_pkg', {
542 'custnum' => $self->custnum,
545 qsearch( 'cust_pkg', {
546 'custnum' => $self->custnum,
554 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
555 conjunction with the collect method.
557 The only currently available option is `time', which bills the customer as if
558 it were that time. It is specified as a UNIX timestamp; see
559 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
562 If there is an error, returns the error, otherwise returns false.
567 my( $self, %options ) = @_;
568 my $time = $options{'time'} || time;
573 local $SIG{HUP} = 'IGNORE';
574 local $SIG{INT} = 'IGNORE';
575 local $SIG{QUIT} = 'IGNORE';
576 local $SIG{TERM} = 'IGNORE';
577 local $SIG{TSTP} = 'IGNORE';
578 local $SIG{PIPE} = 'IGNORE';
580 my $oldAutoCommit = $FS::UID::AutoCommit;
581 local $FS::UID::AutoCommit = 0;
584 # find the packages which are due for billing, find out how much they are
585 # & generate invoice database.
587 my( $total_setup, $total_recur ) = ( 0, 0 );
590 foreach my $cust_pkg (
591 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
594 next if $cust_pkg->getfield('cancel');
596 #? to avoid use of uninitialized value errors... ?
597 $cust_pkg->setfield('bill', '')
598 unless defined($cust_pkg->bill);
600 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
602 #so we don't modify cust_pkg record unnecessarily
603 my $cust_pkg_mod_flag = 0;
604 my %hash = $cust_pkg->hash;
605 my $old_cust_pkg = new FS::cust_pkg \%hash;
609 unless ( $cust_pkg->setup ) {
610 my $setup_prog = $part_pkg->getfield('setup');
612 #$cpt->permit(); #what is necessary?
613 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
614 $setup = $cpt->reval($setup_prog);
615 unless ( defined($setup) ) {
616 warn "Error reval-ing part_pkg->setup pkgpart ",
617 $part_pkg->pkgpart, ": $@";
619 $cust_pkg->setfield('setup',$time);
620 $cust_pkg_mod_flag=1;
627 if ( $part_pkg->getfield('freq') > 0 &&
628 ! $cust_pkg->getfield('susp') &&
629 ( $cust_pkg->getfield('bill') || 0 ) < $time
631 my $recur_prog = $part_pkg->getfield('recur');
633 #$cpt->permit(); #what is necessary?
634 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
635 $recur = $cpt->reval($recur_prog);
636 unless ( defined($recur) ) {
637 warn "Error reval-ing part_pkg->recur pkgpart ",
638 $part_pkg->pkgpart, ": $@";
640 #change this bit to use Date::Manip? CAREFUL with timezones (see
641 # mailing list archive)
642 #$sdate=$cust_pkg->bill || time;
643 #$sdate=$cust_pkg->bill || $time;
644 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
645 my ($sec,$min,$hour,$mday,$mon,$year) =
646 (localtime($sdate) )[0,1,2,3,4,5];
647 $mon += $part_pkg->getfield('freq');
648 until ( $mon < 12 ) { $mon -= 12; $year++; }
649 $cust_pkg->setfield('bill',
650 timelocal($sec,$min,$hour,$mday,$mon,$year));
651 $cust_pkg_mod_flag = 1;
655 warn "setup is undefined" unless defined($setup);
656 warn "recur is undefined" unless defined($recur);
657 warn "cust_pkg bill is undefined" unless defined($cust_pkg->bill);
659 if ( $cust_pkg_mod_flag ) {
660 $error=$cust_pkg->replace($old_cust_pkg);
661 if ( $error ) { #just in case
662 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
664 $setup = sprintf( "%.2f", $setup );
665 $recur = sprintf( "%.2f", $recur );
666 my $cust_bill_pkg = new FS::cust_bill_pkg ({
667 'pkgnum' => $cust_pkg->pkgnum,
671 'edate' => $cust_pkg->bill,
673 push @cust_bill_pkg, $cust_bill_pkg;
674 $total_setup += $setup;
675 $total_recur += $recur;
681 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
683 unless ( @cust_bill_pkg ) {
684 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
688 unless ( $self->getfield('tax') =~ /Y/i
689 || $self->getfield('payby') eq 'COMP'
691 my $cust_main_county = qsearchs('cust_main_county',{
692 'state' => $self->state,
693 'county' => $self->county,
694 'country' => $self->country,
696 my $tax = sprintf( "%.2f",
697 $charged * ( $cust_main_county->getfield('tax') / 100 )
699 $charged = sprintf( "%.2f", $charged+$tax );
701 my $cust_bill_pkg = new FS::cust_bill_pkg ({
708 push @cust_bill_pkg, $cust_bill_pkg;
711 my $cust_bill = new FS::cust_bill ( {
712 'custnum' => $self->getfield('custnum'),
714 'charged' => $charged,
716 $error = $cust_bill->insert;
718 $dbh->rollback if $oldAutoCommit;
719 return "$error for customer #". $self->custnum;
722 my $invnum = $cust_bill->invnum;
724 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
725 $cust_bill_pkg->setfield( 'invnum', $invnum );
726 $error = $cust_bill_pkg->insert;
727 #shouldn't happen, but how else tohandle this?
729 $dbh->rollback if $oldAutoCommit;
730 return "$error for customer #". $self->custnum;
734 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
738 =item collect OPTIONS
740 (Attempt to) collect money for this customer's outstanding invoices (see
741 L<FS::cust_bill>). Usually used after the bill method.
743 Depending on the value of `payby', this may print an invoice (`BILL'), charge
744 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
746 If there is an error, returns the error, otherwise returns false.
748 Currently available options are:
750 invoice_time - Use this time when deciding when to print invoices and
751 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>
752 for conversion functions.
754 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
755 default, cards are processed immediately, which will generate an error if
756 CyberCash is not installed.
758 report_badcard - Set this true if you want bad card transactions to
759 return an error. By default, they don't.
764 my( $self, %options ) = @_;
765 my $invoice_time = $options{'invoice_time'} || time;
768 local $SIG{HUP} = 'IGNORE';
769 local $SIG{INT} = 'IGNORE';
770 local $SIG{QUIT} = 'IGNORE';
771 local $SIG{TERM} = 'IGNORE';
772 local $SIG{TSTP} = 'IGNORE';
773 local $SIG{PIPE} = 'IGNORE';
775 my $oldAutoCommit = $FS::UID::AutoCommit;
776 local $FS::UID::AutoCommit = 0;
779 my $total_owed = $self->balance;
780 warn "collect: total owed $total_owed " if $Debug;
781 unless ( $total_owed > 0 ) { #redundant?????
782 $dbh->rollback if $oldAutoCommit;
786 foreach my $cust_bill (
787 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
790 #this has to be before next's
791 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
795 $total_owed = sprintf( "%.2f", $total_owed - $amount );
797 next unless $cust_bill->owed > 0;
799 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
801 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)" if $Debug;
803 next unless $amount > 0;
805 if ( $self->payby eq 'BILL' ) {
808 my $since = $invoice_time - ( $cust_bill->_date || 0 );
809 #warn "$invoice_time ", $cust_bill->_date, " $since";
810 if ( $since >= 0 #don't print future invoices
811 && ( $cust_bill->printed * 2592000 ) <= $since
814 #my @print_text = $cust_bill->print_text; #( date )
815 my @invoicing_list = $self->invoicing_list;
816 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
817 $ENV{SMTPHOSTS} = $smtpmachine;
818 $ENV{MAILADDRESS} = $invoice_from;
819 my $header = new Mail::Header ( [
820 "From: $invoice_from",
821 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
822 "Sender: $invoice_from",
823 "Reply-To: $invoice_from",
824 "Date: ". time2str("%a, %d %b %Y %X %z", time),
827 my $message = new Mail::Internet (
829 'Body' => [ $cust_bill->print_text ], #( date)
831 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
833 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
834 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
835 print LPR $cust_bill->print_text; #( date )
837 or die $! ? "Error closing $lpr: $!"
838 : "Exit status $? from $lpr";
841 my %hash = $cust_bill->hash;
843 my $new_cust_bill = new FS::cust_bill(\%hash);
844 my $error = $new_cust_bill->replace($cust_bill);
845 warn "Error updating $cust_bill->printed: $error" if $error;
849 } elsif ( $self->payby eq 'COMP' ) {
850 my $cust_pay = new FS::cust_pay ( {
851 'invnum' => $cust_bill->invnum,
855 'payinfo' => $self->payinfo,
858 my $error = $cust_pay->insert;
860 $dbh->rollback if $oldAutoCommit;
861 return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
865 } elsif ( $self->payby eq 'CARD' ) {
867 if ( $options{'batch_card'} ne 'yes' ) {
869 unless ( $processor ) {
870 $dbh->rollback if $oldAutoCommit;
871 return "Real time card processing not enabled!";
874 if ( $processor =~ /^cybercash/ ) {
876 #fix exp. date for cybercash
877 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
878 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
881 my $paybatch = $cust_bill->invnum.
882 '-' . time2str("%y%m%d%H%M%S", time);
884 my $payname = $self->payname ||
885 $self->getfield('first'). ' '. $self->getfield('last');
887 my $address = $self->address1;
888 $address .= ", ". $self->address2 if $self->address2;
890 my $country = 'USA' if $self->country eq 'US';
892 my @full_xaction = ( $xaction,
893 'Order-ID' => $paybatch,
894 'Amount' => "usd $amount",
895 'Card-Number' => $self->getfield('payinfo'),
896 'Card-Name' => $payname,
897 'Card-Address' => $address,
898 'Card-City' => $self->getfield('city'),
899 'Card-State' => $self->getfield('state'),
900 'Card-Zip' => $self->getfield('zip'),
901 'Card-Country' => $country,
906 if ( $processor eq 'cybercash2' ) {
907 $^W=0; #CCLib isn't -w safe, ugh!
908 %result = &CCLib::sendmserver(@full_xaction);
910 } elsif ( $processor eq 'cybercash3.2' ) {
911 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
913 $dbh->rollback if $oldAutoCommit;
914 return "Unknown real-time processor $processor";
917 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
918 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
919 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
920 my $cust_pay = new FS::cust_pay ( {
921 'invnum' => $cust_bill->invnum,
925 'payinfo' => $self->payinfo,
926 'paybatch' => "$processor:$paybatch",
928 my $error = $cust_pay->insert;
930 # gah, even with transactions.
931 $dbh->commit if $oldAutoCommit; #well.
932 my $e = 'WARNING: Card debited but database not updated - '.
933 'error applying payment, invnum #' . $cust_bill->invnum.
934 " (CyberCash Order-ID $paybatch): $error";
938 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
939 || $options{'report_badcard'} ) {
940 $dbh->commit if $oldAutoCommit;
941 return 'Cybercash error, invnum #' .
942 $cust_bill->invnum. ':'. $result{'MErrMsg'};
944 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
949 $dbh->rollback if $oldAutoCommit;
950 return "Unknown real-time processor $processor\n";
955 my $cust_pay_batch = new FS::cust_pay_batch ( {
956 'invnum' => $cust_bill->getfield('invnum'),
957 'custnum' => $self->getfield('custnum'),
958 'last' => $self->getfield('last'),
959 'first' => $self->getfield('first'),
960 'address1' => $self->getfield('address1'),
961 'address2' => $self->getfield('address2'),
962 'city' => $self->getfield('city'),
963 'state' => $self->getfield('state'),
964 'zip' => $self->getfield('zip'),
965 'country' => $self->getfield('country'),
967 'cardnum' => $self->getfield('payinfo'),
968 'exp' => $self->getfield('paydate'),
969 'payname' => $self->getfield('payname'),
972 my $error = $cust_pay_batch->insert;
974 $dbh->rollback if $oldAutoCommit;
975 return "Error adding to cust_pay_batch: $error";
981 $dbh->rollback if $oldAutoCommit;
982 return "Unknown payment type ". $self->payby;
986 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
993 Returns the total owed for this customer on all invoices
994 (see L<FS::cust_bill>).
1001 foreach my $cust_bill ( qsearch('cust_bill', {
1002 'custnum' => $self->custnum,
1004 $total_bill += $cust_bill->owed;
1006 sprintf( "%.2f", $total_bill );
1009 =item total_credited
1011 Returns the total credits (see L<FS::cust_credit>) for this customer.
1015 sub total_credited {
1017 my $total_credit = 0;
1018 foreach my $cust_credit ( qsearch('cust_credit', {
1019 'custnum' => $self->custnum,
1021 $total_credit += $cust_credit->credited;
1023 sprintf( "%.2f", $total_credit );
1028 Returns the balance for this customer (total owed minus total credited).
1034 sprintf( "%.2f", $self->total_owed - $self->total_credited );
1037 =item invoicing_list [ ARRAYREF ]
1039 If an arguement is given, sets these email addresses as invoice recipients
1040 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1041 (except as warnings), so use check_invoicing_list first.
1043 Returns a list of email addresses (with svcnum entries expanded).
1045 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1046 check it without disturbing anything by passing nothing.
1048 This interface may change in the future.
1052 sub invoicing_list {
1053 my( $self, $arrayref ) = @_;
1055 my @cust_main_invoice;
1056 if ( $self->custnum ) {
1057 @cust_main_invoice =
1058 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1060 @cust_main_invoice = ();
1062 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1063 #warn $cust_main_invoice->destnum;
1064 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1065 #warn $cust_main_invoice->destnum;
1066 my $error = $cust_main_invoice->delete;
1067 warn $error if $error;
1070 if ( $self->custnum ) {
1071 @cust_main_invoice =
1072 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1074 @cust_main_invoice = ();
1076 foreach my $address ( @{$arrayref} ) {
1077 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1078 my $cust_main_invoice = new FS::cust_main_invoice ( {
1079 'custnum' => $self->custnum,
1082 my $error = $cust_main_invoice->insert;
1083 warn $error if $error;
1087 if ( $self->custnum ) {
1089 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1095 =item check_invoicing_list ARRAYREF
1097 Checks these arguements as valid input for the invoicing_list method. If there
1098 is an error, returns the error, otherwise returns false.
1102 sub check_invoicing_list {
1103 my( $self, $arrayref ) = @_;
1104 foreach my $address ( @{$arrayref} ) {
1105 my $cust_main_invoice = new FS::cust_main_invoice ( {
1106 'custnum' => $self->custnum,
1109 my $error = $self->custnum
1110 ? $cust_main_invoice->check
1111 : $cust_main_invoice->checkdest
1113 return $error if $error;
1122 $Id: cust_main.pm,v 1.13 2001-05-07 02:07:38 ivan Exp $
1128 The delete method should possibly take an FS::cust_main object reference
1129 instead of a scalar customer number.
1131 Bill and collect options should probably be passed as references instead of a
1134 CyberCash v2 forces us to define some variables in package main.
1136 There should probably be a configuration file with a list of allowed credit
1139 CyberCash is the only processor.
1141 No multiple currency support (probably a larger project than just this module).
1145 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1146 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1147 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1148 L<FS::UID>, schema.html from the base documentation.