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 );
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.
192 if ( $self->payby eq 'PREPAY' ) {
193 $self->payby('BILL');
197 local $SIG{HUP} = 'IGNORE';
198 local $SIG{INT} = 'IGNORE';
199 local $SIG{QUIT} = 'IGNORE';
200 local $SIG{TERM} = 'IGNORE';
201 local $SIG{TSTP} = 'IGNORE';
202 local $SIG{PIPE} = 'IGNORE';
204 my $error = $self->SUPER::insert;
205 return $error if $error;
209 qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
210 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
211 unless $prepay_credit;
212 my $amount = $prepay_credit->amount;
213 my $error = $prepay_credit->delete;
215 warn "WARNING: can't delete prepay_credit: ". $self->payinfo;
217 my $cust_credit = new FS::cust_credit {
218 'custnum' => $self->custnum,
221 my $error = $cust_credit->insert;
222 warn "WARNING: error inserting cust_credit for prepay_credit: $error"
232 =item delete NEW_CUSTNUM
234 This deletes the customer. If there is an error, returns the error, otherwise
237 This will completely remove all traces of the customer record. This is not
238 what you want when a customer cancels service; for that, cancel all of the
239 customer's packages (see L<FS::cust_pkg/cancel>).
241 If the customer has any packages, you need to pass a new (valid) customer
242 number for those packages to be transferred to.
244 You can't delete a customer with invoices (see L<FS::cust_bill>),
245 or credits (see L<FS::cust_credit>).
252 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
253 return "Can't delete a customer with invoices";
255 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
256 return "Can't delete a customer with credits";
259 local $SIG{HUP} = 'IGNORE';
260 local $SIG{INT} = 'IGNORE';
261 local $SIG{QUIT} = 'IGNORE';
262 local $SIG{TERM} = 'IGNORE';
263 local $SIG{TSTP} = 'IGNORE';
264 local $SIG{PIPE} = 'IGNORE';
266 my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
268 my $new_custnum = shift;
269 return "Invalid new customer number: $new_custnum"
270 unless qsearchs( 'cust_main', { 'custnum' => $new_custnum } );
271 foreach my $cust_pkg ( @cust_pkg ) {
272 my %hash = $cust_pkg->hash;
273 $hash{'custnum'} = $new_custnum;
274 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
275 my $error = $new_cust_pkg->replace($cust_pkg);
276 return $error if $error;
279 foreach my $cust_main_invoice (
280 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
282 my $error = $cust_main_invoice->delete;
283 return $error if $error;
286 $self->SUPER::delete;
289 =item replace OLD_RECORD
291 Replaces the OLD_RECORD with this one in the database. If there is an error,
292 returns the error, otherwise returns false.
296 Checks all fields to make sure this is a valid customer record. If there is
297 an error, returns the error, otherwise returns false. Called by the insert
306 $self->ut_numbern('custnum')
307 || $self->ut_number('agentnum')
308 || $self->ut_number('refnum')
309 || $self->ut_textn('company')
310 || $self->ut_text('address1')
311 || $self->ut_textn('address2')
312 || $self->ut_text('city')
313 || $self->ut_textn('county')
314 || $self->ut_textn('state')
316 return $error if $error;
318 return "Unknown agent"
319 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
321 return "Unknown referral"
322 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
324 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/
325 or return "Illegal last name: ". $self->getfield('last');
326 $self->setfield('last',$1);
328 $self->first =~ /^([\w \,\.\-\']+)$/
329 or return "Illegal first name: ". $self->first;
332 if ( $self->ss eq '' ) {
337 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
338 or return "Illegal social security number: ". $self->ss;
339 $self->ss("$1-$2-$3");
342 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
344 unless ( qsearchs('cust_main_county', {
345 'country' => $self->country,
348 return "Unknown state/county/country: ".
349 $self->state. "/". $self->county. "/". $self->country
350 unless qsearchs('cust_main_county',{
351 'state' => $self->state,
352 'county' => $self->county,
353 'country' => $self->country,
358 $self->ut_phonen('daytime', $self->country)
359 || $self->ut_phonen('night', $self->country)
360 || $self->ut_phonen('fax', $self->country)
362 return $error if $error;
364 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
365 or return "Illegal zip: ". $self->zip;
368 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
369 or return "Illegal payby: ". $self->payby;
372 if ( $self->payby eq 'CARD' ) {
374 my $payinfo = $self->payinfo;
376 $payinfo =~ /^(\d{13,16})$/
377 or return "Illegal credit card number: ". $self->payinfo;
379 $self->payinfo($payinfo);
381 or return "Illegal credit card number: ". $self->payinfo;
382 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
384 } elsif ( $self->payby eq 'BILL' ) {
386 $error = $self->ut_textn('payinfo');
387 return "Illegal P.O. number: ". $self->payinfo if $error;
389 } elsif ( $self->payby eq 'COMP' ) {
391 $error = $self->ut_textn('payinfo');
392 return "Illegal comp account issuer: ". $self->payinfo if $error;
394 } elsif ( $self->payby eq 'PREPAY' ) {
396 my $payinfo = $self->payinfo;
397 $payinfo =~ s/\W//g; #anything else would just confuse things
398 $self->payinfo($payinfo);
399 $error = $self->ut_alpha('payinfo');
400 return "Illegal prepayment identifier: ". $self->payinfo if $error;
401 return "Unknown prepayment identifier"
402 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
406 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
407 return "Expriation date required"
408 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
411 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
412 or return "Illegal expiration date: ". $self->paydate;
413 if ( length($2) == 4 ) {
414 $self->paydate("$2-$1-01");
415 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
416 $self->paydate("19$2-$1-01");
418 $self->paydate("20$2-$1-01");
422 if ( $self->payname eq '' ) {
423 $self->payname( $self->first. " ". $self->getfield('last') );
425 $self->payname =~ /^([\w \,\.\-\']+)$/
426 or return "Illegal billing name: ". $self->payname;
430 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
433 $self->otaker(getotaker);
440 Returns all packages (see L<FS::cust_pkg>) for this customer.
446 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
449 =item ncancelled_pkgs
451 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
455 sub ncancelled_pkgs {
457 @{ [ # force list context
458 qsearch( 'cust_pkg', {
459 'custnum' => $self->custnum,
462 qsearch( 'cust_pkg', {
463 'custnum' => $self->custnum,
471 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
472 conjunction with the collect method.
474 The only currently available option is `time', which bills the customer as if
475 it were that time. It is specified as a UNIX timestamp; see
476 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
479 If there is an error, returns the error, otherwise returns false.
484 my( $self, %options ) = @_;
485 my $time = $options{'time'} || time;
490 local $SIG{HUP} = 'IGNORE';
491 local $SIG{INT} = 'IGNORE';
492 local $SIG{QUIT} = 'IGNORE';
493 local $SIG{TERM} = 'IGNORE';
494 local $SIG{TSTP} = 'IGNORE';
495 local $SIG{PIPE} = 'IGNORE';
497 # find the packages which are due for billing, find out how much they are
498 # & generate invoice database.
500 my( $total_setup, $total_recur ) = ( 0, 0 );
503 foreach my $cust_pkg (
504 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
507 next if $cust_pkg->getfield('cancel');
509 #? to avoid use of uninitialized value errors... ?
510 $cust_pkg->setfield('bill', '')
511 unless defined($cust_pkg->bill);
513 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
515 #so we don't modify cust_pkg record unnecessarily
516 my $cust_pkg_mod_flag = 0;
517 my %hash = $cust_pkg->hash;
518 my $old_cust_pkg = new FS::cust_pkg \%hash;
522 unless ( $cust_pkg->setup ) {
523 my $setup_prog = $part_pkg->getfield('setup');
525 #$cpt->permit(); #what is necessary?
526 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
527 $setup = $cpt->reval($setup_prog);
528 unless ( defined($setup) ) {
529 warn "Error reval-ing part_pkg->setup pkgpart ",
530 $part_pkg->pkgpart, ": $@";
532 $cust_pkg->setfield('setup',$time);
533 $cust_pkg_mod_flag=1;
540 if ( $part_pkg->getfield('freq') > 0 &&
541 ! $cust_pkg->getfield('susp') &&
542 ( $cust_pkg->getfield('bill') || 0 ) < $time
544 my $recur_prog = $part_pkg->getfield('recur');
546 #$cpt->permit(); #what is necessary?
547 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
548 $recur = $cpt->reval($recur_prog);
549 unless ( defined($recur) ) {
550 warn "Error reval-ing part_pkg->recur pkgpart ",
551 $part_pkg->pkgpart, ": $@";
553 #change this bit to use Date::Manip? CAREFUL with timezones (see
554 # mailing list archive)
555 #$sdate=$cust_pkg->bill || time;
556 #$sdate=$cust_pkg->bill || $time;
557 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
558 my ($sec,$min,$hour,$mday,$mon,$year) =
559 (localtime($sdate) )[0,1,2,3,4,5];
560 $mon += $part_pkg->getfield('freq');
561 until ( $mon < 12 ) { $mon -= 12; $year++; }
562 $cust_pkg->setfield('bill',
563 timelocal($sec,$min,$hour,$mday,$mon,$year));
564 $cust_pkg_mod_flag = 1;
568 warn "setup is undefinded" unless defined($setup);
569 warn "recur is undefinded" unless defined($recur);
570 warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
572 if ( $cust_pkg_mod_flag ) {
573 $error=$cust_pkg->replace($old_cust_pkg);
574 if ( $error ) { #just in case
575 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
577 $setup = sprintf( "%.2f", $setup );
578 $recur = sprintf( "%.2f", $recur );
579 my $cust_bill_pkg = new FS::cust_bill_pkg ({
580 'pkgnum' => $cust_pkg->pkgnum,
584 'edate' => $cust_pkg->bill,
586 push @cust_bill_pkg, $cust_bill_pkg;
587 $total_setup += $setup;
588 $total_recur += $recur;
594 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
596 return '' if scalar(@cust_bill_pkg) == 0;
598 unless ( $self->getfield('tax') =~ /Y/i
599 || $self->getfield('payby') eq 'COMP'
601 my $cust_main_county = qsearchs('cust_main_county',{
602 'state' => $self->state,
603 'county' => $self->county,
604 'country' => $self->country,
606 my $tax = sprintf( "%.2f",
607 $charged * ( $cust_main_county->getfield('tax') / 100 )
609 $charged = sprintf( "%.2f", $charged+$tax );
611 my $cust_bill_pkg = new FS::cust_bill_pkg ({
618 push @cust_bill_pkg, $cust_bill_pkg;
621 my $cust_bill = new FS::cust_bill ( {
622 'custnum' => $self->getfield('custnum'),
624 'charged' => $charged,
626 $error = $cust_bill->insert;
627 #shouldn't happen, but how else to handle this? (wrap me in eval, to catch
629 die "Error creating cust_bill record: $error!\n",
630 "Check updated but unbilled packages for customer", $self->custnum, "\n"
633 my $invnum = $cust_bill->invnum;
635 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
636 $cust_bill_pkg->setfield( 'invnum', $invnum );
637 $error = $cust_bill_pkg->insert;
638 #shouldn't happen, but how else tohandle this?
639 die "Error creating cust_bill_pkg record: $error!\n",
640 "Check incomplete invoice ", $invnum, "\n"
647 =item collect OPTIONS
649 (Attempt to) collect money for this customer's outstanding invoices (see
650 L<FS::cust_bill>). Usually used after the bill method.
652 Depending on the value of `payby', this may print an invoice (`BILL'), charge
653 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
655 If there is an error, returns the error, otherwise returns false.
657 Currently available options are:
659 invoice_time - Use this time when deciding when to print invoices and
660 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>
661 for conversion functions.
663 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
664 default, cards are processed immediately, which will generate an error if
665 CyberCash is not installed.
667 report_badcard - Set this true if you want bad card transactions to
668 return an error. By default, they don't.
673 my( $self, %options ) = @_;
674 my $invoice_time = $options{'invoice_time'} || time;
676 my $total_owed = $self->balance;
677 warn "collect: total owed $total_owed " if $Debug;
678 return '' unless $total_owed > 0; #redundant?????
681 local $SIG{HUP} = 'IGNORE';
682 local $SIG{INT} = 'IGNORE';
683 local $SIG{QUIT} = 'IGNORE';
684 local $SIG{TERM} = 'IGNORE';
685 local $SIG{TSTP} = 'IGNORE';
686 local $SIG{PIPE} = 'IGNORE';
688 foreach my $cust_bill (
689 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
692 #this has to be before next's
693 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
697 $total_owed = sprintf( "%.2f", $total_owed - $amount );
699 next unless $cust_bill->owed > 0;
701 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
703 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)" if $Debug;
705 next unless $amount > 0;
707 if ( $self->payby eq 'BILL' ) {
710 my $since = $invoice_time - ( $cust_bill->_date || 0 );
711 #warn "$invoice_time ", $cust_bill->_date, " $since";
712 if ( $since >= 0 #don't print future invoices
713 && ( $cust_bill->printed * 2592000 ) <= $since
716 #my @print_text = $cust_bill->print_text; #( date )
717 my @invoicing_list = $self->invoicing_list;
718 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
719 $ENV{SMTPHOSTS} = $smtpmachine;
720 $ENV{MAILADDRESS} = $invoice_from;
721 my $header = new Mail::Header ( [
722 "From: $invoice_from",
723 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
724 "Sender: $invoice_from",
725 "Reply-To: $invoice_from",
726 "Date: ". time2str("%a, %d %b %Y %X %z", time),
729 my $message = new Mail::Internet (
731 'Body' => [ $cust_bill->print_text ], #( date)
733 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
735 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
736 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
737 print LPR $cust_bill->print_text; #( date )
739 or die $! ? "Error closing $lpr: $!"
740 : "Exit status $? from $lpr";
743 my %hash = $cust_bill->hash;
745 my $new_cust_bill = new FS::cust_bill(\%hash);
746 my $error = $new_cust_bill->replace($cust_bill);
747 warn "Error updating $cust_bill->printed: $error" if $error;
751 } elsif ( $self->payby eq 'COMP' ) {
752 my $cust_pay = new FS::cust_pay ( {
753 'invnum' => $cust_bill->invnum,
757 'payinfo' => $self->payinfo,
760 my $error = $cust_pay->insert;
761 return 'Error COMPing invnum #' . $cust_bill->invnum .
762 ':' . $error if $error;
764 } elsif ( $self->payby eq 'CARD' ) {
766 if ( $options{'batch_card'} ne 'yes' ) {
768 return "Real time card processing not enabled!" unless $processor;
770 if ( $processor =~ /^cybercash/ ) {
772 #fix exp. date for cybercash
773 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
774 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
777 my $paybatch = $cust_bill->invnum.
778 '-' . time2str("%y%m%d%H%M%S", time);
780 my $payname = $self->payname ||
781 $self->getfield('first'). ' '. $self->getfield('last');
783 my $address = $self->address1;
784 $address .= ", ". $self->address2 if $self->address2;
786 my $country = 'USA' if $self->country eq 'US';
788 my @full_xaction = ( $xaction,
789 'Order-ID' => $paybatch,
790 'Amount' => "usd $amount",
791 'Card-Number' => $self->getfield('payinfo'),
792 'Card-Name' => $payname,
793 'Card-Address' => $address,
794 'Card-City' => $self->getfield('city'),
795 'Card-State' => $self->getfield('state'),
796 'Card-Zip' => $self->getfield('zip'),
797 'Card-Country' => $country,
802 if ( $processor eq 'cybercash2' ) {
803 $^W=0; #CCLib isn't -w safe, ugh!
804 %result = &CCLib::sendmserver(@full_xaction);
806 } elsif ( $processor eq 'cybercash3.2' ) {
807 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
809 return "Unkonwn real-time processor $processor\n";
812 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
813 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
814 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
815 my $cust_pay = new FS::cust_pay ( {
816 'invnum' => $cust_bill->invnum,
820 'payinfo' => $self->payinfo,
821 'paybatch' => "$processor:$paybatch",
823 my $error = $cust_pay->insert;
824 return 'Error applying payment, invnum #' .
825 $cust_bill->invnum. ':'. $error if $error;
826 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
827 || $options{'report_badcard'} ) {
828 return 'Cybercash error, invnum #' .
829 $cust_bill->invnum. ':'. $result{'MErrMsg'};
835 return "Unkonwn real-time processor $processor\n";
840 my $cust_pay_batch = new FS::cust_pay_batch ( {
841 'invnum' => $cust_bill->getfield('invnum'),
842 'custnum' => $self->getfield('custnum'),
843 'last' => $self->getfield('last'),
844 'first' => $self->getfield('first'),
845 'address1' => $self->getfield('address1'),
846 'address2' => $self->getfield('address2'),
847 'city' => $self->getfield('city'),
848 'state' => $self->getfield('state'),
849 'zip' => $self->getfield('zip'),
850 'country' => $self->getfield('country'),
852 'cardnum' => $self->getfield('payinfo'),
853 'exp' => $self->getfield('paydate'),
854 'payname' => $self->getfield('payname'),
857 my $error = $cust_pay_batch->insert;
858 return "Error adding to cust_pay_batch: $error" if $error;
863 return "Unknown payment type ". $self->payby;
873 Returns the total owed for this customer on all invoices
874 (see L<FS::cust_bill>).
881 foreach my $cust_bill ( qsearch('cust_bill', {
882 'custnum' => $self->custnum,
884 $total_bill += $cust_bill->owed;
886 sprintf( "%.2f", $total_bill );
891 Returns the total credits (see L<FS::cust_credit>) for this customer.
897 my $total_credit = 0;
898 foreach my $cust_credit ( qsearch('cust_credit', {
899 'custnum' => $self->custnum,
901 $total_credit += $cust_credit->credited;
903 sprintf( "%.2f", $total_credit );
908 Returns the balance for this customer (total owed minus total credited).
914 sprintf( "%.2f", $self->total_owed - $self->total_credited );
917 =item invoicing_list [ ARRAYREF ]
919 If an arguement is given, sets these email addresses as invoice recipients
920 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
921 (except as warnings), so use check_invoicing_list first.
923 Returns a list of email addresses (with svcnum entries expanded).
925 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
926 check it without disturbing anything by passing nothing.
928 This interface may change in the future.
933 my( $self, $arrayref ) = @_;
935 my @cust_main_invoice;
936 if ( $self->custnum ) {
938 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
940 @cust_main_invoice = ();
942 foreach my $cust_main_invoice ( @cust_main_invoice ) {
943 #warn $cust_main_invoice->destnum;
944 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
945 #warn $cust_main_invoice->destnum;
946 my $error = $cust_main_invoice->delete;
947 warn $error if $error;
950 if ( $self->custnum ) {
952 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
954 @cust_main_invoice = ();
956 foreach my $address ( @{$arrayref} ) {
957 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
958 my $cust_main_invoice = new FS::cust_main_invoice ( {
959 'custnum' => $self->custnum,
962 my $error = $cust_main_invoice->insert;
963 warn $error if $error;
967 if ( $self->custnum ) {
969 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
975 =item check_invoicing_list ARRAYREF
977 Checks these arguements as valid input for the invoicing_list method. If there
978 is an error, returns the error, otherwise returns false.
982 sub check_invoicing_list {
983 my( $self, $arrayref ) = @_;
984 foreach my $address ( @{$arrayref} ) {
985 my $cust_main_invoice = new FS::cust_main_invoice ( {
986 'custnum' => $self->custnum,
989 my $error = $self->custnum
990 ? $cust_main_invoice->check
991 : $cust_main_invoice->checkdest
993 return $error if $error;
1002 $Id: cust_main.pm,v 1.7 2000-06-27 12:15:37 ivan Exp $
1008 The delete method should possibly take an FS::cust_main object reference
1009 instead of a scalar customer number.
1011 Bill and collect options should probably be passed as references instead of a
1014 CyberCash v2 forces us to define some variables in package main.
1016 There should probably be a configuration file with a list of allowed credit
1019 CyberCash is the only processor.
1021 No multiple currency support (probably a larger project than just this module).
1025 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1026 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1027 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1028 L<FS::UID>, schema.html from the base documentation.