4 use vars qw( @ISA $conf $Debug $import );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
9 eval "use Time::Local;";
10 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
11 if $] < 5.006 && !defined($Time::Local::VERSION);
12 eval "use Time::Local qw(timelocal timelocal_nocheck);";
16 use Business::CreditCard;
17 use FS::UID qw( getotaker dbh );
18 use FS::Record qw( qsearchs qsearch dbdef );
19 use FS::Misc qw( send_email );
22 use FS::cust_bill_pkg;
25 use FS::part_referral;
26 use FS::cust_main_county;
28 use FS::cust_main_invoice;
29 use FS::cust_credit_bill;
30 use FS::cust_bill_pay;
31 use FS::prepay_credit;
34 use FS::part_bill_event;
35 use FS::cust_bill_event;
36 use FS::cust_tax_exempt;
38 use FS::Msgcat qw(gettext);
40 @ISA = qw( FS::Record );
42 $realtime_bop_decline_quiet = 0;
49 #ask FS::UID to run this stuff for us later
50 #$FS::UID::callback{'FS::cust_main'} = sub {
51 install_callback FS::UID sub {
53 #yes, need it for stuff below (prolly should be cached)
58 my ( $hashref, $cache ) = @_;
59 if ( exists $hashref->{'pkgnum'} ) {
60 # #@{ $self->{'_pkgnum'} } = ();
61 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
62 $self->{'_pkgnum'} = $subcache;
63 #push @{ $self->{'_pkgnum'} },
64 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
70 FS::cust_main - Object methods for cust_main records
76 $record = new FS::cust_main \%hash;
77 $record = new FS::cust_main { 'column' => 'value' };
79 $error = $record->insert;
81 $error = $new_record->replace($old_record);
83 $error = $record->delete;
85 $error = $record->check;
87 @cust_pkg = $record->all_pkgs;
89 @cust_pkg = $record->ncancelled_pkgs;
91 @cust_pkg = $record->suspended_pkgs;
93 $error = $record->bill;
94 $error = $record->bill %options;
95 $error = $record->bill 'time' => $time;
97 $error = $record->collect;
98 $error = $record->collect %options;
99 $error = $record->collect 'invoice_time' => $time,
100 'batch_card' => 'yes',
101 'report_badcard' => 'yes',
106 An FS::cust_main object represents a customer. FS::cust_main inherits from
107 FS::Record. The following fields are currently supported:
111 =item custnum - primary key (assigned automatically for new customers)
113 =item agentnum - agent (see L<FS::agent>)
115 =item refnum - Advertising source (see L<FS::part_referral>)
121 =item ss - social security number (optional)
123 =item company - (optional)
127 =item address2 - (optional)
131 =item county - (optional, see L<FS::cust_main_county>)
133 =item state - (see L<FS::cust_main_county>)
137 =item country - (see L<FS::cust_main_county>)
139 =item daytime - phone (optional)
141 =item night - phone (optional)
143 =item fax - phone (optional)
145 =item ship_first - name
147 =item ship_last - name
149 =item ship_company - (optional)
153 =item ship_address2 - (optional)
157 =item ship_county - (optional, see L<FS::cust_main_county>)
159 =item ship_state - (see L<FS::cust_main_county>)
163 =item ship_country - (see L<FS::cust_main_county>)
165 =item ship_daytime - phone (optional)
167 =item ship_night - phone (optional)
169 =item ship_fax - phone (optional)
171 =item payby - I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to I<BILL>)
173 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
175 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
177 =item payname - name on card or billing name
179 =item tax - tax exempt, empty or `Y'
181 =item otaker - order taker (assigned automatically, see L<FS::UID>)
183 =item comments - comments (optional)
185 =item referral_custnum - referring customer number
195 Creates a new customer. To add the customer to the database, see L<"insert">.
197 Note that this stores the hash reference, not a distinct copy of the hash it
198 points to. You can ask the object for a copy with the I<hash> method.
202 sub table { 'cust_main'; }
204 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
206 Adds this customer to the database. If there is an error, returns the error,
207 otherwise returns false.
209 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
210 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
211 are inserted atomicly, or the transaction is rolled back. Passing an empty
212 hash reference is equivalent to not supplying this parameter. There should be
213 a better explanation of this, but until then, here's an example:
216 tie %hash, 'Tie::RefHash'; #this part is important
218 $cust_pkg => [ $svc_acct ],
221 $cust_main->insert( \%hash );
223 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
224 be set as the invoicing list (see L<"invoicing_list">). Errors return as
225 expected and rollback the entire transaction; it is not necessary to call
226 check_invoicing_list first. The invoicing_list is set after the records in the
227 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
228 invoicing_list destination to the newly-created svc_acct. Here's an example:
230 $cust_main->insert( {}, [ $email, 'POST' ] );
232 Currently available options are: I<noexport>
234 If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
235 (You can schedule them later with the B<reexport> method.)
241 my $cust_pkgs = @_ ? shift : {};
242 my $invoicing_list = @_ ? shift : '';
245 local $SIG{HUP} = 'IGNORE';
246 local $SIG{INT} = 'IGNORE';
247 local $SIG{QUIT} = 'IGNORE';
248 local $SIG{TERM} = 'IGNORE';
249 local $SIG{TSTP} = 'IGNORE';
250 local $SIG{PIPE} = 'IGNORE';
252 my $oldAutoCommit = $FS::UID::AutoCommit;
253 local $FS::UID::AutoCommit = 0;
258 if ( $self->payby eq 'PREPAY' ) {
259 $self->payby('BILL');
260 my $prepay_credit = qsearchs(
262 { 'identifier' => $self->payinfo },
266 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
267 unless $prepay_credit;
268 $amount = $prepay_credit->amount;
269 $seconds = $prepay_credit->seconds;
270 my $error = $prepay_credit->delete;
272 $dbh->rollback if $oldAutoCommit;
273 return "removing prepay_credit (transaction rolled back): $error";
277 my $error = $self->SUPER::insert;
279 $dbh->rollback if $oldAutoCommit;
280 #return "inserting cust_main record (transaction rolled back): $error";
285 if ( $invoicing_list ) {
286 $error = $self->check_invoicing_list( $invoicing_list );
288 $dbh->rollback if $oldAutoCommit;
289 return "checking invoicing_list (transaction rolled back): $error";
291 $self->invoicing_list( $invoicing_list );
295 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
296 $error = $self->order_pkgs($cust_pkgs, \$seconds);
298 $dbh->rollback if $oldAutoCommit;
303 $dbh->rollback if $oldAutoCommit;
304 return "No svc_acct record to apply pre-paid time";
308 my $cust_credit = new FS::cust_credit {
309 'custnum' => $self->custnum,
312 $error = $cust_credit->insert;
314 $dbh->rollback if $oldAutoCommit;
315 return "inserting credit (transaction rolled back): $error";
319 $error = $self->queue_fuzzyfiles_update;
321 $dbh->rollback if $oldAutoCommit;
322 return "updating fuzzy search cache: $error";
325 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
332 document me. like ->insert(%cust_pkg) on an existing record
338 my $cust_pkgs = shift;
341 local $SIG{HUP} = 'IGNORE';
342 local $SIG{INT} = 'IGNORE';
343 local $SIG{QUIT} = 'IGNORE';
344 local $SIG{TERM} = 'IGNORE';
345 local $SIG{TSTP} = 'IGNORE';
346 local $SIG{PIPE} = 'IGNORE';
348 my $oldAutoCommit = $FS::UID::AutoCommit;
349 local $FS::UID::AutoCommit = 0;
352 foreach my $cust_pkg ( keys %$cust_pkgs ) {
353 $cust_pkg->custnum( $self->custnum );
354 my $error = $cust_pkg->insert;
356 $dbh->rollback if $oldAutoCommit;
357 return "inserting cust_pkg (transaction rolled back): $error";
359 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
360 $svc_something->pkgnum( $cust_pkg->pkgnum );
361 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
362 $svc_something->seconds( $svc_something->seconds + $$seconds );
365 $error = $svc_something->insert;
367 $dbh->rollback if $oldAutoCommit;
368 #return "inserting svc_ (transaction rolled back): $error";
374 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
380 document me. Re-schedules all exports by calling the B<reexport> method
381 of all associated packages (see L<FS::cust_pkg>). If there is an error,
382 returns the error; otherwise returns false.
389 local $SIG{HUP} = 'IGNORE';
390 local $SIG{INT} = 'IGNORE';
391 local $SIG{QUIT} = 'IGNORE';
392 local $SIG{TERM} = 'IGNORE';
393 local $SIG{TSTP} = 'IGNORE';
394 local $SIG{PIPE} = 'IGNORE';
396 my $oldAutoCommit = $FS::UID::AutoCommit;
397 local $FS::UID::AutoCommit = 0;
400 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
401 my $error = $cust_pkg->reexport;
403 $dbh->rollback if $oldAutoCommit;
408 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
413 =item delete NEW_CUSTNUM
415 This deletes the customer. If there is an error, returns the error, otherwise
418 This will completely remove all traces of the customer record. This is not
419 what you want when a customer cancels service; for that, cancel all of the
420 customer's packages (see L</cancel>).
422 If the customer has any uncancelled packages, you need to pass a new (valid)
423 customer number for those packages to be transferred to. Cancelled packages
424 will be deleted. Did I mention that this is NOT what you want when a customer
425 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
427 You can't delete a customer with invoices (see L<FS::cust_bill>),
428 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
429 refunds (see L<FS::cust_refund>).
436 local $SIG{HUP} = 'IGNORE';
437 local $SIG{INT} = 'IGNORE';
438 local $SIG{QUIT} = 'IGNORE';
439 local $SIG{TERM} = 'IGNORE';
440 local $SIG{TSTP} = 'IGNORE';
441 local $SIG{PIPE} = 'IGNORE';
443 my $oldAutoCommit = $FS::UID::AutoCommit;
444 local $FS::UID::AutoCommit = 0;
447 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
448 $dbh->rollback if $oldAutoCommit;
449 return "Can't delete a customer with invoices";
451 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
452 $dbh->rollback if $oldAutoCommit;
453 return "Can't delete a customer with credits";
455 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
456 $dbh->rollback if $oldAutoCommit;
457 return "Can't delete a customer with payments";
459 if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
460 $dbh->rollback if $oldAutoCommit;
461 return "Can't delete a customer with refunds";
464 my @cust_pkg = $self->ncancelled_pkgs;
466 my $new_custnum = shift;
467 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
468 $dbh->rollback if $oldAutoCommit;
469 return "Invalid new customer number: $new_custnum";
471 foreach my $cust_pkg ( @cust_pkg ) {
472 my %hash = $cust_pkg->hash;
473 $hash{'custnum'} = $new_custnum;
474 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
475 my $error = $new_cust_pkg->replace($cust_pkg);
477 $dbh->rollback if $oldAutoCommit;
482 my @cancelled_cust_pkg = $self->all_pkgs;
483 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
484 my $error = $cust_pkg->delete;
486 $dbh->rollback if $oldAutoCommit;
491 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
492 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
494 my $error = $cust_main_invoice->delete;
496 $dbh->rollback if $oldAutoCommit;
501 my $error = $self->SUPER::delete;
503 $dbh->rollback if $oldAutoCommit;
507 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
512 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
514 Replaces the OLD_RECORD with this one in the database. If there is an error,
515 returns the error, otherwise returns false.
517 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
518 be set as the invoicing list (see L<"invoicing_list">). Errors return as
519 expected and rollback the entire transaction; it is not necessary to call
520 check_invoicing_list first. Here's an example:
522 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
531 local $SIG{HUP} = 'IGNORE';
532 local $SIG{INT} = 'IGNORE';
533 local $SIG{QUIT} = 'IGNORE';
534 local $SIG{TERM} = 'IGNORE';
535 local $SIG{TSTP} = 'IGNORE';
536 local $SIG{PIPE} = 'IGNORE';
538 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
539 && $conf->config('users-allow_comp') ) {
540 return "You are not permitted to create complimentary accounts."
541 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
544 my $oldAutoCommit = $FS::UID::AutoCommit;
545 local $FS::UID::AutoCommit = 0;
548 my $error = $self->SUPER::replace($old);
551 $dbh->rollback if $oldAutoCommit;
555 if ( @param ) { # INVOICING_LIST_ARYREF
556 my $invoicing_list = shift @param;
557 $error = $self->check_invoicing_list( $invoicing_list );
559 $dbh->rollback if $oldAutoCommit;
562 $self->invoicing_list( $invoicing_list );
565 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
566 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
567 # card/check/lec info has changed, want to retry realtime_ invoice events
568 my $error = $self->retry_realtime;
570 $dbh->rollback if $oldAutoCommit;
575 $error = $self->queue_fuzzyfiles_update;
577 $dbh->rollback if $oldAutoCommit;
578 return "updating fuzzy search cache: $error";
581 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
586 =item queue_fuzzyfiles_update
588 Used by insert & replace to update the fuzzy search cache
592 sub queue_fuzzyfiles_update {
595 local $SIG{HUP} = 'IGNORE';
596 local $SIG{INT} = 'IGNORE';
597 local $SIG{QUIT} = 'IGNORE';
598 local $SIG{TERM} = 'IGNORE';
599 local $SIG{TSTP} = 'IGNORE';
600 local $SIG{PIPE} = 'IGNORE';
602 my $oldAutoCommit = $FS::UID::AutoCommit;
603 local $FS::UID::AutoCommit = 0;
606 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
607 my $error = $queue->insert($self->getfield('last'), $self->company);
609 $dbh->rollback if $oldAutoCommit;
610 return "queueing job (transaction rolled back): $error";
613 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
614 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
615 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
617 $dbh->rollback if $oldAutoCommit;
618 return "queueing job (transaction rolled back): $error";
622 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
629 Checks all fields to make sure this is a valid customer record. If there is
630 an error, returns the error, otherwise returns false. Called by the insert
638 #warn "BEFORE: \n". $self->_dump;
641 $self->ut_numbern('custnum')
642 || $self->ut_number('agentnum')
643 || $self->ut_number('refnum')
644 || $self->ut_name('last')
645 || $self->ut_name('first')
646 || $self->ut_textn('company')
647 || $self->ut_text('address1')
648 || $self->ut_textn('address2')
649 || $self->ut_text('city')
650 || $self->ut_textn('county')
651 || $self->ut_textn('state')
652 || $self->ut_country('country')
653 || $self->ut_anything('comments')
654 || $self->ut_numbern('referral_custnum')
656 #barf. need message catalogs. i18n. etc.
657 $error .= "Please select an advertising source."
658 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
659 return $error if $error;
661 return "Unknown agent"
662 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
664 return "Unknown refnum"
665 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
667 return "Unknown referring custnum ". $self->referral_custnum
668 unless ! $self->referral_custnum
669 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
671 if ( $self->ss eq '' ) {
676 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
677 or return "Illegal social security number: ". $self->ss;
678 $self->ss("$1-$2-$3");
682 # bad idea to disable, causes billing to fail because of no tax rates later
683 # unless ( $import ) {
684 unless ( qsearch('cust_main_county', {
685 'country' => $self->country,
688 return "Unknown state/county/country: ".
689 $self->state. "/". $self->county. "/". $self->country
690 unless qsearch('cust_main_county',{
691 'state' => $self->state,
692 'county' => $self->county,
693 'country' => $self->country,
699 $self->ut_phonen('daytime', $self->country)
700 || $self->ut_phonen('night', $self->country)
701 || $self->ut_phonen('fax', $self->country)
702 || $self->ut_zip('zip', $self->country)
704 return $error if $error;
707 last first company address1 address2 city county state zip
708 country daytime night fax
711 if ( defined $self->dbdef_table->column('ship_last') ) {
712 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
714 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
718 $self->ut_name('ship_last')
719 || $self->ut_name('ship_first')
720 || $self->ut_textn('ship_company')
721 || $self->ut_text('ship_address1')
722 || $self->ut_textn('ship_address2')
723 || $self->ut_text('ship_city')
724 || $self->ut_textn('ship_county')
725 || $self->ut_textn('ship_state')
726 || $self->ut_country('ship_country')
728 return $error if $error;
730 #false laziness with above
731 unless ( qsearchs('cust_main_county', {
732 'country' => $self->ship_country,
735 return "Unknown ship_state/ship_county/ship_country: ".
736 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
737 unless qsearchs('cust_main_county',{
738 'state' => $self->ship_state,
739 'county' => $self->ship_county,
740 'country' => $self->ship_country,
746 $self->ut_phonen('ship_daytime', $self->ship_country)
747 || $self->ut_phonen('ship_night', $self->ship_country)
748 || $self->ut_phonen('ship_fax', $self->ship_country)
749 || $self->ut_zip('ship_zip', $self->ship_country)
751 return $error if $error;
753 } else { # ship_ info eq billing info, so don't store dup info in database
754 $self->setfield("ship_$_", '')
755 foreach qw( last first company address1 address2 city county state zip
756 country daytime night fax );
760 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
761 or return "Illegal payby: ". $self->payby;
764 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
766 my $payinfo = $self->payinfo;
768 $payinfo =~ /^(\d{13,16})$/
769 or return gettext('invalid_card'); # . ": ". $self->payinfo;
771 $self->payinfo($payinfo);
773 or return gettext('invalid_card'); # . ": ". $self->payinfo;
774 return gettext('unknown_card_type')
775 if cardtype($self->payinfo) eq "Unknown";
777 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
779 my $payinfo = $self->payinfo;
780 $payinfo =~ s/[^\d\@]//g;
781 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
783 $self->payinfo($payinfo);
785 } elsif ( $self->payby eq 'LECB' ) {
787 my $payinfo = $self->payinfo;
789 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
791 $self->payinfo($payinfo);
793 } elsif ( $self->payby eq 'BILL' ) {
795 $error = $self->ut_textn('payinfo');
796 return "Illegal P.O. number: ". $self->payinfo if $error;
798 } elsif ( $self->payby eq 'COMP' ) {
800 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
801 return "You are not permitted to create complimentary accounts."
802 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
805 $error = $self->ut_textn('payinfo');
806 return "Illegal comp account issuer: ". $self->payinfo if $error;
808 } elsif ( $self->payby eq 'PREPAY' ) {
810 my $payinfo = $self->payinfo;
811 $payinfo =~ s/\W//g; #anything else would just confuse things
812 $self->payinfo($payinfo);
813 $error = $self->ut_alpha('payinfo');
814 return "Illegal prepayment identifier: ". $self->payinfo if $error;
815 return "Unknown prepayment identifier"
816 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
820 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
821 return "Expriation date required"
822 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
826 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
827 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
828 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{2})[\/\-]\d+$/ ) {
829 ( $m, $y ) = ( $3, "20$2" );
831 return "Illegal expiration date: ". $self->paydate;
833 $self->paydate("$y-$m-01");
834 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
835 return gettext('expired_card')
836 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
839 if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
840 ( ! $conf->exists('require_cardname')
841 || $self->payby !~ /^(CARD|DCRD)$/ )
843 $self->payname( $self->first. " ". $self->getfield('last') );
845 $self->payname =~ /^([\w \,\.\-\']+)$/
846 or return gettext('illegal_name'). " payname: ". $self->payname;
850 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
853 $self->otaker(getotaker);
855 #warn "AFTER: \n". $self->_dump;
862 Returns all packages (see L<FS::cust_pkg>) for this customer.
868 if ( $self->{'_pkgnum'} ) {
869 values %{ $self->{'_pkgnum'}->cache };
871 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
875 =item ncancelled_pkgs
877 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
881 sub ncancelled_pkgs {
883 if ( $self->{'_pkgnum'} ) {
884 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
886 @{ [ # force list context
887 qsearch( 'cust_pkg', {
888 'custnum' => $self->custnum,
891 qsearch( 'cust_pkg', {
892 'custnum' => $self->custnum,
901 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
907 grep { $_->susp } $self->ncancelled_pkgs;
910 =item unflagged_suspended_pkgs
912 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
913 customer (thouse packages without the `manual_flag' set).
917 sub unflagged_suspended_pkgs {
919 return $self->suspended_pkgs
920 unless dbdef->table('cust_pkg')->column('manual_flag');
921 grep { ! $_->manual_flag } $self->suspended_pkgs;
924 =item unsuspended_pkgs
926 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
931 sub unsuspended_pkgs {
933 grep { ! $_->susp } $self->ncancelled_pkgs;
938 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
939 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
940 on success or a list of errors.
946 grep { $_->unsuspend } $self->suspended_pkgs;
951 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
952 Always returns a list: an empty list on success or a list of errors.
958 grep { $_->suspend } $self->unsuspended_pkgs;
961 =item cancel [ OPTION => VALUE ... ]
963 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
965 Available options are: I<quiet>
967 I<quiet> can be set true to supress email cancellation notices.
969 Always returns a list: an empty list on success or a list of errors.
975 grep { $_->cancel(@_) } $self->ncancelled_pkgs;
980 Returns the agent (see L<FS::agent>) for this customer.
986 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
991 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
992 conjunction with the collect method.
994 Options are passed as name-value pairs.
996 The only currently available option is `time', which bills the customer as if
997 it were that time. It is specified as a UNIX timestamp; see
998 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
999 functions. For example:
1003 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1005 If there is an error, returns the error, otherwise returns false.
1010 my( $self, %options ) = @_;
1011 my $time = $options{'time'} || time;
1016 local $SIG{HUP} = 'IGNORE';
1017 local $SIG{INT} = 'IGNORE';
1018 local $SIG{QUIT} = 'IGNORE';
1019 local $SIG{TERM} = 'IGNORE';
1020 local $SIG{TSTP} = 'IGNORE';
1021 local $SIG{PIPE} = 'IGNORE';
1023 my $oldAutoCommit = $FS::UID::AutoCommit;
1024 local $FS::UID::AutoCommit = 0;
1027 # find the packages which are due for billing, find out how much they are
1028 # & generate invoice database.
1030 my( $total_setup, $total_recur ) = ( 0, 0 );
1031 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1032 my @cust_bill_pkg = ();
1034 #my $taxable_charged = 0;##
1039 foreach my $cust_pkg (
1040 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1043 #NO!! next if $cust_pkg->cancel;
1044 next if $cust_pkg->getfield('cancel');
1046 #? to avoid use of uninitialized value errors... ?
1047 $cust_pkg->setfield('bill', '')
1048 unless defined($cust_pkg->bill);
1050 my $part_pkg = $cust_pkg->part_pkg;
1052 #so we don't modify cust_pkg record unnecessarily
1053 my $cust_pkg_mod_flag = 0;
1054 my %hash = $cust_pkg->hash;
1055 my $old_cust_pkg = new FS::cust_pkg \%hash;
1061 unless ( $cust_pkg->setup ) {
1062 my $setup_prog = $part_pkg->getfield('setup');
1063 $setup_prog =~ /^(.*)$/ or do {
1064 $dbh->rollback if $oldAutoCommit;
1065 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
1069 $setup_prog = '0' if $setup_prog =~ /^\s*$/;
1071 #my $cpt = new Safe;
1072 ##$cpt->permit(); #what is necessary?
1073 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1074 #$setup = $cpt->reval($setup_prog);
1075 $setup = eval $setup_prog;
1076 unless ( defined($setup) ) {
1077 $dbh->rollback if $oldAutoCommit;
1078 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
1079 "(expression $setup_prog): $@";
1081 $cust_pkg->setfield('setup',$time);
1082 $cust_pkg_mod_flag=1;
1088 if ( $part_pkg->getfield('freq') > 0 &&
1089 ! $cust_pkg->getfield('susp') &&
1090 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1092 my $recur_prog = $part_pkg->getfield('recur');
1093 $recur_prog =~ /^(.*)$/ or do {
1094 $dbh->rollback if $oldAutoCommit;
1095 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1099 $recur_prog = '0' if $recur_prog =~ /^\s*$/;
1101 # shared with $recur_prog
1102 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1104 #my $cpt = new Safe;
1105 ##$cpt->permit(); #what is necessary?
1106 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1107 #$recur = $cpt->reval($recur_prog);
1108 $recur = eval $recur_prog;
1109 unless ( defined($recur) ) {
1110 $dbh->rollback if $oldAutoCommit;
1111 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
1112 "(expression $recur_prog): $@";
1114 #change this bit to use Date::Manip? CAREFUL with timezones (see
1115 # mailing list archive)
1116 my ($sec,$min,$hour,$mday,$mon,$year) =
1117 (localtime($sdate) )[0,1,2,3,4,5];
1119 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1120 # only for figuring next bill date, nothing else, so, reset $sdate again
1122 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1123 $cust_pkg->last_bill($sdate)
1124 if $cust_pkg->dbdef_table->column('last_bill');
1126 $mon += $part_pkg->freq;
1127 until ( $mon < 12 ) { $mon -= 12; $year++; }
1128 $cust_pkg->setfield('bill',
1129 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1130 $cust_pkg_mod_flag = 1;
1133 warn "\$setup is undefined" unless defined($setup);
1134 warn "\$recur is undefined" unless defined($recur);
1135 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1137 if ( $cust_pkg_mod_flag ) {
1138 $error=$cust_pkg->replace($old_cust_pkg);
1139 if ( $error ) { #just in case
1140 $dbh->rollback if $oldAutoCommit;
1141 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1143 $setup = sprintf( "%.2f", $setup );
1144 $recur = sprintf( "%.2f", $recur );
1146 $dbh->rollback if $oldAutoCommit;
1147 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1150 $dbh->rollback if $oldAutoCommit;
1151 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1153 if ( $setup > 0 || $recur > 0 ) {
1154 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1155 'pkgnum' => $cust_pkg->pkgnum,
1159 'edate' => $cust_pkg->bill,
1160 'details' => \@details,
1162 push @cust_bill_pkg, $cust_bill_pkg;
1163 $total_setup += $setup;
1164 $total_recur += $recur;
1166 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1168 my @taxes = qsearch( 'cust_main_county', {
1169 'state' => $self->state,
1170 'county' => $self->county,
1171 'country' => $self->country,
1172 'taxclass' => $part_pkg->taxclass,
1175 @taxes = qsearch( 'cust_main_county', {
1176 'state' => $self->state,
1177 'county' => $self->county,
1178 'country' => $self->country,
1183 # maybe eliminate this entirely, along with all the 0% records
1185 $dbh->rollback if $oldAutoCommit;
1187 "fatal: can't find tax rate for state/county/country/taxclass ".
1188 join('/', ( map $self->$_(), qw(state county country) ),
1189 $part_pkg->taxclass ). "\n";
1192 foreach my $tax ( @taxes ) {
1194 my $taxable_charged = 0;
1195 $taxable_charged += $setup
1196 unless $part_pkg->setuptax =~ /^Y$/i
1197 || $tax->setuptax =~ /^Y$/i;
1198 $taxable_charged += $recur
1199 unless $part_pkg->recurtax =~ /^Y$/i
1200 || $tax->recurtax =~ /^Y$/i;
1201 next unless $taxable_charged;
1203 if ( $tax->exempt_amount ) {
1204 my ($mon,$year) = (localtime($sdate) )[4,5];
1206 my $freq = $part_pkg->freq || 1;
1207 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1208 foreach my $which_month ( 1 .. $freq ) {
1210 'custnum' => $self->custnum,
1211 'taxnum' => $tax->taxnum,
1212 'year' => 1900+$year,
1215 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1216 until ( $mon < 13 ) { $mon -= 12; $year++; }
1217 my $cust_tax_exempt =
1218 qsearchs('cust_tax_exempt', \%hash)
1219 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1220 my $remaining_exemption = sprintf("%.2f",
1221 $tax->exempt_amount - $cust_tax_exempt->amount );
1222 if ( $remaining_exemption > 0 ) {
1223 my $addl = $remaining_exemption > $taxable_per_month
1224 ? $taxable_per_month
1225 : $remaining_exemption;
1226 $taxable_charged -= $addl;
1227 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1228 $cust_tax_exempt->hash,
1230 sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1232 $error = $new_cust_tax_exempt->exemptnum
1233 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1234 : $new_cust_tax_exempt->insert;
1236 $dbh->rollback if $oldAutoCommit;
1237 return "fatal: can't update cust_tax_exempt: $error";
1240 } # if $remaining_exemption > 0
1242 } #foreach $which_month
1244 } #if $tax->exempt_amount
1246 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1248 #$tax += $taxable_charged * $cust_main_county->tax / 100
1249 $tax{ $tax->taxname || 'Tax' } +=
1250 $taxable_charged * $tax->tax / 100
1252 } #foreach my $tax ( @taxes )
1254 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1256 } #if $setup > 0 || $recur > 0
1258 } #if $cust_pkg_mod_flag
1260 } #foreach my $cust_pkg
1262 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1263 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1265 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1266 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1270 # unless ( $self->tax =~ /Y/i
1271 # || $self->payby eq 'COMP'
1272 # || $taxable_charged == 0 ) {
1273 # my $cust_main_county = qsearchs('cust_main_county',{
1274 # 'state' => $self->state,
1275 # 'county' => $self->county,
1276 # 'country' => $self->country,
1277 # } ) or die "fatal: can't find tax rate for state/county/country ".
1278 # $self->state. "/". $self->county. "/". $self->country. "\n";
1279 # my $tax = sprintf( "%.2f",
1280 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1283 if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1285 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1286 my $tax = sprintf("%.2f", $tax{$taxname} );
1287 $charged = sprintf( "%.2f", $charged+$tax );
1289 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1295 'itemdesc' => $taxname,
1297 push @cust_bill_pkg, $cust_bill_pkg;
1300 } else { #1.4 schema
1303 foreach ( values %tax ) { $tax += $_ };
1304 $tax = sprintf("%.2f", $tax);
1306 $charged = sprintf( "%.2f", $charged+$tax );
1308 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1315 push @cust_bill_pkg, $cust_bill_pkg;
1320 my $cust_bill = new FS::cust_bill ( {
1321 'custnum' => $self->custnum,
1323 'charged' => $charged,
1325 $error = $cust_bill->insert;
1327 $dbh->rollback if $oldAutoCommit;
1328 return "can't create invoice for customer #". $self->custnum. ": $error";
1331 my $invnum = $cust_bill->invnum;
1333 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1335 $cust_bill_pkg->invnum($invnum);
1336 $error = $cust_bill_pkg->insert;
1338 $dbh->rollback if $oldAutoCommit;
1339 return "can't create invoice line item for customer #". $self->custnum.
1344 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1348 =item collect OPTIONS
1350 (Attempt to) collect money for this customer's outstanding invoices (see
1351 L<FS::cust_bill>). Usually used after the bill method.
1353 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1354 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1355 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1357 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1358 and the invoice events web interface.
1360 If there is an error, returns the error, otherwise returns false.
1362 Options are passed as name-value pairs.
1364 Currently available options are:
1366 invoice_time - Use this time when deciding when to print invoices and
1367 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>
1368 for conversion functions.
1370 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1373 retry_card - Deprecated alias for 'retry'
1375 batch_card - This option is deprecated. See the invoice events web interface
1376 to control whether cards are batched or run against a realtime gateway.
1378 report_badcard - This option is deprecated.
1380 force_print - This option is deprecated; see the invoice events web interface.
1382 quiet - set true to surpress email card/ACH decline notices.
1387 my( $self, %options ) = @_;
1388 my $invoice_time = $options{'invoice_time'} || time;
1391 local $SIG{HUP} = 'IGNORE';
1392 local $SIG{INT} = 'IGNORE';
1393 local $SIG{QUIT} = 'IGNORE';
1394 local $SIG{TERM} = 'IGNORE';
1395 local $SIG{TSTP} = 'IGNORE';
1396 local $SIG{PIPE} = 'IGNORE';
1398 my $oldAutoCommit = $FS::UID::AutoCommit;
1399 local $FS::UID::AutoCommit = 0;
1402 my $balance = $self->balance;
1403 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1404 unless ( $balance > 0 ) { #redundant?????
1405 $dbh->rollback if $oldAutoCommit; #hmm
1409 if ( exists($options{'retry_card'}) ) {
1410 carp 'retry_card option passed to collect is deprecated; use retry';
1411 $options{'retry'} ||= $options{'retry_card'};
1413 if ( exists($options{'retry'}) && $options{'retry'} ) {
1414 my $error = $self->retry_realtime;
1416 $dbh->rollback if $oldAutoCommit;
1421 foreach my $cust_bill ( $self->cust_bill ) {
1423 #this has to be before next's
1424 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1428 $balance = sprintf( "%.2f", $balance - $amount );
1430 next unless $cust_bill->owed > 0;
1432 # don't try to charge for the same invoice if it's already in a batch
1433 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1435 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1437 next unless $amount > 0;
1440 foreach my $part_bill_event (
1441 sort { $a->seconds <=> $b->seconds
1442 || $a->weight <=> $b->weight
1443 || $a->eventpart <=> $b->eventpart }
1444 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1445 && ! qsearchs( 'cust_bill_event', {
1446 'invnum' => $cust_bill->invnum,
1447 'eventpart' => $_->eventpart,
1451 qsearch('part_bill_event', { 'payby' => $self->payby,
1452 'disabled' => '', } )
1455 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1457 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1459 my $cust_main = $self; #for callback
1463 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1464 $error = eval $part_bill_event->eventcode;
1468 my $statustext = '';
1472 } elsif ( $error ) {
1474 $statustext = $error;
1479 #add cust_bill_event
1480 my $cust_bill_event = new FS::cust_bill_event {
1481 'invnum' => $cust_bill->invnum,
1482 'eventpart' => $part_bill_event->eventpart,
1483 #'_date' => $invoice_time,
1485 'status' => $status,
1486 'statustext' => $statustext,
1488 $error = $cust_bill_event->insert;
1490 #$dbh->rollback if $oldAutoCommit;
1491 #return "error: $error";
1493 # gah, even with transactions.
1494 $dbh->commit if $oldAutoCommit; #well.
1495 my $e = 'WARNING: Event run but database not updated - '.
1496 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1497 ', eventpart '. $part_bill_event->eventpart.
1508 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1513 =item retry_realtime
1515 Schedules realtime credit card / electronic check / LEC billing events for
1516 for retry. Useful if card information has changed or manual retry is desired.
1517 The 'collect' method must be called to actually retry the transaction.
1519 Implementation details: For each of this customer's open invoices, changes
1520 the status of the first "done" (with statustext error) realtime processing
1525 sub retry_realtime {
1528 local $SIG{HUP} = 'IGNORE';
1529 local $SIG{INT} = 'IGNORE';
1530 local $SIG{QUIT} = 'IGNORE';
1531 local $SIG{TERM} = 'IGNORE';
1532 local $SIG{TSTP} = 'IGNORE';
1533 local $SIG{PIPE} = 'IGNORE';
1535 my $oldAutoCommit = $FS::UID::AutoCommit;
1536 local $FS::UID::AutoCommit = 0;
1539 foreach my $cust_bill (
1540 grep { $_->cust_bill_event }
1541 $self->open_cust_bill
1543 my @cust_bill_event =
1544 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1546 #$_->part_bill_event->plan eq 'realtime-card'
1547 $_->part_bill_event->eventcode =~
1548 /\$cust_bill\->realtime_(card|ach|lec)/
1549 && $_->status eq 'done'
1552 $cust_bill->cust_bill_event;
1553 next unless @cust_bill_event;
1554 my $error = $cust_bill_event[0]->retry;
1556 $dbh->rollback if $oldAutoCommit;
1557 return "error scheduling invoice event for retry: $error";
1562 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1567 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1569 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1570 via a Business::OnlinePayment realtime gateway. See
1571 L<http://420.am/business-onlinepayment> for supported gateways.
1573 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1575 Available options are: I<description>, I<invnum>, I<quiet>
1577 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1578 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1579 if set, will override the value from the customer record.
1581 I<description> is a free-text field passed to the gateway. It defaults to
1582 "Internet services".
1584 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1585 specified invoice. If you don't specify an I<invnum> you might want to
1586 call the B<apply_payments> method.
1588 I<quiet> can be set true to surpress email decline notices.
1590 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1595 my( $self, $method, $amount, %options ) = @_;
1597 warn "$self $method $amount\n";
1598 warn " $_ => $options{$_}\n" foreach keys %options;
1601 $options{'description'} ||= 'Internet services';
1604 die "Real-time processing not enabled\n"
1605 unless $conf->exists('business-onlinepayment');
1606 eval "use Business::OnlinePayment";
1610 $self->set( $_ => $options{$_} )
1611 foreach grep { exists($options{$_}) }
1612 qw( payname address1 address2 city state zip payinfo paydate );
1615 my $bop_config = 'business-onlinepayment';
1616 $bop_config .= '-ach'
1617 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1618 my ( $processor, $login, $password, $action, @bop_options ) =
1619 $conf->config($bop_config);
1620 $action ||= 'normal authorization';
1621 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1625 my $address = $self->address1;
1626 $address .= ", ". $self->address2 if $self->address2;
1628 my($payname, $payfirst, $paylast);
1629 if ( $self->payname && $method ne 'ECHECK' ) {
1630 $payname = $self->payname;
1631 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1632 or return "Illegal payname $payname";
1633 ($payfirst, $paylast) = ($1, $2);
1635 $payfirst = $self->getfield('first');
1636 $paylast = $self->getfield('last');
1637 $payname = "$payfirst $paylast";
1640 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1641 if ( $conf->exists('emailinvoiceauto')
1642 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1643 push @invoicing_list, $self->all_emails;
1645 my $email = $invoicing_list[0];
1648 if ( $method eq 'CC' ) {
1649 $content{card_number} = $self->payinfo;
1650 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1651 $content{expiration} = "$2/$1";
1652 } elsif ( $method eq 'ECHECK' ) {
1653 my($account_number,$routing_code) = $self->payinfo;
1654 ( $content{account_number}, $content{routing_code} ) =
1655 split('@', $self->payinfo);
1656 $content{bank_name} = $self->payname;
1657 $content{account_type} = 'CHECKING';
1658 $content{account_name} = $payname;
1659 $content{customer_org} = $self->company ? 'B' : 'I';
1660 $content{customer_ssn} = $self->ss;
1661 } elsif ( $method eq 'LEC' ) {
1662 $content{phone} = $self->payinfo;
1667 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1670 new Business::OnlinePayment( $processor, @bop_options );
1671 $transaction->content(
1674 'password' => $password,
1675 'action' => $action1,
1676 'description' => $options{'description'},
1677 'amount' => $amount,
1678 'invoice_number' => $options{'invnum'},
1679 'customer_id' => $self->custnum,
1680 'last_name' => $paylast,
1681 'first_name' => $payfirst,
1683 'address' => $address,
1684 'city' => $self->city,
1685 'state' => $self->state,
1686 'zip' => $self->zip,
1687 'country' => $self->country,
1688 'referer' => 'http://cleanwhisker.420.am/',
1690 'phone' => $self->daytime || $self->night,
1693 $transaction->submit();
1695 if ( $transaction->is_success() && $action2 ) {
1696 my $auth = $transaction->authorization;
1697 my $ordernum = $transaction->can('order_number')
1698 ? $transaction->order_number
1702 new Business::OnlinePayment( $processor, @bop_options );
1709 password => $password,
1710 order_number => $ordernum,
1712 authorization => $auth,
1713 description => $options{'description'},
1716 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
1717 transaction_sequence_num local_transaction_date
1718 local_transaction_time AVS_result_code )) {
1719 $capture{$field} = $transaction->$field() if $transaction->can($field);
1722 $capture->content( %capture );
1726 unless ( $capture->is_success ) {
1727 my $e = "Authorization sucessful but capture failed, custnum #".
1728 $self->custnum. ': '. $capture->result_code.
1729 ": ". $capture->error_message;
1737 if ( $transaction->is_success() ) {
1739 my %method2payby = (
1745 my $cust_pay = new FS::cust_pay ( {
1746 'custnum' => $self->custnum,
1747 'invnum' => $options{'invnum'},
1750 'payby' => $method2payby{$method},
1751 'payinfo' => $self->payinfo,
1752 'paybatch' => "$processor:". $transaction->authorization,
1754 my $error = $cust_pay->insert;
1756 # gah, even with transactions.
1757 my $e = 'WARNING: Card/ACH debited but database not updated - '.
1758 'error applying payment, invnum #' . $self->invnum.
1759 " ($processor): $error";
1768 my $perror = "$processor error: ". $transaction->error_message;
1770 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1771 && $conf->exists('emaildecline')
1772 && grep { $_ ne 'POST' } $self->invoicing_list
1774 my @templ = $conf->config('declinetemplate');
1775 my $template = new Text::Template (
1777 SOURCE => [ map "$_\n", @templ ],
1778 ) or return "($perror) can't create template: $Text::Template::ERROR";
1779 $template->compile()
1780 or return "($perror) can't compile template: $Text::Template::ERROR";
1782 my $templ_hash = { error => $transaction->error_message };
1784 my $error = send_email(
1785 'from' => $conf->config('invoice_from'),
1786 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1787 'subject' => 'Your payment could not be processed',
1788 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1791 $perror .= " (also received error sending decline notification: $error)"
1803 Returns the total owed for this customer on all invoices
1804 (see L<FS::cust_bill/owed>).
1810 $self->total_owed_date(2145859200); #12/31/2037
1813 =item total_owed_date TIME
1815 Returns the total owed for this customer on all invoices with date earlier than
1816 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1817 see L<Time::Local> and L<Date::Parse> for conversion functions.
1821 sub total_owed_date {
1825 foreach my $cust_bill (
1826 grep { $_->_date <= $time }
1827 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1829 $total_bill += $cust_bill->owed;
1831 sprintf( "%.2f", $total_bill );
1836 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1837 to outstanding invoice balances in chronological order and returns the value
1838 of any remaining unapplied credits available for refund
1839 (see L<FS::cust_refund>).
1846 return 0 unless $self->total_credited;
1848 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1849 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1851 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1852 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1856 foreach my $cust_bill ( @invoices ) {
1859 if ( !defined($credit) || $credit->credited == 0) {
1860 $credit = pop @credits or last;
1863 if ($cust_bill->owed >= $credit->credited) {
1864 $amount=$credit->credited;
1866 $amount=$cust_bill->owed;
1869 my $cust_credit_bill = new FS::cust_credit_bill ( {
1870 'crednum' => $credit->crednum,
1871 'invnum' => $cust_bill->invnum,
1872 'amount' => $amount,
1874 my $error = $cust_credit_bill->insert;
1875 die $error if $error;
1877 redo if ($cust_bill->owed > 0);
1881 return $self->total_credited;
1884 =item apply_payments
1886 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1887 to outstanding invoice balances in chronological order.
1889 #and returns the value of any remaining unapplied payments.
1893 sub apply_payments {
1898 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1899 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1901 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1902 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1906 foreach my $cust_bill ( @invoices ) {
1909 if ( !defined($payment) || $payment->unapplied == 0 ) {
1910 $payment = pop @payments or last;
1913 if ( $cust_bill->owed >= $payment->unapplied ) {
1914 $amount = $payment->unapplied;
1916 $amount = $cust_bill->owed;
1919 my $cust_bill_pay = new FS::cust_bill_pay ( {
1920 'paynum' => $payment->paynum,
1921 'invnum' => $cust_bill->invnum,
1922 'amount' => $amount,
1924 my $error = $cust_bill_pay->insert;
1925 die $error if $error;
1927 redo if ( $cust_bill->owed > 0);
1931 return $self->total_unapplied_payments;
1934 =item total_credited
1936 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1937 customer. See L<FS::cust_credit/credited>.
1941 sub total_credited {
1943 my $total_credit = 0;
1944 foreach my $cust_credit ( qsearch('cust_credit', {
1945 'custnum' => $self->custnum,
1947 $total_credit += $cust_credit->credited;
1949 sprintf( "%.2f", $total_credit );
1952 =item total_unapplied_payments
1954 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1955 See L<FS::cust_pay/unapplied>.
1959 sub total_unapplied_payments {
1961 my $total_unapplied = 0;
1962 foreach my $cust_pay ( qsearch('cust_pay', {
1963 'custnum' => $self->custnum,
1965 $total_unapplied += $cust_pay->unapplied;
1967 sprintf( "%.2f", $total_unapplied );
1972 Returns the balance for this customer (total_owed minus total_credited
1973 minus total_unapplied_payments).
1980 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1984 =item balance_date TIME
1986 Returns the balance for this customer, only considering invoices with date
1987 earlier than TIME (total_owed_date minus total_credited minus
1988 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1989 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1998 $self->total_owed_date($time)
1999 - $self->total_credited
2000 - $self->total_unapplied_payments
2004 =item invoicing_list [ ARRAYREF ]
2006 If an arguement is given, sets these email addresses as invoice recipients
2007 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
2008 (except as warnings), so use check_invoicing_list first.
2010 Returns a list of email addresses (with svcnum entries expanded).
2012 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
2013 check it without disturbing anything by passing nothing.
2015 This interface may change in the future.
2019 sub invoicing_list {
2020 my( $self, $arrayref ) = @_;
2022 my @cust_main_invoice;
2023 if ( $self->custnum ) {
2024 @cust_main_invoice =
2025 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2027 @cust_main_invoice = ();
2029 foreach my $cust_main_invoice ( @cust_main_invoice ) {
2030 #warn $cust_main_invoice->destnum;
2031 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2032 #warn $cust_main_invoice->destnum;
2033 my $error = $cust_main_invoice->delete;
2034 warn $error if $error;
2037 if ( $self->custnum ) {
2038 @cust_main_invoice =
2039 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2041 @cust_main_invoice = ();
2043 my %seen = map { $_->address => 1 } @cust_main_invoice;
2044 foreach my $address ( @{$arrayref} ) {
2045 next if exists $seen{$address} && $seen{$address};
2046 $seen{$address} = 1;
2047 my $cust_main_invoice = new FS::cust_main_invoice ( {
2048 'custnum' => $self->custnum,
2051 my $error = $cust_main_invoice->insert;
2052 warn $error if $error;
2055 if ( $self->custnum ) {
2057 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2063 =item check_invoicing_list ARRAYREF
2065 Checks these arguements as valid input for the invoicing_list method. If there
2066 is an error, returns the error, otherwise returns false.
2070 sub check_invoicing_list {
2071 my( $self, $arrayref ) = @_;
2072 foreach my $address ( @{$arrayref} ) {
2073 my $cust_main_invoice = new FS::cust_main_invoice ( {
2074 'custnum' => $self->custnum,
2077 my $error = $self->custnum
2078 ? $cust_main_invoice->check
2079 : $cust_main_invoice->checkdest
2081 return $error if $error;
2086 =item set_default_invoicing_list
2088 Sets the invoicing list to all accounts associated with this customer,
2089 overwriting any previous invoicing list.
2093 sub set_default_invoicing_list {
2095 $self->invoicing_list($self->all_emails);
2100 Returns the email addresses of all accounts provisioned for this customer.
2107 foreach my $cust_pkg ( $self->all_pkgs ) {
2108 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2110 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2111 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2113 $list{$_}=1 foreach map { $_->email } @svc_acct;
2118 =item invoicing_list_addpost
2120 Adds postal invoicing to this customer. If this customer is already configured
2121 to receive postal invoices, does nothing.
2125 sub invoicing_list_addpost {
2127 return if grep { $_ eq 'POST' } $self->invoicing_list;
2128 my @invoicing_list = $self->invoicing_list;
2129 push @invoicing_list, 'POST';
2130 $self->invoicing_list(\@invoicing_list);
2133 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2135 Returns an array of customers referred by this customer (referral_custnum set
2136 to this custnum). If DEPTH is given, recurses up to the given depth, returning
2137 customers referred by customers referred by this customer and so on, inclusive.
2138 The default behavior is DEPTH 1 (no recursion).
2142 sub referral_cust_main {
2144 my $depth = @_ ? shift : 1;
2145 my $exclude = @_ ? shift : {};
2148 map { $exclude->{$_->custnum}++; $_; }
2149 grep { ! $exclude->{ $_->custnum } }
2150 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2154 map { $_->referral_cust_main($depth-1, $exclude) }
2161 =item referral_cust_main_ncancelled
2163 Same as referral_cust_main, except only returns customers with uncancelled
2168 sub referral_cust_main_ncancelled {
2170 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2173 =item referral_cust_pkg [ DEPTH ]
2175 Like referral_cust_main, except returns a flat list of all unsuspended (and
2176 uncancelled) packages for each customer. The number of items in this list may
2177 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2181 sub referral_cust_pkg {
2183 my $depth = @_ ? shift : 1;
2185 map { $_->unsuspended_pkgs }
2186 grep { $_->unsuspended_pkgs }
2187 $self->referral_cust_main($depth);
2190 =item credit AMOUNT, REASON
2192 Applies a credit to this customer. If there is an error, returns the error,
2193 otherwise returns false.
2198 my( $self, $amount, $reason ) = @_;
2199 my $cust_credit = new FS::cust_credit {
2200 'custnum' => $self->custnum,
2201 'amount' => $amount,
2202 'reason' => $reason,
2204 $cust_credit->insert;
2207 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2209 Creates a one-time charge for this customer. If there is an error, returns
2210 the error, otherwise returns false.
2215 my ( $self, $amount ) = ( shift, shift );
2216 my $pkg = @_ ? shift : 'One-time charge';
2217 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2218 my $taxclass = @_ ? shift : '';
2220 local $SIG{HUP} = 'IGNORE';
2221 local $SIG{INT} = 'IGNORE';
2222 local $SIG{QUIT} = 'IGNORE';
2223 local $SIG{TERM} = 'IGNORE';
2224 local $SIG{TSTP} = 'IGNORE';
2225 local $SIG{PIPE} = 'IGNORE';
2227 my $oldAutoCommit = $FS::UID::AutoCommit;
2228 local $FS::UID::AutoCommit = 0;
2231 my $part_pkg = new FS::part_pkg ( {
2233 'comment' => $comment,
2238 'taxclass' => $taxclass,
2241 my $error = $part_pkg->insert;
2243 $dbh->rollback if $oldAutoCommit;
2247 my $pkgpart = $part_pkg->pkgpart;
2248 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2249 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2250 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2251 $error = $type_pkgs->insert;
2253 $dbh->rollback if $oldAutoCommit;
2258 my $cust_pkg = new FS::cust_pkg ( {
2259 'custnum' => $self->custnum,
2260 'pkgpart' => $pkgpart,
2263 $error = $cust_pkg->insert;
2265 $dbh->rollback if $oldAutoCommit;
2269 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2276 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2282 sort { $a->_date <=> $b->_date }
2283 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2286 =item open_cust_bill
2288 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2293 sub open_cust_bill {
2295 grep { $_->owed > 0 } $self->cust_bill;
2304 =item check_and_rebuild_fuzzyfiles
2308 sub check_and_rebuild_fuzzyfiles {
2309 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2310 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
2311 or &rebuild_fuzzyfiles;
2314 =item rebuild_fuzzyfiles
2318 sub rebuild_fuzzyfiles {
2320 use Fcntl qw(:flock);
2322 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2326 open(LASTLOCK,">>$dir/cust_main.last")
2327 or die "can't open $dir/cust_main.last: $!";
2328 flock(LASTLOCK,LOCK_EX)
2329 or die "can't lock $dir/cust_main.last: $!";
2331 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
2333 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
2334 if defined dbdef->table('cust_main')->column('ship_last');
2336 open (LASTCACHE,">$dir/cust_main.last.tmp")
2337 or die "can't open $dir/cust_main.last.tmp: $!";
2338 print LASTCACHE join("\n", @all_last), "\n";
2339 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
2341 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
2346 open(COMPANYLOCK,">>$dir/cust_main.company")
2347 or die "can't open $dir/cust_main.company: $!";
2348 flock(COMPANYLOCK,LOCK_EX)
2349 or die "can't lock $dir/cust_main.company: $!";
2351 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
2353 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
2354 if defined dbdef->table('cust_main')->column('ship_last');
2356 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
2357 or die "can't open $dir/cust_main.company.tmp: $!";
2358 print COMPANYCACHE join("\n", @all_company), "\n";
2359 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
2361 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
2371 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2372 open(LASTCACHE,"<$dir/cust_main.last")
2373 or die "can't open $dir/cust_main.last: $!";
2374 my @array = map { chomp; $_; } <LASTCACHE>;
2384 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2385 open(COMPANYCACHE,"<$dir/cust_main.company")
2386 or die "can't open $dir/cust_main.last: $!";
2387 my @array = map { chomp; $_; } <COMPANYCACHE>;
2392 =item append_fuzzyfiles LASTNAME COMPANY
2396 sub append_fuzzyfiles {
2397 my( $last, $company ) = @_;
2399 &check_and_rebuild_fuzzyfiles;
2401 use Fcntl qw(:flock);
2403 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2407 open(LAST,">>$dir/cust_main.last")
2408 or die "can't open $dir/cust_main.last: $!";
2410 or die "can't lock $dir/cust_main.last: $!";
2412 print LAST "$last\n";
2415 or die "can't unlock $dir/cust_main.last: $!";
2421 open(COMPANY,">>$dir/cust_main.company")
2422 or die "can't open $dir/cust_main.company: $!";
2423 flock(COMPANY,LOCK_EX)
2424 or die "can't lock $dir/cust_main.company: $!";
2426 print COMPANY "$company\n";
2428 flock(COMPANY,LOCK_UN)
2429 or die "can't unlock $dir/cust_main.company: $!";
2443 #warn join('-',keys %$param);
2444 my $fh = $param->{filehandle};
2445 my $agentnum = $param->{agentnum};
2446 my $refnum = $param->{refnum};
2447 my $pkgpart = $param->{pkgpart};
2448 my @fields = @{$param->{fields}};
2450 eval "use Date::Parse;";
2452 eval "use Text::CSV_XS;";
2455 my $csv = new Text::CSV_XS;
2462 local $SIG{HUP} = 'IGNORE';
2463 local $SIG{INT} = 'IGNORE';
2464 local $SIG{QUIT} = 'IGNORE';
2465 local $SIG{TERM} = 'IGNORE';
2466 local $SIG{TSTP} = 'IGNORE';
2467 local $SIG{PIPE} = 'IGNORE';
2469 my $oldAutoCommit = $FS::UID::AutoCommit;
2470 local $FS::UID::AutoCommit = 0;
2473 #while ( $columns = $csv->getline($fh) ) {
2475 while ( defined($line=<$fh>) ) {
2477 $csv->parse($line) or do {
2478 $dbh->rollback if $oldAutoCommit;
2479 return "can't parse: ". $csv->error_input();
2482 my @columns = $csv->fields();
2483 #warn join('-',@columns);
2486 agentnum => $agentnum,
2488 country => 'US', #default
2489 payby => 'BILL', #default
2490 paydate => '12/2037', #default
2492 my $billtime = time;
2493 my %cust_pkg = ( pkgpart => $pkgpart );
2494 foreach my $field ( @fields ) {
2495 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2496 #$cust_pkg{$1} = str2time( shift @$columns );
2497 if ( $1 eq 'setup' ) {
2498 $billtime = str2time(shift @columns);
2500 $cust_pkg{$1} = str2time( shift @columns );
2503 #$cust_main{$field} = shift @$columns;
2504 $cust_main{$field} = shift @columns;
2508 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2509 my $cust_main = new FS::cust_main ( \%cust_main );
2511 tie my %hash, 'Tie::RefHash'; #this part is important
2512 $hash{$cust_pkg} = [] if $pkgpart;
2513 my $error = $cust_main->insert( \%hash );
2516 $dbh->rollback if $oldAutoCommit;
2517 return "can't insert customer for $line: $error";
2520 #false laziness w/bill.cgi
2521 $error = $cust_main->bill( 'time' => $billtime );
2523 $dbh->rollback if $oldAutoCommit;
2524 return "can't bill customer for $line: $error";
2527 $cust_main->apply_payments;
2528 $cust_main->apply_credits;
2530 $error = $cust_main->collect();
2532 $dbh->rollback if $oldAutoCommit;
2533 return "can't collect customer for $line: $error";
2539 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2541 return "Empty file!" unless $imported;
2553 #warn join('-',keys %$param);
2554 my $fh = $param->{filehandle};
2555 my @fields = @{$param->{fields}};
2557 eval "use Date::Parse;";
2559 eval "use Text::CSV_XS;";
2562 my $csv = new Text::CSV_XS;
2569 local $SIG{HUP} = 'IGNORE';
2570 local $SIG{INT} = 'IGNORE';
2571 local $SIG{QUIT} = 'IGNORE';
2572 local $SIG{TERM} = 'IGNORE';
2573 local $SIG{TSTP} = 'IGNORE';
2574 local $SIG{PIPE} = 'IGNORE';
2576 my $oldAutoCommit = $FS::UID::AutoCommit;
2577 local $FS::UID::AutoCommit = 0;
2580 #while ( $columns = $csv->getline($fh) ) {
2582 while ( defined($line=<$fh>) ) {
2584 $csv->parse($line) or do {
2585 $dbh->rollback if $oldAutoCommit;
2586 return "can't parse: ". $csv->error_input();
2589 my @columns = $csv->fields();
2590 #warn join('-',@columns);
2593 foreach my $field ( @fields ) {
2594 $row{$field} = shift @columns;
2597 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2598 unless ( $cust_main ) {
2599 $dbh->rollback if $oldAutoCommit;
2600 return "unknown custnum $row{'custnum'}";
2603 if ( $row{'amount'} > 0 ) {
2604 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2606 $dbh->rollback if $oldAutoCommit;
2610 } elsif ( $row{'amount'} < 0 ) {
2611 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2614 $dbh->rollback if $oldAutoCommit;
2624 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2626 return "Empty file!" unless $imported;
2638 The delete method should possibly take an FS::cust_main object reference
2639 instead of a scalar customer number.
2641 Bill and collect options should probably be passed as references instead of a
2644 There should probably be a configuration file with a list of allowed credit
2647 No multiple currency support (probably a larger project than just this module).
2651 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2652 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2653 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.