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 local $FS::UID::AutoCommit = 0;
216 if ( $self->payby eq 'PREPAY' ) {
217 $self->payby('BILL');
218 my $prepay_credit = qsearchs(
220 { 'identifier' => $self->payinfo },
224 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
225 unless $prepay_credit;
226 $amount = $prepay_credit->amount;
227 $seconds = $prepay_credit->seconds;
228 my $error = $prepay_credit->delete;
235 my $error = $self->SUPER::insert;
242 my $cust_pkgs = shift;
243 foreach my $cust_pkg ( keys %$cust_pkgs ) {
244 $cust_pkg->custnum( $self->custnum );
245 $error = $cust_pkg->insert;
250 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
251 $svc_something->pkgnum( $cust_pkg->pkgnum );
252 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
253 $svc_something->seconds( $svc_something->seconds + $seconds );
256 $error = $svc_something->insert;
267 return "No svc_acct record to apply pre-paid time";
271 my $cust_credit = new FS::cust_credit {
272 'custnum' => $self->custnum,
275 $error = $cust_credit->insert;
282 $dbh->commit or die $dbh->errstr;
287 =item delete NEW_CUSTNUM
289 This deletes the customer. If there is an error, returns the error, otherwise
292 This will completely remove all traces of the customer record. This is not
293 what you want when a customer cancels service; for that, cancel all of the
294 customer's packages (see L<FS::cust_pkg/cancel>).
296 If the customer has any packages, you need to pass a new (valid) customer
297 number for those packages to be transferred to.
299 You can't delete a customer with invoices (see L<FS::cust_bill>),
300 or credits (see L<FS::cust_credit>).
307 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
308 return "Can't delete a customer with invoices";
310 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
311 return "Can't delete a customer with credits";
314 local $SIG{HUP} = 'IGNORE';
315 local $SIG{INT} = 'IGNORE';
316 local $SIG{QUIT} = 'IGNORE';
317 local $SIG{TERM} = 'IGNORE';
318 local $SIG{TSTP} = 'IGNORE';
319 local $SIG{PIPE} = 'IGNORE';
321 my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
323 my $new_custnum = shift;
324 return "Invalid new customer number: $new_custnum"
325 unless qsearchs( 'cust_main', { 'custnum' => $new_custnum } );
326 foreach my $cust_pkg ( @cust_pkg ) {
327 my %hash = $cust_pkg->hash;
328 $hash{'custnum'} = $new_custnum;
329 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
330 my $error = $new_cust_pkg->replace($cust_pkg);
331 return $error if $error;
334 foreach my $cust_main_invoice (
335 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
337 my $error = $cust_main_invoice->delete;
338 return $error if $error;
341 $self->SUPER::delete;
344 =item replace OLD_RECORD
346 Replaces the OLD_RECORD with this one in the database. If there is an error,
347 returns the error, otherwise returns false.
351 Checks all fields to make sure this is a valid customer record. If there is
352 an error, returns the error, otherwise returns false. Called by the insert
361 $self->ut_numbern('custnum')
362 || $self->ut_number('agentnum')
363 || $self->ut_number('refnum')
364 || $self->ut_textn('company')
365 || $self->ut_text('address1')
366 || $self->ut_textn('address2')
367 || $self->ut_text('city')
368 || $self->ut_textn('county')
369 || $self->ut_textn('state')
371 return $error if $error;
373 return "Unknown agent"
374 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
376 return "Unknown referral"
377 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
379 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/
380 or return "Illegal last name: ". $self->getfield('last');
381 $self->setfield('last',$1);
383 $self->first =~ /^([\w \,\.\-\']+)$/
384 or return "Illegal first name: ". $self->first;
387 if ( $self->ss eq '' ) {
392 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
393 or return "Illegal social security number: ". $self->ss;
394 $self->ss("$1-$2-$3");
397 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
399 unless ( qsearchs('cust_main_county', {
400 'country' => $self->country,
403 return "Unknown state/county/country: ".
404 $self->state. "/". $self->county. "/". $self->country
405 unless qsearchs('cust_main_county',{
406 'state' => $self->state,
407 'county' => $self->county,
408 'country' => $self->country,
413 $self->ut_phonen('daytime', $self->country)
414 || $self->ut_phonen('night', $self->country)
415 || $self->ut_phonen('fax', $self->country)
417 return $error if $error;
419 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
420 or return "Illegal zip: ". $self->zip;
423 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
424 or return "Illegal payby: ". $self->payby;
427 if ( $self->payby eq 'CARD' ) {
429 my $payinfo = $self->payinfo;
431 $payinfo =~ /^(\d{13,16})$/
432 or return "Illegal credit card number: ". $self->payinfo;
434 $self->payinfo($payinfo);
436 or return "Illegal credit card number: ". $self->payinfo;
437 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
439 } elsif ( $self->payby eq 'BILL' ) {
441 $error = $self->ut_textn('payinfo');
442 return "Illegal P.O. number: ". $self->payinfo if $error;
444 } elsif ( $self->payby eq 'COMP' ) {
446 $error = $self->ut_textn('payinfo');
447 return "Illegal comp account issuer: ". $self->payinfo if $error;
449 } elsif ( $self->payby eq 'PREPAY' ) {
451 my $payinfo = $self->payinfo;
452 $payinfo =~ s/\W//g; #anything else would just confuse things
453 $self->payinfo($payinfo);
454 $error = $self->ut_alpha('payinfo');
455 return "Illegal prepayment identifier: ". $self->payinfo if $error;
456 return "Unknown prepayment identifier"
457 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
461 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
462 return "Expriation date required"
463 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
466 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
467 or return "Illegal expiration date: ". $self->paydate;
468 if ( length($2) == 4 ) {
469 $self->paydate("$2-$1-01");
470 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
471 $self->paydate("19$2-$1-01");
473 $self->paydate("20$2-$1-01");
477 if ( $self->payname eq '' ) {
478 $self->payname( $self->first. " ". $self->getfield('last') );
480 $self->payname =~ /^([\w \,\.\-\']+)$/
481 or return "Illegal billing name: ". $self->payname;
485 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
488 $self->otaker(getotaker);
495 Returns all packages (see L<FS::cust_pkg>) for this customer.
501 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
504 =item ncancelled_pkgs
506 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
510 sub ncancelled_pkgs {
512 @{ [ # force list context
513 qsearch( 'cust_pkg', {
514 'custnum' => $self->custnum,
517 qsearch( 'cust_pkg', {
518 'custnum' => $self->custnum,
526 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
527 conjunction with the collect method.
529 The only currently available option is `time', which bills the customer as if
530 it were that time. It is specified as a UNIX timestamp; see
531 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
534 If there is an error, returns the error, otherwise returns false.
539 my( $self, %options ) = @_;
540 my $time = $options{'time'} || time;
545 local $SIG{HUP} = 'IGNORE';
546 local $SIG{INT} = 'IGNORE';
547 local $SIG{QUIT} = 'IGNORE';
548 local $SIG{TERM} = 'IGNORE';
549 local $SIG{TSTP} = 'IGNORE';
550 local $SIG{PIPE} = 'IGNORE';
552 # find the packages which are due for billing, find out how much they are
553 # & generate invoice database.
555 my( $total_setup, $total_recur ) = ( 0, 0 );
558 foreach my $cust_pkg (
559 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
562 next if $cust_pkg->getfield('cancel');
564 #? to avoid use of uninitialized value errors... ?
565 $cust_pkg->setfield('bill', '')
566 unless defined($cust_pkg->bill);
568 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
570 #so we don't modify cust_pkg record unnecessarily
571 my $cust_pkg_mod_flag = 0;
572 my %hash = $cust_pkg->hash;
573 my $old_cust_pkg = new FS::cust_pkg \%hash;
577 unless ( $cust_pkg->setup ) {
578 my $setup_prog = $part_pkg->getfield('setup');
580 #$cpt->permit(); #what is necessary?
581 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
582 $setup = $cpt->reval($setup_prog);
583 unless ( defined($setup) ) {
584 warn "Error reval-ing part_pkg->setup pkgpart ",
585 $part_pkg->pkgpart, ": $@";
587 $cust_pkg->setfield('setup',$time);
588 $cust_pkg_mod_flag=1;
595 if ( $part_pkg->getfield('freq') > 0 &&
596 ! $cust_pkg->getfield('susp') &&
597 ( $cust_pkg->getfield('bill') || 0 ) < $time
599 my $recur_prog = $part_pkg->getfield('recur');
601 #$cpt->permit(); #what is necessary?
602 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
603 $recur = $cpt->reval($recur_prog);
604 unless ( defined($recur) ) {
605 warn "Error reval-ing part_pkg->recur pkgpart ",
606 $part_pkg->pkgpart, ": $@";
608 #change this bit to use Date::Manip? CAREFUL with timezones (see
609 # mailing list archive)
610 #$sdate=$cust_pkg->bill || time;
611 #$sdate=$cust_pkg->bill || $time;
612 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
613 my ($sec,$min,$hour,$mday,$mon,$year) =
614 (localtime($sdate) )[0,1,2,3,4,5];
615 $mon += $part_pkg->getfield('freq');
616 until ( $mon < 12 ) { $mon -= 12; $year++; }
617 $cust_pkg->setfield('bill',
618 timelocal($sec,$min,$hour,$mday,$mon,$year));
619 $cust_pkg_mod_flag = 1;
623 warn "setup is undefined" unless defined($setup);
624 warn "recur is undefined" unless defined($recur);
625 warn "cust_pkg bill is undefined" unless defined($cust_pkg->bill);
627 if ( $cust_pkg_mod_flag ) {
628 $error=$cust_pkg->replace($old_cust_pkg);
629 if ( $error ) { #just in case
630 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
632 $setup = sprintf( "%.2f", $setup );
633 $recur = sprintf( "%.2f", $recur );
634 my $cust_bill_pkg = new FS::cust_bill_pkg ({
635 'pkgnum' => $cust_pkg->pkgnum,
639 'edate' => $cust_pkg->bill,
641 push @cust_bill_pkg, $cust_bill_pkg;
642 $total_setup += $setup;
643 $total_recur += $recur;
649 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
651 return '' if scalar(@cust_bill_pkg) == 0;
653 unless ( $self->getfield('tax') =~ /Y/i
654 || $self->getfield('payby') eq 'COMP'
656 my $cust_main_county = qsearchs('cust_main_county',{
657 'state' => $self->state,
658 'county' => $self->county,
659 'country' => $self->country,
661 my $tax = sprintf( "%.2f",
662 $charged * ( $cust_main_county->getfield('tax') / 100 )
664 $charged = sprintf( "%.2f", $charged+$tax );
666 my $cust_bill_pkg = new FS::cust_bill_pkg ({
673 push @cust_bill_pkg, $cust_bill_pkg;
676 my $cust_bill = new FS::cust_bill ( {
677 'custnum' => $self->getfield('custnum'),
679 'charged' => $charged,
681 $error = $cust_bill->insert;
682 #shouldn't happen, but how else to handle this? (wrap me in eval, to catch
684 die "Error creating cust_bill record: $error!\n",
685 "Check updated but unbilled packages for customer", $self->custnum, "\n"
688 my $invnum = $cust_bill->invnum;
690 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
691 $cust_bill_pkg->setfield( 'invnum', $invnum );
692 $error = $cust_bill_pkg->insert;
693 #shouldn't happen, but how else tohandle this?
694 die "Error creating cust_bill_pkg record: $error!\n",
695 "Check incomplete invoice ", $invnum, "\n"
702 =item collect OPTIONS
704 (Attempt to) collect money for this customer's outstanding invoices (see
705 L<FS::cust_bill>). Usually used after the bill method.
707 Depending on the value of `payby', this may print an invoice (`BILL'), charge
708 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
710 If there is an error, returns the error, otherwise returns false.
712 Currently available options are:
714 invoice_time - Use this time when deciding when to print invoices and
715 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>
716 for conversion functions.
718 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
719 default, cards are processed immediately, which will generate an error if
720 CyberCash is not installed.
722 report_badcard - Set this true if you want bad card transactions to
723 return an error. By default, they don't.
728 my( $self, %options ) = @_;
729 my $invoice_time = $options{'invoice_time'} || time;
731 my $total_owed = $self->balance;
732 warn "collect: total owed $total_owed " if $Debug;
733 return '' unless $total_owed > 0; #redundant?????
736 local $SIG{HUP} = 'IGNORE';
737 local $SIG{INT} = 'IGNORE';
738 local $SIG{QUIT} = 'IGNORE';
739 local $SIG{TERM} = 'IGNORE';
740 local $SIG{TSTP} = 'IGNORE';
741 local $SIG{PIPE} = 'IGNORE';
743 foreach my $cust_bill (
744 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
747 #this has to be before next's
748 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
752 $total_owed = sprintf( "%.2f", $total_owed - $amount );
754 next unless $cust_bill->owed > 0;
756 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
758 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)" if $Debug;
760 next unless $amount > 0;
762 if ( $self->payby eq 'BILL' ) {
765 my $since = $invoice_time - ( $cust_bill->_date || 0 );
766 #warn "$invoice_time ", $cust_bill->_date, " $since";
767 if ( $since >= 0 #don't print future invoices
768 && ( $cust_bill->printed * 2592000 ) <= $since
771 #my @print_text = $cust_bill->print_text; #( date )
772 my @invoicing_list = $self->invoicing_list;
773 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
774 $ENV{SMTPHOSTS} = $smtpmachine;
775 $ENV{MAILADDRESS} = $invoice_from;
776 my $header = new Mail::Header ( [
777 "From: $invoice_from",
778 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
779 "Sender: $invoice_from",
780 "Reply-To: $invoice_from",
781 "Date: ". time2str("%a, %d %b %Y %X %z", time),
784 my $message = new Mail::Internet (
786 'Body' => [ $cust_bill->print_text ], #( date)
788 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
790 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
791 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
792 print LPR $cust_bill->print_text; #( date )
794 or die $! ? "Error closing $lpr: $!"
795 : "Exit status $? from $lpr";
798 my %hash = $cust_bill->hash;
800 my $new_cust_bill = new FS::cust_bill(\%hash);
801 my $error = $new_cust_bill->replace($cust_bill);
802 warn "Error updating $cust_bill->printed: $error" if $error;
806 } elsif ( $self->payby eq 'COMP' ) {
807 my $cust_pay = new FS::cust_pay ( {
808 'invnum' => $cust_bill->invnum,
812 'payinfo' => $self->payinfo,
815 my $error = $cust_pay->insert;
816 return 'Error COMPing invnum #' . $cust_bill->invnum .
817 ':' . $error if $error;
819 } elsif ( $self->payby eq 'CARD' ) {
821 if ( $options{'batch_card'} ne 'yes' ) {
823 return "Real time card processing not enabled!" unless $processor;
825 if ( $processor =~ /^cybercash/ ) {
827 #fix exp. date for cybercash
828 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
829 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
832 my $paybatch = $cust_bill->invnum.
833 '-' . time2str("%y%m%d%H%M%S", time);
835 my $payname = $self->payname ||
836 $self->getfield('first'). ' '. $self->getfield('last');
838 my $address = $self->address1;
839 $address .= ", ". $self->address2 if $self->address2;
841 my $country = 'USA' if $self->country eq 'US';
843 my @full_xaction = ( $xaction,
844 'Order-ID' => $paybatch,
845 'Amount' => "usd $amount",
846 'Card-Number' => $self->getfield('payinfo'),
847 'Card-Name' => $payname,
848 'Card-Address' => $address,
849 'Card-City' => $self->getfield('city'),
850 'Card-State' => $self->getfield('state'),
851 'Card-Zip' => $self->getfield('zip'),
852 'Card-Country' => $country,
857 if ( $processor eq 'cybercash2' ) {
858 $^W=0; #CCLib isn't -w safe, ugh!
859 %result = &CCLib::sendmserver(@full_xaction);
861 } elsif ( $processor eq 'cybercash3.2' ) {
862 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
864 return "Unknown real-time processor $processor\n";
867 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
868 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
869 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
870 my $cust_pay = new FS::cust_pay ( {
871 'invnum' => $cust_bill->invnum,
875 'payinfo' => $self->payinfo,
876 'paybatch' => "$processor:$paybatch",
878 my $error = $cust_pay->insert;
879 return 'Error applying payment, invnum #' .
880 $cust_bill->invnum. ':'. $error if $error;
881 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
882 || $options{'report_badcard'} ) {
883 return 'Cybercash error, invnum #' .
884 $cust_bill->invnum. ':'. $result{'MErrMsg'};
890 return "Unknown real-time processor $processor\n";
895 my $cust_pay_batch = new FS::cust_pay_batch ( {
896 'invnum' => $cust_bill->getfield('invnum'),
897 'custnum' => $self->getfield('custnum'),
898 'last' => $self->getfield('last'),
899 'first' => $self->getfield('first'),
900 'address1' => $self->getfield('address1'),
901 'address2' => $self->getfield('address2'),
902 'city' => $self->getfield('city'),
903 'state' => $self->getfield('state'),
904 'zip' => $self->getfield('zip'),
905 'country' => $self->getfield('country'),
907 'cardnum' => $self->getfield('payinfo'),
908 'exp' => $self->getfield('paydate'),
909 'payname' => $self->getfield('payname'),
912 my $error = $cust_pay_batch->insert;
913 return "Error adding to cust_pay_batch: $error" if $error;
918 return "Unknown payment type ". $self->payby;
928 Returns the total owed for this customer on all invoices
929 (see L<FS::cust_bill>).
936 foreach my $cust_bill ( qsearch('cust_bill', {
937 'custnum' => $self->custnum,
939 $total_bill += $cust_bill->owed;
941 sprintf( "%.2f", $total_bill );
946 Returns the total credits (see L<FS::cust_credit>) for this customer.
952 my $total_credit = 0;
953 foreach my $cust_credit ( qsearch('cust_credit', {
954 'custnum' => $self->custnum,
956 $total_credit += $cust_credit->credited;
958 sprintf( "%.2f", $total_credit );
963 Returns the balance for this customer (total owed minus total credited).
969 sprintf( "%.2f", $self->total_owed - $self->total_credited );
972 =item invoicing_list [ ARRAYREF ]
974 If an arguement is given, sets these email addresses as invoice recipients
975 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
976 (except as warnings), so use check_invoicing_list first.
978 Returns a list of email addresses (with svcnum entries expanded).
980 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
981 check it without disturbing anything by passing nothing.
983 This interface may change in the future.
988 my( $self, $arrayref ) = @_;
990 my @cust_main_invoice;
991 if ( $self->custnum ) {
993 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
995 @cust_main_invoice = ();
997 foreach my $cust_main_invoice ( @cust_main_invoice ) {
998 #warn $cust_main_invoice->destnum;
999 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1000 #warn $cust_main_invoice->destnum;
1001 my $error = $cust_main_invoice->delete;
1002 warn $error if $error;
1005 if ( $self->custnum ) {
1006 @cust_main_invoice =
1007 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1009 @cust_main_invoice = ();
1011 foreach my $address ( @{$arrayref} ) {
1012 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1013 my $cust_main_invoice = new FS::cust_main_invoice ( {
1014 'custnum' => $self->custnum,
1017 my $error = $cust_main_invoice->insert;
1018 warn $error if $error;
1022 if ( $self->custnum ) {
1024 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1030 =item check_invoicing_list ARRAYREF
1032 Checks these arguements as valid input for the invoicing_list method. If there
1033 is an error, returns the error, otherwise returns false.
1037 sub check_invoicing_list {
1038 my( $self, $arrayref ) = @_;
1039 foreach my $address ( @{$arrayref} ) {
1040 my $cust_main_invoice = new FS::cust_main_invoice ( {
1041 'custnum' => $self->custnum,
1044 my $error = $self->custnum
1045 ? $cust_main_invoice->check
1046 : $cust_main_invoice->checkdest
1048 return $error if $error;
1057 $Id: cust_main.pm,v 1.10 2001-02-03 14:03:50 ivan Exp $
1063 The delete method should possibly take an FS::cust_main object reference
1064 instead of a scalar customer number.
1066 Bill and collect options should probably be passed as references instead of a
1069 CyberCash v2 forces us to define some variables in package main.
1071 There should probably be a configuration file with a list of allowed credit
1074 CyberCash is the only processor.
1076 No multiple currency support (probably a larger project than just this module).
1080 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1081 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1082 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1083 L<FS::UID>, schema.html from the base documentation.