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')
315 || $self->ut_phonen('daytime')
316 || $self->ut_phonen('night')
317 || $self->ut_phonen('fax')
319 return $error if $error;
321 return "Unknown agent"
322 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
324 return "Unknown referral"
325 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
327 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/
328 or return "Illegal last name: ". $self->getfield('last');
329 $self->setfield('last',$1);
331 $self->first =~ /^([\w \,\.\-\']+)$/
332 or return "Illegal first name: ". $self->first;
335 if ( $self->ss eq '' ) {
340 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
341 or return "Illegal social security number: ". $self->ss;
342 $self->ss("$1-$2-$3");
345 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
347 unless ( qsearchs('cust_main_county', {
348 'country' => $self->country,
351 return "Unknown state/county/country: ".
352 $self->state. "/". $self->county. "/". $self->country
353 unless qsearchs('cust_main_county',{
354 'state' => $self->state,
355 'county' => $self->county,
356 'country' => $self->country,
360 $self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
361 or return "Illegal zip: ". $self->zip;
364 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
365 or return "Illegal payby: ". $self->payby;
368 if ( $self->payby eq 'CARD' ) {
370 my $payinfo = $self->payinfo;
372 $payinfo =~ /^(\d{13,16})$/
373 or return "Illegal credit card number: ". $self->payinfo;
375 $self->payinfo($payinfo);
377 or return "Illegal credit card number: ". $self->payinfo;
378 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
380 } elsif ( $self->payby eq 'BILL' ) {
382 $error = $self->ut_textn('payinfo');
383 return "Illegal P.O. number: ". $self->payinfo if $error;
385 } elsif ( $self->payby eq 'COMP' ) {
387 $error = $self->ut_textn('payinfo');
388 return "Illegal comp account issuer: ". $self->payinfo if $error;
390 } elsif ( $self->payby eq 'PREPAY' ) {
392 my $payinfo = $self->payinfo;
393 $payinfo =~ s/\W//g; #anything else would just confuse things
394 $self->payinfo($payinfo);
395 $error = $self->ut_alpha('payinfo');
396 return "Illegal prepayment identifier: ". $self->payinfo if $error;
397 return "Unknown prepayment identifier"
398 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
402 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
403 return "Expriation date required"
404 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
407 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
408 or return "Illegal expiration date: ". $self->paydate;
409 if ( length($2) == 4 ) {
410 $self->paydate("$2-$1-01");
411 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
412 $self->paydate("19$2-$1-01");
414 $self->paydate("20$2-$1-01");
418 if ( $self->payname eq '' ) {
419 $self->payname( $self->first. " ". $self->getfield('last') );
421 $self->payname =~ /^([\w \,\.\-\']+)$/
422 or return "Illegal billing name: ". $self->payname;
426 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
429 $self->otaker(getotaker);
436 Returns all packages (see L<FS::cust_pkg>) for this customer.
442 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
445 =item ncancelled_pkgs
447 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
451 sub ncancelled_pkgs {
453 @{ [ # force list context
454 qsearch( 'cust_pkg', {
455 'custnum' => $self->custnum,
458 qsearch( 'cust_pkg', {
459 'custnum' => $self->custnum,
467 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
468 conjunction with the collect method.
470 The only currently available option is `time', which bills the customer as if
471 it were that time. It is specified as a UNIX timestamp; see
472 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
475 If there is an error, returns the error, otherwise returns false.
480 my( $self, %options ) = @_;
481 my $time = $options{'time'} || time;
486 local $SIG{HUP} = 'IGNORE';
487 local $SIG{INT} = 'IGNORE';
488 local $SIG{QUIT} = 'IGNORE';
489 local $SIG{TERM} = 'IGNORE';
490 local $SIG{TSTP} = 'IGNORE';
491 local $SIG{PIPE} = 'IGNORE';
493 # find the packages which are due for billing, find out how much they are
494 # & generate invoice database.
496 my( $total_setup, $total_recur ) = ( 0, 0 );
499 foreach my $cust_pkg (
500 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
503 next if $cust_pkg->getfield('cancel');
505 #? to avoid use of uninitialized value errors... ?
506 $cust_pkg->setfield('bill', '')
507 unless defined($cust_pkg->bill);
509 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
511 #so we don't modify cust_pkg record unnecessarily
512 my $cust_pkg_mod_flag = 0;
513 my %hash = $cust_pkg->hash;
514 my $old_cust_pkg = new FS::cust_pkg \%hash;
518 unless ( $cust_pkg->setup ) {
519 my $setup_prog = $part_pkg->getfield('setup');
521 #$cpt->permit(); #what is necessary?
522 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
523 $setup = $cpt->reval($setup_prog);
524 unless ( defined($setup) ) {
525 warn "Error reval-ing part_pkg->setup pkgpart ",
526 $part_pkg->pkgpart, ": $@";
528 $cust_pkg->setfield('setup',$time);
529 $cust_pkg_mod_flag=1;
536 if ( $part_pkg->getfield('freq') > 0 &&
537 ! $cust_pkg->getfield('susp') &&
538 ( $cust_pkg->getfield('bill') || 0 ) < $time
540 my $recur_prog = $part_pkg->getfield('recur');
542 #$cpt->permit(); #what is necessary?
543 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
544 $recur = $cpt->reval($recur_prog);
545 unless ( defined($recur) ) {
546 warn "Error reval-ing part_pkg->recur pkgpart ",
547 $part_pkg->pkgpart, ": $@";
549 #change this bit to use Date::Manip? CAREFUL with timezones (see
550 # mailing list archive)
551 #$sdate=$cust_pkg->bill || time;
552 #$sdate=$cust_pkg->bill || $time;
553 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
554 my ($sec,$min,$hour,$mday,$mon,$year) =
555 (localtime($sdate) )[0,1,2,3,4,5];
556 $mon += $part_pkg->getfield('freq');
557 until ( $mon < 12 ) { $mon -= 12; $year++; }
558 $cust_pkg->setfield('bill',
559 timelocal($sec,$min,$hour,$mday,$mon,$year));
560 $cust_pkg_mod_flag = 1;
564 warn "setup is undefinded" unless defined($setup);
565 warn "recur is undefinded" unless defined($recur);
566 warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
568 if ( $cust_pkg_mod_flag ) {
569 $error=$cust_pkg->replace($old_cust_pkg);
570 if ( $error ) { #just in case
571 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
573 $setup = sprintf( "%.2f", $setup );
574 $recur = sprintf( "%.2f", $recur );
575 my $cust_bill_pkg = new FS::cust_bill_pkg ({
576 'pkgnum' => $cust_pkg->pkgnum,
580 'edate' => $cust_pkg->bill,
582 push @cust_bill_pkg, $cust_bill_pkg;
583 $total_setup += $setup;
584 $total_recur += $recur;
590 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
592 return '' if scalar(@cust_bill_pkg) == 0;
594 unless ( $self->getfield('tax') =~ /Y/i
595 || $self->getfield('payby') eq 'COMP'
597 my $cust_main_county = qsearchs('cust_main_county',{
598 'state' => $self->state,
599 'county' => $self->county,
600 'country' => $self->country,
602 my $tax = sprintf( "%.2f",
603 $charged * ( $cust_main_county->getfield('tax') / 100 )
605 $charged = sprintf( "%.2f", $charged+$tax );
607 my $cust_bill_pkg = new FS::cust_bill_pkg ({
614 push @cust_bill_pkg, $cust_bill_pkg;
617 my $cust_bill = new FS::cust_bill ( {
618 'custnum' => $self->getfield('custnum'),
620 'charged' => $charged,
622 $error = $cust_bill->insert;
623 #shouldn't happen, but how else to handle this? (wrap me in eval, to catch
625 die "Error creating cust_bill record: $error!\n",
626 "Check updated but unbilled packages for customer", $self->custnum, "\n"
629 my $invnum = $cust_bill->invnum;
631 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
632 $cust_bill_pkg->setfield( 'invnum', $invnum );
633 $error = $cust_bill_pkg->insert;
634 #shouldn't happen, but how else tohandle this?
635 die "Error creating cust_bill_pkg record: $error!\n",
636 "Check incomplete invoice ", $invnum, "\n"
643 =item collect OPTIONS
645 (Attempt to) collect money for this customer's outstanding invoices (see
646 L<FS::cust_bill>). Usually used after the bill method.
648 Depending on the value of `payby', this may print an invoice (`BILL'), charge
649 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
651 If there is an error, returns the error, otherwise returns false.
653 Currently available options are:
655 invoice_time - Use this time when deciding when to print invoices and
656 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>
657 for conversion functions.
659 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
660 default, cards are processed immediately, which will generate an error if
661 CyberCash is not installed.
663 report_badcard - Set this true if you want bad card transactions to
664 return an error. By default, they don't.
669 my( $self, %options ) = @_;
670 my $invoice_time = $options{'invoice_time'} || time;
672 my $total_owed = $self->balance;
673 warn "collect: total owed $total_owed " if $Debug;
674 return '' unless $total_owed > 0; #redundant?????
677 local $SIG{HUP} = 'IGNORE';
678 local $SIG{INT} = 'IGNORE';
679 local $SIG{QUIT} = 'IGNORE';
680 local $SIG{TERM} = 'IGNORE';
681 local $SIG{TSTP} = 'IGNORE';
682 local $SIG{PIPE} = 'IGNORE';
684 foreach my $cust_bill (
685 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
688 #this has to be before next's
689 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
693 $total_owed = sprintf( "%.2f", $total_owed - $amount );
695 next unless $cust_bill->owed > 0;
697 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
699 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)" if $Debug;
701 next unless $amount > 0;
703 if ( $self->payby eq 'BILL' ) {
706 my $since = $invoice_time - ( $cust_bill->_date || 0 );
707 #warn "$invoice_time ", $cust_bill->_date, " $since";
708 if ( $since >= 0 #don't print future invoices
709 && ( $cust_bill->printed * 2592000 ) <= $since
712 #my @print_text = $cust_bill->print_text; #( date )
713 my @invoicing_list = $self->invoicing_list;
714 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
715 $ENV{SMTPHOSTS} = $smtpmachine;
716 $ENV{MAILADDRESS} = $invoice_from;
717 my $header = new Mail::Header ( [
718 "From: $invoice_from",
719 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
720 "Sender: $invoice_from",
721 "Reply-To: $invoice_from",
722 "Date: ". time2str("%a, %d %b %Y %X %z", time),
725 my $message = new Mail::Internet (
727 'Body' => [ $cust_bill->print_text ], #( date)
729 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
731 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
732 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
733 print LPR $cust_bill->print_text; #( date )
735 or die $! ? "Error closing $lpr: $!"
736 : "Exit status $? from $lpr";
739 my %hash = $cust_bill->hash;
741 my $new_cust_bill = new FS::cust_bill(\%hash);
742 my $error = $new_cust_bill->replace($cust_bill);
743 warn "Error updating $cust_bill->printed: $error" if $error;
747 } elsif ( $self->payby eq 'COMP' ) {
748 my $cust_pay = new FS::cust_pay ( {
749 'invnum' => $cust_bill->invnum,
753 'payinfo' => $self->payinfo,
756 my $error = $cust_pay->insert;
757 return 'Error COMPing invnum #' . $cust_bill->invnum .
758 ':' . $error if $error;
760 } elsif ( $self->payby eq 'CARD' ) {
762 if ( $options{'batch_card'} ne 'yes' ) {
764 return "Real time card processing not enabled!" unless $processor;
766 if ( $processor =~ /^cybercash/ ) {
768 #fix exp. date for cybercash
769 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
770 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
773 my $paybatch = $cust_bill->invnum.
774 '-' . time2str("%y%m%d%H%M%S", time);
776 my $payname = $self->payname ||
777 $self->getfield('first'). ' '. $self->getfield('last');
779 my $address = $self->address1;
780 $address .= ", ". $self->address2 if $self->address2;
782 my $country = 'USA' if $self->country eq 'US';
784 my @full_xaction = ( $xaction,
785 'Order-ID' => $paybatch,
786 'Amount' => "usd $amount",
787 'Card-Number' => $self->getfield('payinfo'),
788 'Card-Name' => $payname,
789 'Card-Address' => $address,
790 'Card-City' => $self->getfield('city'),
791 'Card-State' => $self->getfield('state'),
792 'Card-Zip' => $self->getfield('zip'),
793 'Card-Country' => $country,
798 if ( $processor eq 'cybercash2' ) {
799 $^W=0; #CCLib isn't -w safe, ugh!
800 %result = &CCLib::sendmserver(@full_xaction);
802 } elsif ( $processor eq 'cybercash3.2' ) {
803 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
805 return "Unkonwn real-time processor $processor\n";
808 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
809 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
810 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
811 my $cust_pay = new FS::cust_pay ( {
812 'invnum' => $cust_bill->invnum,
816 'payinfo' => $self->payinfo,
817 'paybatch' => "$processor:$paybatch",
819 my $error = $cust_pay->insert;
820 return 'Error applying payment, invnum #' .
821 $cust_bill->invnum. ':'. $error if $error;
822 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
823 || $options{'report_badcard'} ) {
824 return 'Cybercash error, invnum #' .
825 $cust_bill->invnum. ':'. $result{'MErrMsg'};
831 return "Unkonwn real-time processor $processor\n";
836 my $cust_pay_batch = new FS::cust_pay_batch ( {
837 'invnum' => $cust_bill->getfield('invnum'),
838 'custnum' => $self->getfield('custnum'),
839 'last' => $self->getfield('last'),
840 'first' => $self->getfield('first'),
841 'address1' => $self->getfield('address1'),
842 'address2' => $self->getfield('address2'),
843 'city' => $self->getfield('city'),
844 'state' => $self->getfield('state'),
845 'zip' => $self->getfield('zip'),
846 'country' => $self->getfield('country'),
848 'cardnum' => $self->getfield('payinfo'),
849 'exp' => $self->getfield('paydate'),
850 'payname' => $self->getfield('payname'),
853 my $error = $cust_pay_batch->insert;
854 return "Error adding to cust_pay_batch: $error" if $error;
859 return "Unknown payment type ". $self->payby;
869 Returns the total owed for this customer on all invoices
870 (see L<FS::cust_bill>).
877 foreach my $cust_bill ( qsearch('cust_bill', {
878 'custnum' => $self->custnum,
880 $total_bill += $cust_bill->owed;
882 sprintf( "%.2f", $total_bill );
887 Returns the total credits (see L<FS::cust_credit>) for this customer.
893 my $total_credit = 0;
894 foreach my $cust_credit ( qsearch('cust_credit', {
895 'custnum' => $self->custnum,
897 $total_credit += $cust_credit->credited;
899 sprintf( "%.2f", $total_credit );
904 Returns the balance for this customer (total owed minus total credited).
910 sprintf( "%.2f", $self->total_owed - $self->total_credited );
913 =item invoicing_list [ ARRAYREF ]
915 If an arguement is given, sets these email addresses as invoice recipients
916 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
917 (except as warnings), so use check_invoicing_list first.
919 Returns a list of email addresses (with svcnum entries expanded).
921 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
922 check it without disturbing anything by passing nothing.
924 This interface may change in the future.
929 my( $self, $arrayref ) = @_;
931 my @cust_main_invoice;
932 if ( $self->custnum ) {
934 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
936 @cust_main_invoice = ();
938 foreach my $cust_main_invoice ( @cust_main_invoice ) {
939 #warn $cust_main_invoice->destnum;
940 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
941 #warn $cust_main_invoice->destnum;
942 my $error = $cust_main_invoice->delete;
943 warn $error if $error;
946 if ( $self->custnum ) {
948 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
950 @cust_main_invoice = ();
952 foreach my $address ( @{$arrayref} ) {
953 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
954 my $cust_main_invoice = new FS::cust_main_invoice ( {
955 'custnum' => $self->custnum,
958 my $error = $cust_main_invoice->insert;
959 warn $error if $error;
963 if ( $self->custnum ) {
965 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
971 =item check_invoicing_list ARRAYREF
973 Checks these arguements as valid input for the invoicing_list method. If there
974 is an error, returns the error, otherwise returns false.
978 sub check_invoicing_list {
979 my( $self, $arrayref ) = @_;
980 foreach my $address ( @{$arrayref} ) {
981 my $cust_main_invoice = new FS::cust_main_invoice ( {
982 'custnum' => $self->custnum,
985 my $error = $self->custnum
986 ? $cust_main_invoice->check
987 : $cust_main_invoice->checkdest
989 return $error if $error;
998 $Id: cust_main.pm,v 1.6 2000-06-24 00:28:30 ivan Exp $
1004 The delete method should possibly take an FS::cust_main object reference
1005 instead of a scalar customer number.
1007 Bill and collect options should probably be passed as references instead of a
1010 CyberCash v2 forces us to define some variables in package main.
1012 There should probably be a configuration file with a list of allowed credit
1015 CyberCash is the only processor.
1017 No multiple currency support (probably a larger project than just this module).
1021 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1022 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1023 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1024 L<FS::UID>, schema.html from the base documentation.