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);";
13 eval "use Time::Local qw(timelocal_nocheck);";
17 use String::Approx qw(amatch);
18 use Business::CreditCard;
19 use FS::UID qw( getotaker dbh );
20 use FS::Record qw( qsearchs qsearch dbdef );
21 use FS::Misc qw( send_email );
24 use FS::cust_bill_pkg;
26 use FS::cust_pay_void;
29 use FS::part_referral;
30 use FS::cust_main_county;
32 use FS::cust_main_invoice;
33 use FS::cust_credit_bill;
34 use FS::cust_bill_pay;
35 use FS::prepay_credit;
38 use FS::part_bill_event;
39 use FS::cust_bill_event;
40 use FS::cust_tax_exempt;
42 use FS::Msgcat qw(gettext);
44 @ISA = qw( FS::Record );
46 $realtime_bop_decline_quiet = 0;
53 #ask FS::UID to run this stuff for us later
54 #$FS::UID::callback{'FS::cust_main'} = sub {
55 install_callback FS::UID sub {
57 #yes, need it for stuff below (prolly should be cached)
62 my ( $hashref, $cache ) = @_;
63 if ( exists $hashref->{'pkgnum'} ) {
64 # #@{ $self->{'_pkgnum'} } = ();
65 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
66 $self->{'_pkgnum'} = $subcache;
67 #push @{ $self->{'_pkgnum'} },
68 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
74 FS::cust_main - Object methods for cust_main records
80 $record = new FS::cust_main \%hash;
81 $record = new FS::cust_main { 'column' => 'value' };
83 $error = $record->insert;
85 $error = $new_record->replace($old_record);
87 $error = $record->delete;
89 $error = $record->check;
91 @cust_pkg = $record->all_pkgs;
93 @cust_pkg = $record->ncancelled_pkgs;
95 @cust_pkg = $record->suspended_pkgs;
97 $error = $record->bill;
98 $error = $record->bill %options;
99 $error = $record->bill 'time' => $time;
101 $error = $record->collect;
102 $error = $record->collect %options;
103 $error = $record->collect 'invoice_time' => $time,
104 'batch_card' => 'yes',
105 'report_badcard' => 'yes',
110 An FS::cust_main object represents a customer. FS::cust_main inherits from
111 FS::Record. The following fields are currently supported:
115 =item custnum - primary key (assigned automatically for new customers)
117 =item agentnum - agent (see L<FS::agent>)
119 =item refnum - Advertising source (see L<FS::part_referral>)
125 =item ss - social security number (optional)
127 =item company - (optional)
131 =item address2 - (optional)
135 =item county - (optional, see L<FS::cust_main_county>)
137 =item state - (see L<FS::cust_main_county>)
141 =item country - (see L<FS::cust_main_county>)
143 =item daytime - phone (optional)
145 =item night - phone (optional)
147 =item fax - phone (optional)
149 =item ship_first - name
151 =item ship_last - name
153 =item ship_company - (optional)
157 =item ship_address2 - (optional)
161 =item ship_county - (optional, see L<FS::cust_main_county>)
163 =item ship_state - (see L<FS::cust_main_county>)
167 =item ship_country - (see L<FS::cust_main_county>)
169 =item ship_daytime - phone (optional)
171 =item ship_night - phone (optional)
173 =item ship_fax - phone (optional)
175 =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>)
177 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
179 =item paycvv - Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
181 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
183 =item payname - name on card or billing name
185 =item tax - tax exempt, empty or `Y'
187 =item otaker - order taker (assigned automatically, see L<FS::UID>)
189 =item comments - comments (optional)
191 =item referral_custnum - referring customer number
201 Creates a new customer. To add the customer to the database, see L<"insert">.
203 Note that this stores the hash reference, not a distinct copy of the hash it
204 points to. You can ask the object for a copy with the I<hash> method.
208 sub table { 'cust_main'; }
210 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
212 Adds this customer to the database. If there is an error, returns the error,
213 otherwise returns false.
215 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
216 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
217 are inserted atomicly, or the transaction is rolled back. Passing an empty
218 hash reference is equivalent to not supplying this parameter. There should be
219 a better explanation of this, but until then, here's an example:
222 tie %hash, 'Tie::RefHash'; #this part is important
224 $cust_pkg => [ $svc_acct ],
227 $cust_main->insert( \%hash );
229 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
230 be set as the invoicing list (see L<"invoicing_list">). Errors return as
231 expected and rollback the entire transaction; it is not necessary to call
232 check_invoicing_list first. The invoicing_list is set after the records in the
233 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
234 invoicing_list destination to the newly-created svc_acct. Here's an example:
236 $cust_main->insert( {}, [ $email, 'POST' ] );
238 Currently available options are: I<depend_jobnum> and I<noexport>.
240 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
241 on the supplied jobnum (they will not run until the specific job completes).
242 This can be used to defer provisioning until some action completes (such
243 as running the customer's credit card sucessfully).
245 The I<noexport> option is deprecated. If I<noexport> is set true, no
246 provisioning jobs (exports) are scheduled. (You can schedule them later with
247 the B<reexport> method.)
253 my $cust_pkgs = @_ ? shift : {};
254 my $invoicing_list = @_ ? shift : '';
256 warn "FS::cust_main::insert called with options ".
257 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
260 local $SIG{HUP} = 'IGNORE';
261 local $SIG{INT} = 'IGNORE';
262 local $SIG{QUIT} = 'IGNORE';
263 local $SIG{TERM} = 'IGNORE';
264 local $SIG{TSTP} = 'IGNORE';
265 local $SIG{PIPE} = 'IGNORE';
267 my $oldAutoCommit = $FS::UID::AutoCommit;
268 local $FS::UID::AutoCommit = 0;
273 if ( $self->payby eq 'PREPAY' ) {
274 $self->payby('BILL');
275 my $prepay_credit = qsearchs(
277 { 'identifier' => $self->payinfo },
281 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
282 unless $prepay_credit;
283 $amount = $prepay_credit->amount;
284 $seconds = $prepay_credit->seconds;
285 my $error = $prepay_credit->delete;
287 $dbh->rollback if $oldAutoCommit;
288 return "removing prepay_credit (transaction rolled back): $error";
292 my $error = $self->SUPER::insert;
294 $dbh->rollback if $oldAutoCommit;
295 #return "inserting cust_main record (transaction rolled back): $error";
300 if ( $invoicing_list ) {
301 $error = $self->check_invoicing_list( $invoicing_list );
303 $dbh->rollback if $oldAutoCommit;
304 return "checking invoicing_list (transaction rolled back): $error";
306 $self->invoicing_list( $invoicing_list );
310 $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
312 $dbh->rollback if $oldAutoCommit;
317 $dbh->rollback if $oldAutoCommit;
318 return "No svc_acct record to apply pre-paid time";
322 my $cust_credit = new FS::cust_credit {
323 'custnum' => $self->custnum,
326 $error = $cust_credit->insert;
328 $dbh->rollback if $oldAutoCommit;
329 return "inserting credit (transaction rolled back): $error";
333 $error = $self->queue_fuzzyfiles_update;
335 $dbh->rollback if $oldAutoCommit;
336 return "updating fuzzy search cache: $error";
339 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
344 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
346 Like the insert method on an existing record, this method orders a package
347 and included services atomicaly. Pass a Tie::RefHash data structure to this
348 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
349 be a better explanation of this, but until then, here's an example:
352 tie %hash, 'Tie::RefHash'; #this part is important
354 $cust_pkg => [ $svc_acct ],
357 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
359 Currently available options are: I<depend_jobnum> and I<noexport>.
361 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
362 on the supplied jobnum (they will not run until the specific job completes).
363 This can be used to defer provisioning until some action completes (such
364 as running the customer's credit card sucessfully).
366 The I<noexport> option is deprecated. If I<noexport> is set true, no
367 provisioning jobs (exports) are scheduled. (You can schedule them later with
368 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
369 on the cust_main object is not recommended, as existing services will also be
376 my $cust_pkgs = shift;
379 my %svc_options = ();
380 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
381 if exists $options{'depend_jobnum'};
382 warn "FS::cust_main::order_pkgs called with options ".
383 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
386 local $SIG{HUP} = 'IGNORE';
387 local $SIG{INT} = 'IGNORE';
388 local $SIG{QUIT} = 'IGNORE';
389 local $SIG{TERM} = 'IGNORE';
390 local $SIG{TSTP} = 'IGNORE';
391 local $SIG{PIPE} = 'IGNORE';
393 my $oldAutoCommit = $FS::UID::AutoCommit;
394 local $FS::UID::AutoCommit = 0;
397 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
399 foreach my $cust_pkg ( keys %$cust_pkgs ) {
400 $cust_pkg->custnum( $self->custnum );
401 my $error = $cust_pkg->insert;
403 $dbh->rollback if $oldAutoCommit;
404 return "inserting cust_pkg (transaction rolled back): $error";
406 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
407 $svc_something->pkgnum( $cust_pkg->pkgnum );
408 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
409 $svc_something->seconds( $svc_something->seconds + $$seconds );
412 $error = $svc_something->insert(%svc_options);
414 $dbh->rollback if $oldAutoCommit;
415 #return "inserting svc_ (transaction rolled back): $error";
421 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
427 This method is deprecated. See the I<depend_jobnum> option to the insert and
428 order_pkgs methods for a better way to defer provisioning.
430 Re-schedules all exports by calling the B<reexport> method of all associated
431 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
432 otherwise returns false.
439 carp "warning: FS::cust_main::reexport is deprectated; ".
440 "use the depend_jobnum option to insert or order_pkgs to delay export";
442 local $SIG{HUP} = 'IGNORE';
443 local $SIG{INT} = 'IGNORE';
444 local $SIG{QUIT} = 'IGNORE';
445 local $SIG{TERM} = 'IGNORE';
446 local $SIG{TSTP} = 'IGNORE';
447 local $SIG{PIPE} = 'IGNORE';
449 my $oldAutoCommit = $FS::UID::AutoCommit;
450 local $FS::UID::AutoCommit = 0;
453 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
454 my $error = $cust_pkg->reexport;
456 $dbh->rollback if $oldAutoCommit;
461 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
466 =item delete NEW_CUSTNUM
468 This deletes the customer. If there is an error, returns the error, otherwise
471 This will completely remove all traces of the customer record. This is not
472 what you want when a customer cancels service; for that, cancel all of the
473 customer's packages (see L</cancel>).
475 If the customer has any uncancelled packages, you need to pass a new (valid)
476 customer number for those packages to be transferred to. Cancelled packages
477 will be deleted. Did I mention that this is NOT what you want when a customer
478 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
480 You can't delete a customer with invoices (see L<FS::cust_bill>),
481 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
482 refunds (see L<FS::cust_refund>).
489 local $SIG{HUP} = 'IGNORE';
490 local $SIG{INT} = 'IGNORE';
491 local $SIG{QUIT} = 'IGNORE';
492 local $SIG{TERM} = 'IGNORE';
493 local $SIG{TSTP} = 'IGNORE';
494 local $SIG{PIPE} = 'IGNORE';
496 my $oldAutoCommit = $FS::UID::AutoCommit;
497 local $FS::UID::AutoCommit = 0;
500 if ( $self->cust_bill ) {
501 $dbh->rollback if $oldAutoCommit;
502 return "Can't delete a customer with invoices";
504 if ( $self->cust_credit ) {
505 $dbh->rollback if $oldAutoCommit;
506 return "Can't delete a customer with credits";
508 if ( $self->cust_pay ) {
509 $dbh->rollback if $oldAutoCommit;
510 return "Can't delete a customer with payments";
512 if ( $self->cust_refund ) {
513 $dbh->rollback if $oldAutoCommit;
514 return "Can't delete a customer with refunds";
517 my @cust_pkg = $self->ncancelled_pkgs;
519 my $new_custnum = shift;
520 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
521 $dbh->rollback if $oldAutoCommit;
522 return "Invalid new customer number: $new_custnum";
524 foreach my $cust_pkg ( @cust_pkg ) {
525 my %hash = $cust_pkg->hash;
526 $hash{'custnum'} = $new_custnum;
527 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
528 my $error = $new_cust_pkg->replace($cust_pkg);
530 $dbh->rollback if $oldAutoCommit;
535 my @cancelled_cust_pkg = $self->all_pkgs;
536 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
537 my $error = $cust_pkg->delete;
539 $dbh->rollback if $oldAutoCommit;
544 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
545 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
547 my $error = $cust_main_invoice->delete;
549 $dbh->rollback if $oldAutoCommit;
554 my $error = $self->SUPER::delete;
556 $dbh->rollback if $oldAutoCommit;
560 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
565 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
567 Replaces the OLD_RECORD with this one in the database. If there is an error,
568 returns the error, otherwise returns false.
570 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
571 be set as the invoicing list (see L<"invoicing_list">). Errors return as
572 expected and rollback the entire transaction; it is not necessary to call
573 check_invoicing_list first. Here's an example:
575 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
584 local $SIG{HUP} = 'IGNORE';
585 local $SIG{INT} = 'IGNORE';
586 local $SIG{QUIT} = 'IGNORE';
587 local $SIG{TERM} = 'IGNORE';
588 local $SIG{TSTP} = 'IGNORE';
589 local $SIG{PIPE} = 'IGNORE';
591 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
592 && $conf->config('users-allow_comp') ) {
593 return "You are not permitted to create complimentary accounts."
594 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
597 my $oldAutoCommit = $FS::UID::AutoCommit;
598 local $FS::UID::AutoCommit = 0;
601 my $error = $self->SUPER::replace($old);
604 $dbh->rollback if $oldAutoCommit;
608 if ( @param ) { # INVOICING_LIST_ARYREF
609 my $invoicing_list = shift @param;
610 $error = $self->check_invoicing_list( $invoicing_list );
612 $dbh->rollback if $oldAutoCommit;
615 $self->invoicing_list( $invoicing_list );
618 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
619 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
620 # card/check/lec info has changed, want to retry realtime_ invoice events
621 my $error = $self->retry_realtime;
623 $dbh->rollback if $oldAutoCommit;
628 $error = $self->queue_fuzzyfiles_update;
630 $dbh->rollback if $oldAutoCommit;
631 return "updating fuzzy search cache: $error";
634 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
639 =item queue_fuzzyfiles_update
641 Used by insert & replace to update the fuzzy search cache
645 sub queue_fuzzyfiles_update {
648 local $SIG{HUP} = 'IGNORE';
649 local $SIG{INT} = 'IGNORE';
650 local $SIG{QUIT} = 'IGNORE';
651 local $SIG{TERM} = 'IGNORE';
652 local $SIG{TSTP} = 'IGNORE';
653 local $SIG{PIPE} = 'IGNORE';
655 my $oldAutoCommit = $FS::UID::AutoCommit;
656 local $FS::UID::AutoCommit = 0;
659 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
660 my $error = $queue->insert($self->getfield('last'), $self->company);
662 $dbh->rollback if $oldAutoCommit;
663 return "queueing job (transaction rolled back): $error";
666 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
667 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
668 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
670 $dbh->rollback if $oldAutoCommit;
671 return "queueing job (transaction rolled back): $error";
675 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
682 Checks all fields to make sure this is a valid customer record. If there is
683 an error, returns the error, otherwise returns false. Called by the insert
691 #warn "BEFORE: \n". $self->_dump;
694 $self->ut_numbern('custnum')
695 || $self->ut_number('agentnum')
696 || $self->ut_number('refnum')
697 || $self->ut_name('last')
698 || $self->ut_name('first')
699 || $self->ut_textn('company')
700 || $self->ut_text('address1')
701 || $self->ut_textn('address2')
702 || $self->ut_text('city')
703 || $self->ut_textn('county')
704 || $self->ut_textn('state')
705 || $self->ut_country('country')
706 || $self->ut_anything('comments')
707 || $self->ut_numbern('referral_custnum')
709 #barf. need message catalogs. i18n. etc.
710 $error .= "Please select an advertising source."
711 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
712 return $error if $error;
714 return "Unknown agent"
715 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
717 return "Unknown refnum"
718 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
720 return "Unknown referring custnum ". $self->referral_custnum
721 unless ! $self->referral_custnum
722 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
724 if ( $self->ss eq '' ) {
729 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
730 or return "Illegal social security number: ". $self->ss;
731 $self->ss("$1-$2-$3");
735 # bad idea to disable, causes billing to fail because of no tax rates later
736 # unless ( $import ) {
737 unless ( qsearch('cust_main_county', {
738 'country' => $self->country,
741 return "Unknown state/county/country: ".
742 $self->state. "/". $self->county. "/". $self->country
743 unless qsearch('cust_main_county',{
744 'state' => $self->state,
745 'county' => $self->county,
746 'country' => $self->country,
752 $self->ut_phonen('daytime', $self->country)
753 || $self->ut_phonen('night', $self->country)
754 || $self->ut_phonen('fax', $self->country)
755 || $self->ut_zip('zip', $self->country)
757 return $error if $error;
760 last first company address1 address2 city county state zip
761 country daytime night fax
764 if ( defined $self->dbdef_table->column('ship_last') ) {
765 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
767 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
771 $self->ut_name('ship_last')
772 || $self->ut_name('ship_first')
773 || $self->ut_textn('ship_company')
774 || $self->ut_text('ship_address1')
775 || $self->ut_textn('ship_address2')
776 || $self->ut_text('ship_city')
777 || $self->ut_textn('ship_county')
778 || $self->ut_textn('ship_state')
779 || $self->ut_country('ship_country')
781 return $error if $error;
783 #false laziness with above
784 unless ( qsearchs('cust_main_county', {
785 'country' => $self->ship_country,
788 return "Unknown ship_state/ship_county/ship_country: ".
789 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
790 unless qsearchs('cust_main_county',{
791 'state' => $self->ship_state,
792 'county' => $self->ship_county,
793 'country' => $self->ship_country,
799 $self->ut_phonen('ship_daytime', $self->ship_country)
800 || $self->ut_phonen('ship_night', $self->ship_country)
801 || $self->ut_phonen('ship_fax', $self->ship_country)
802 || $self->ut_zip('ship_zip', $self->ship_country)
804 return $error if $error;
806 } else { # ship_ info eq billing info, so don't store dup info in database
807 $self->setfield("ship_$_", '')
808 foreach qw( last first company address1 address2 city county state zip
809 country daytime night fax );
813 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
814 or return "Illegal payby: ". $self->payby;
817 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
819 my $payinfo = $self->payinfo;
821 $payinfo =~ /^(\d{13,16})$/
822 or return gettext('invalid_card'); # . ": ". $self->payinfo;
824 $self->payinfo($payinfo);
826 or return gettext('invalid_card'); # . ": ". $self->payinfo;
827 return gettext('unknown_card_type')
828 if cardtype($self->payinfo) eq "Unknown";
829 if ( defined $self->dbdef_table->column('paycvv') ) {
830 if ( length($self->paycvv) ) {
831 if ( cardtype($self->payinfo) eq 'American Express card' ) {
832 $self->paycvv =~ /^(\d{4})$/
833 or return "CVV2 (CID) for American Express cards is four digits.";
836 $self->paycvv =~ /^(\d{3})$/
837 or return "CVV2 (CVC2/CID) is three digits.";
845 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
847 my $payinfo = $self->payinfo;
848 $payinfo =~ s/[^\d\@]//g;
849 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
851 $self->payinfo($payinfo);
852 $self->paycvv('') if $self->dbdef_table->column('paycvv');
854 } elsif ( $self->payby eq 'LECB' ) {
856 my $payinfo = $self->payinfo;
858 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
860 $self->payinfo($payinfo);
861 $self->paycvv('') if $self->dbdef_table->column('paycvv');
863 } elsif ( $self->payby eq 'BILL' ) {
865 $error = $self->ut_textn('payinfo');
866 return "Illegal P.O. number: ". $self->payinfo if $error;
867 $self->paycvv('') if $self->dbdef_table->column('paycvv');
869 } elsif ( $self->payby eq 'COMP' ) {
871 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
872 return "You are not permitted to create complimentary accounts."
873 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
876 $error = $self->ut_textn('payinfo');
877 return "Illegal comp account issuer: ". $self->payinfo if $error;
878 $self->paycvv('') if $self->dbdef_table->column('paycvv');
880 } elsif ( $self->payby eq 'PREPAY' ) {
882 my $payinfo = $self->payinfo;
883 $payinfo =~ s/\W//g; #anything else would just confuse things
884 $self->payinfo($payinfo);
885 $error = $self->ut_alpha('payinfo');
886 return "Illegal prepayment identifier: ". $self->payinfo if $error;
887 return "Unknown prepayment identifier"
888 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
889 $self->paycvv('') if $self->dbdef_table->column('paycvv');
893 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
894 return "Expriation date required"
895 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
899 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
900 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
901 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
902 ( $m, $y ) = ( $3, "20$2" );
904 return "Illegal expiration date: ". $self->paydate;
906 $self->paydate("$y-$m-01");
907 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
908 return gettext('expired_card')
909 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
912 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
913 ( ! $conf->exists('require_cardname')
914 || $self->payby !~ /^(CARD|DCRD)$/ )
916 $self->payname( $self->first. " ". $self->getfield('last') );
918 $self->payname =~ /^([\w \,\.\-\']+)$/
919 or return gettext('illegal_name'). " payname: ". $self->payname;
923 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
926 $self->otaker(getotaker) unless $self->otaker;
928 #warn "AFTER: \n". $self->_dump;
935 Returns all packages (see L<FS::cust_pkg>) for this customer.
941 if ( $self->{'_pkgnum'} ) {
942 values %{ $self->{'_pkgnum'}->cache };
944 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
948 =item ncancelled_pkgs
950 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
954 sub ncancelled_pkgs {
956 if ( $self->{'_pkgnum'} ) {
957 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
959 @{ [ # force list context
960 qsearch( 'cust_pkg', {
961 'custnum' => $self->custnum,
964 qsearch( 'cust_pkg', {
965 'custnum' => $self->custnum,
974 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
980 grep { $_->susp } $self->ncancelled_pkgs;
983 =item unflagged_suspended_pkgs
985 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
986 customer (thouse packages without the `manual_flag' set).
990 sub unflagged_suspended_pkgs {
992 return $self->suspended_pkgs
993 unless dbdef->table('cust_pkg')->column('manual_flag');
994 grep { ! $_->manual_flag } $self->suspended_pkgs;
997 =item unsuspended_pkgs
999 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1004 sub unsuspended_pkgs {
1006 grep { ! $_->susp } $self->ncancelled_pkgs;
1011 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1012 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
1013 on success or a list of errors.
1019 grep { $_->unsuspend } $self->suspended_pkgs;
1024 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1025 Always returns a list: an empty list on success or a list of errors.
1031 grep { $_->suspend } $self->unsuspended_pkgs;
1034 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1036 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1037 PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list on
1038 success or a list of errors.
1042 sub suspend_if_pkgpart {
1045 grep { $_->suspend }
1046 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1047 $self->unsuspended_pkgs;
1050 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1052 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1053 listed PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list
1054 on success or a list of errors.
1058 sub suspend_unless_pkgpart {
1061 grep { $_->suspend }
1062 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1063 $self->unsuspended_pkgs;
1066 =item cancel [ OPTION => VALUE ... ]
1068 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1070 Available options are: I<quiet>
1072 I<quiet> can be set true to supress email cancellation notices.
1074 Always returns a list: an empty list on success or a list of errors.
1080 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1085 Returns the agent (see L<FS::agent>) for this customer.
1091 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1096 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1097 conjunction with the collect method.
1099 Options are passed as name-value pairs.
1101 Currently available options are:
1103 resetup - if set true, re-charges setup fees.
1105 time - bills the customer as if it were that time. Specified as a UNIX
1106 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1107 L<Date::Parse> for conversion functions. For example:
1111 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1114 If there is an error, returns the error, otherwise returns false.
1119 my( $self, %options ) = @_;
1120 warn "bill customer ". $self->custnum if $DEBUG;
1122 my $time = $options{'time'} || time;
1127 local $SIG{HUP} = 'IGNORE';
1128 local $SIG{INT} = 'IGNORE';
1129 local $SIG{QUIT} = 'IGNORE';
1130 local $SIG{TERM} = 'IGNORE';
1131 local $SIG{TSTP} = 'IGNORE';
1132 local $SIG{PIPE} = 'IGNORE';
1134 my $oldAutoCommit = $FS::UID::AutoCommit;
1135 local $FS::UID::AutoCommit = 0;
1138 $self->select_for_update; #mutex
1140 # find the packages which are due for billing, find out how much they are
1141 # & generate invoice database.
1143 my( $total_setup, $total_recur ) = ( 0, 0 );
1144 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1145 my @cust_bill_pkg = ();
1147 #my $taxable_charged = 0;##
1152 foreach my $cust_pkg (
1153 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1156 #NO!! next if $cust_pkg->cancel;
1157 next if $cust_pkg->getfield('cancel');
1159 warn " bill package ". $cust_pkg->pkgnum if $DEBUG;
1161 #? to avoid use of uninitialized value errors... ?
1162 $cust_pkg->setfield('bill', '')
1163 unless defined($cust_pkg->bill);
1165 my $part_pkg = $cust_pkg->part_pkg;
1167 my %hash = $cust_pkg->hash;
1168 my $old_cust_pkg = new FS::cust_pkg \%hash;
1174 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1176 warn " bill setup" if $DEBUG;
1178 $setup = eval { $cust_pkg->calc_setup( $time ) };
1180 $dbh->rollback if $oldAutoCommit;
1184 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1190 if ( $part_pkg->getfield('freq') ne '0' &&
1191 ! $cust_pkg->getfield('susp') &&
1192 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1195 warn " bill recur" if $DEBUG;
1197 # XXX shared with $recur_prog
1198 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1200 $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details ) };
1202 $dbh->rollback if $oldAutoCommit;
1206 #change this bit to use Date::Manip? CAREFUL with timezones (see
1207 # mailing list archive)
1208 my ($sec,$min,$hour,$mday,$mon,$year) =
1209 (localtime($sdate) )[0,1,2,3,4,5];
1211 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1212 # only for figuring next bill date, nothing else, so, reset $sdate again
1214 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1215 $cust_pkg->last_bill($sdate)
1216 if $cust_pkg->dbdef_table->column('last_bill');
1218 if ( $part_pkg->freq =~ /^\d+$/ ) {
1219 $mon += $part_pkg->freq;
1220 until ( $mon < 12 ) { $mon -= 12; $year++; }
1221 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1223 $mday += $weeks * 7;
1224 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1228 $dbh->rollback if $oldAutoCommit;
1229 return "unparsable frequency: ". $part_pkg->freq;
1231 $cust_pkg->setfield('bill',
1232 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1235 warn "\$setup is undefined" unless defined($setup);
1236 warn "\$recur is undefined" unless defined($recur);
1237 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1239 if ( $cust_pkg->modified ) {
1241 warn " package ". $cust_pkg->pkgnum. " modified; updating\n" if $DEBUG;
1243 $error=$cust_pkg->replace($old_cust_pkg);
1244 if ( $error ) { #just in case
1245 $dbh->rollback if $oldAutoCommit;
1246 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1249 $setup = sprintf( "%.2f", $setup );
1250 $recur = sprintf( "%.2f", $recur );
1251 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1252 $dbh->rollback if $oldAutoCommit;
1253 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1255 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1256 $dbh->rollback if $oldAutoCommit;
1257 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1259 if ( $setup != 0 || $recur != 0 ) {
1260 warn " charges (setup=$setup, recur=$recur); queueing line items\n"
1262 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1263 'pkgnum' => $cust_pkg->pkgnum,
1267 'edate' => $cust_pkg->bill,
1268 'details' => \@details,
1270 push @cust_bill_pkg, $cust_bill_pkg;
1271 $total_setup += $setup;
1272 $total_recur += $recur;
1274 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1276 my @taxes = qsearch( 'cust_main_county', {
1277 'state' => $self->state,
1278 'county' => $self->county,
1279 'country' => $self->country,
1280 'taxclass' => $part_pkg->taxclass,
1283 @taxes = qsearch( 'cust_main_county', {
1284 'state' => $self->state,
1285 'county' => $self->county,
1286 'country' => $self->country,
1291 #one more try at a whole-country tax rate
1293 @taxes = qsearch( 'cust_main_county', {
1296 'country' => $self->country,
1301 # maybe eliminate this entirely, along with all the 0% records
1303 $dbh->rollback if $oldAutoCommit;
1305 "fatal: can't find tax rate for state/county/country/taxclass ".
1306 join('/', ( map $self->$_(), qw(state county country) ),
1307 $part_pkg->taxclass ). "\n";
1310 foreach my $tax ( @taxes ) {
1312 my $taxable_charged = 0;
1313 $taxable_charged += $setup
1314 unless $part_pkg->setuptax =~ /^Y$/i
1315 || $tax->setuptax =~ /^Y$/i;
1316 $taxable_charged += $recur
1317 unless $part_pkg->recurtax =~ /^Y$/i
1318 || $tax->recurtax =~ /^Y$/i;
1319 next unless $taxable_charged;
1321 if ( $tax->exempt_amount > 0 ) {
1322 my ($mon,$year) = (localtime($sdate) )[4,5];
1324 my $freq = $part_pkg->freq || 1;
1325 if ( $freq !~ /(\d+)$/ ) {
1326 $dbh->rollback if $oldAutoCommit;
1327 return "daily/weekly package definitions not (yet?)".
1328 " compatible with monthly tax exemptions";
1330 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1331 foreach my $which_month ( 1 .. $freq ) {
1333 'custnum' => $self->custnum,
1334 'taxnum' => $tax->taxnum,
1335 'year' => 1900+$year,
1338 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1339 until ( $mon < 13 ) { $mon -= 12; $year++; }
1340 my $cust_tax_exempt =
1341 qsearchs('cust_tax_exempt', \%hash)
1342 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1343 my $remaining_exemption = sprintf("%.2f",
1344 $tax->exempt_amount - $cust_tax_exempt->amount );
1345 if ( $remaining_exemption > 0 ) {
1346 my $addl = $remaining_exemption > $taxable_per_month
1347 ? $taxable_per_month
1348 : $remaining_exemption;
1349 $taxable_charged -= $addl;
1350 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1351 $cust_tax_exempt->hash,
1353 sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1355 $error = $new_cust_tax_exempt->exemptnum
1356 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1357 : $new_cust_tax_exempt->insert;
1359 $dbh->rollback if $oldAutoCommit;
1360 return "fatal: can't update cust_tax_exempt: $error";
1363 } # if $remaining_exemption > 0
1365 } #foreach $which_month
1367 } #if $tax->exempt_amount
1369 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1371 #$tax += $taxable_charged * $cust_main_county->tax / 100
1372 $tax{ $tax->taxname || 'Tax' } +=
1373 $taxable_charged * $tax->tax / 100
1375 } #foreach my $tax ( @taxes )
1377 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1379 } #if $setup != 0 || $recur != 0
1381 } #if $cust_pkg->modified
1383 } #foreach my $cust_pkg
1385 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1386 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1388 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1389 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1393 # unless ( $self->tax =~ /Y/i
1394 # || $self->payby eq 'COMP'
1395 # || $taxable_charged == 0 ) {
1396 # my $cust_main_county = qsearchs('cust_main_county',{
1397 # 'state' => $self->state,
1398 # 'county' => $self->county,
1399 # 'country' => $self->country,
1400 # } ) or die "fatal: can't find tax rate for state/county/country ".
1401 # $self->state. "/". $self->county. "/". $self->country. "\n";
1402 # my $tax = sprintf( "%.2f",
1403 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1406 if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1408 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1409 my $tax = sprintf("%.2f", $tax{$taxname} );
1410 $charged = sprintf( "%.2f", $charged+$tax );
1412 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1418 'itemdesc' => $taxname,
1420 push @cust_bill_pkg, $cust_bill_pkg;
1423 } else { #1.4 schema
1426 foreach ( values %tax ) { $tax += $_ };
1427 $tax = sprintf("%.2f", $tax);
1429 $charged = sprintf( "%.2f", $charged+$tax );
1431 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1438 push @cust_bill_pkg, $cust_bill_pkg;
1443 my $cust_bill = new FS::cust_bill ( {
1444 'custnum' => $self->custnum,
1446 'charged' => $charged,
1448 $error = $cust_bill->insert;
1450 $dbh->rollback if $oldAutoCommit;
1451 return "can't create invoice for customer #". $self->custnum. ": $error";
1454 my $invnum = $cust_bill->invnum;
1456 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1458 $cust_bill_pkg->invnum($invnum);
1459 $error = $cust_bill_pkg->insert;
1461 $dbh->rollback if $oldAutoCommit;
1462 return "can't create invoice line item for customer #". $self->custnum.
1467 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1471 =item collect OPTIONS
1473 (Attempt to) collect money for this customer's outstanding invoices (see
1474 L<FS::cust_bill>). Usually used after the bill method.
1476 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1477 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1478 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1480 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1481 and the invoice events web interface.
1483 If there is an error, returns the error, otherwise returns false.
1485 Options are passed as name-value pairs.
1487 Currently available options are:
1489 invoice_time - Use this time when deciding when to print invoices and
1490 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>
1491 for conversion functions.
1493 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1496 retry_card - Deprecated alias for 'retry'
1498 batch_card - This option is deprecated. See the invoice events web interface
1499 to control whether cards are batched or run against a realtime gateway.
1501 report_badcard - This option is deprecated.
1503 force_print - This option is deprecated; see the invoice events web interface.
1505 quiet - set true to surpress email card/ACH decline notices.
1510 my( $self, %options ) = @_;
1511 my $invoice_time = $options{'invoice_time'} || time;
1514 local $SIG{HUP} = 'IGNORE';
1515 local $SIG{INT} = 'IGNORE';
1516 local $SIG{QUIT} = 'IGNORE';
1517 local $SIG{TERM} = 'IGNORE';
1518 local $SIG{TSTP} = 'IGNORE';
1519 local $SIG{PIPE} = 'IGNORE';
1521 my $oldAutoCommit = $FS::UID::AutoCommit;
1522 local $FS::UID::AutoCommit = 0;
1525 $self->select_for_update; #mutex
1527 my $balance = $self->balance;
1528 warn "collect customer ". $self->custnum. ": balance $balance" if $DEBUG;
1529 unless ( $balance > 0 ) { #redundant?????
1530 $dbh->rollback if $oldAutoCommit; #hmm
1534 if ( exists($options{'retry_card'}) ) {
1535 carp 'retry_card option passed to collect is deprecated; use retry';
1536 $options{'retry'} ||= $options{'retry_card'};
1538 if ( exists($options{'retry'}) && $options{'retry'} ) {
1539 my $error = $self->retry_realtime;
1541 $dbh->rollback if $oldAutoCommit;
1546 foreach my $cust_bill ( $self->open_cust_bill ) {
1548 # don't try to charge for the same invoice if it's already in a batch
1549 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1551 last if $self->balance <= 0;
1553 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
1556 foreach my $part_bill_event (
1557 sort { $a->seconds <=> $b->seconds
1558 || $a->weight <=> $b->weight
1559 || $a->eventpart <=> $b->eventpart }
1560 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1561 && ! qsearch( 'cust_bill_event', {
1562 'invnum' => $cust_bill->invnum,
1563 'eventpart' => $_->eventpart,
1567 qsearch('part_bill_event', { 'payby' => $self->payby,
1568 'disabled' => '', } )
1571 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
1572 || $self->balance <= 0; # or if balance<=0
1574 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1576 my $cust_main = $self; #for callback
1580 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1581 $error = eval $part_bill_event->eventcode;
1585 my $statustext = '';
1589 } elsif ( $error ) {
1591 $statustext = $error;
1596 #add cust_bill_event
1597 my $cust_bill_event = new FS::cust_bill_event {
1598 'invnum' => $cust_bill->invnum,
1599 'eventpart' => $part_bill_event->eventpart,
1600 #'_date' => $invoice_time,
1602 'status' => $status,
1603 'statustext' => $statustext,
1605 $error = $cust_bill_event->insert;
1607 #$dbh->rollback if $oldAutoCommit;
1608 #return "error: $error";
1610 # gah, even with transactions.
1611 $dbh->commit if $oldAutoCommit; #well.
1612 my $e = 'WARNING: Event run but database not updated - '.
1613 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1614 ', eventpart '. $part_bill_event->eventpart.
1625 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1630 =item retry_realtime
1632 Schedules realtime credit card / electronic check / LEC billing events for
1633 for retry. Useful if card information has changed or manual retry is desired.
1634 The 'collect' method must be called to actually retry the transaction.
1636 Implementation details: For each of this customer's open invoices, changes
1637 the status of the first "done" (with statustext error) realtime processing
1642 sub retry_realtime {
1645 local $SIG{HUP} = 'IGNORE';
1646 local $SIG{INT} = 'IGNORE';
1647 local $SIG{QUIT} = 'IGNORE';
1648 local $SIG{TERM} = 'IGNORE';
1649 local $SIG{TSTP} = 'IGNORE';
1650 local $SIG{PIPE} = 'IGNORE';
1652 my $oldAutoCommit = $FS::UID::AutoCommit;
1653 local $FS::UID::AutoCommit = 0;
1656 foreach my $cust_bill (
1657 grep { $_->cust_bill_event }
1658 $self->open_cust_bill
1660 my @cust_bill_event =
1661 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1663 #$_->part_bill_event->plan eq 'realtime-card'
1664 $_->part_bill_event->eventcode =~
1665 /\$cust_bill\->realtime_(card|ach|lec)/
1666 && $_->status eq 'done'
1669 $cust_bill->cust_bill_event;
1670 next unless @cust_bill_event;
1671 my $error = $cust_bill_event[0]->retry;
1673 $dbh->rollback if $oldAutoCommit;
1674 return "error scheduling invoice event for retry: $error";
1679 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1684 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1686 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1687 via a Business::OnlinePayment realtime gateway. See
1688 L<http://420.am/business-onlinepayment> for supported gateways.
1690 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1692 Available options are: I<description>, I<invnum>, I<quiet>
1694 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1695 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1696 if set, will override the value from the customer record.
1698 I<description> is a free-text field passed to the gateway. It defaults to
1699 "Internet services".
1701 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1702 specified invoice. If you don't specify an I<invnum> you might want to
1703 call the B<apply_payments> method.
1705 I<quiet> can be set true to surpress email decline notices.
1707 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1712 my( $self, $method, $amount, %options ) = @_;
1714 warn "$self $method $amount\n";
1715 warn " $_ => $options{$_}\n" foreach keys %options;
1718 $options{'description'} ||= 'Internet services';
1721 die "Real-time processing not enabled\n"
1722 unless $conf->exists('business-onlinepayment');
1723 eval "use Business::OnlinePayment";
1727 my $bop_config = 'business-onlinepayment';
1728 $bop_config .= '-ach'
1729 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1730 my ( $processor, $login, $password, $action, @bop_options ) =
1731 $conf->config($bop_config);
1732 $action ||= 'normal authorization';
1733 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1734 die "No real-time processor is enabled - ".
1735 "did you set the business-onlinepayment configuration value?\n"
1740 my $address = exists($options{'address1'})
1741 ? $options{'address1'}
1743 my $address2 = exists($options{'address2'})
1744 ? $options{'address2'}
1746 $address .= ", ". $address2 if length($address2);
1748 my $o_payname = exists($options{'payname'})
1749 ? $options{'payname'}
1751 my($payname, $payfirst, $paylast);
1752 if ( $o_payname && $method ne 'ECHECK' ) {
1753 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1754 or return "Illegal payname $payname";
1755 ($payfirst, $paylast) = ($1, $2);
1757 $payfirst = $self->getfield('first');
1758 $paylast = $self->getfield('last');
1759 $payname = "$payfirst $paylast";
1762 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1763 if ( $conf->exists('emailinvoiceauto')
1764 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1765 push @invoicing_list, $self->all_emails;
1767 my $email = $invoicing_list[0];
1769 my $payinfo = exists($options{'payinfo'})
1770 ? $options{'payinfo'}
1774 if ( $method eq 'CC' ) {
1776 $content{card_number} = $payinfo;
1777 my $paydate = exists($options{'paydate'})
1778 ? $options{'paydate'}
1780 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1781 $content{expiration} = "$2/$1";
1783 if ( defined $self->dbdef_table->column('paycvv') ) {
1784 my $paycvv = exists($options{'paycvv'})
1785 ? $options{'paycvv'}
1787 $content{cvv2} = $self->paycvv
1791 $content{recurring_billing} = 'YES'
1792 if qsearch('cust_pay', { 'custnum' => $self->custnum,
1794 'payinfo' => $payinfo,
1797 } elsif ( $method eq 'ECHECK' ) {
1798 ( $content{account_number}, $content{routing_code} ) =
1799 split('@', $payinfo);
1800 $content{bank_name} = $o_payname;
1801 $content{account_type} = 'CHECKING';
1802 $content{account_name} = $payname;
1803 $content{customer_org} = $self->company ? 'B' : 'I';
1804 $content{customer_ssn} = exists($options{'ss'})
1807 } elsif ( $method eq 'LEC' ) {
1808 $content{phone} = $payinfo;
1813 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1815 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
1816 $transaction->content(
1819 'password' => $password,
1820 'action' => $action1,
1821 'description' => $options{'description'},
1822 'amount' => $amount,
1823 'invoice_number' => $options{'invnum'},
1824 'customer_id' => $self->custnum,
1825 'last_name' => $paylast,
1826 'first_name' => $payfirst,
1828 'address' => $address,
1829 'city' => ( exists($options{'city'})
1832 'state' => ( exists($options{'state'})
1835 'zip' => ( exists($options{'zip'})
1838 'country' => ( exists($options{'country'})
1839 ? $options{'country'}
1841 'referer' => 'http://cleanwhisker.420.am/',
1843 'phone' => $self->daytime || $self->night,
1846 $transaction->submit();
1848 if ( $transaction->is_success() && $action2 ) {
1849 my $auth = $transaction->authorization;
1850 my $ordernum = $transaction->can('order_number')
1851 ? $transaction->order_number
1855 new Business::OnlinePayment( $processor, @bop_options );
1862 password => $password,
1863 order_number => $ordernum,
1865 authorization => $auth,
1866 description => $options{'description'},
1869 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
1870 transaction_sequence_num local_transaction_date
1871 local_transaction_time AVS_result_code )) {
1872 $capture{$field} = $transaction->$field() if $transaction->can($field);
1875 $capture->content( %capture );
1879 unless ( $capture->is_success ) {
1880 my $e = "Authorization sucessful but capture failed, custnum #".
1881 $self->custnum. ': '. $capture->result_code.
1882 ": ". $capture->error_message;
1889 #remove paycvv after initial transaction
1890 #false laziness w/misc/process/payment.cgi - check both to make sure working
1892 if ( defined $self->dbdef_table->column('paycvv')
1893 && length($self->paycvv)
1894 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
1896 my $error = $self->remove_cvv;
1898 warn "error removing cvv: $error\n";
1903 if ( $transaction->is_success() ) {
1905 my %method2payby = (
1911 my $paybatch = "$processor:". $transaction->authorization;
1912 $paybatch .= ':'. $transaction->order_number
1913 if $transaction->can('order_number')
1914 && length($transaction->order_number);
1916 my $cust_pay = new FS::cust_pay ( {
1917 'custnum' => $self->custnum,
1918 'invnum' => $options{'invnum'},
1921 'payby' => $method2payby{$method},
1922 'payinfo' => $payinfo,
1923 'paybatch' => $paybatch,
1925 my $error = $cust_pay->insert;
1927 $cust_pay->invnum(''); #try again with no specific invnum
1928 my $error2 = $cust_pay->insert;
1930 # gah, even with transactions.
1931 my $e = 'WARNING: Card/ACH debited but database not updated - '.
1932 "error inserting payment ($processor): $error2".
1933 " (previously tried insert with invnum #$options{'invnum'}" .
1939 return ''; #no error
1943 my $perror = "$processor error: ". $transaction->error_message;
1945 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1946 && $conf->exists('emaildecline')
1947 && grep { $_ ne 'POST' } $self->invoicing_list
1948 && ! grep { $transaction->error_message =~ /$_/ }
1949 $conf->config('emaildecline-exclude')
1951 my @templ = $conf->config('declinetemplate');
1952 my $template = new Text::Template (
1954 SOURCE => [ map "$_\n", @templ ],
1955 ) or return "($perror) can't create template: $Text::Template::ERROR";
1956 $template->compile()
1957 or return "($perror) can't compile template: $Text::Template::ERROR";
1959 my $templ_hash = { error => $transaction->error_message };
1961 my $error = send_email(
1962 'from' => $conf->config('invoice_from'),
1963 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1964 'subject' => 'Your payment could not be processed',
1965 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1968 $perror .= " (also received error sending decline notification: $error)"
1980 Removes the I<paycvv> field from the database directly.
1982 If there is an error, returns the error, otherwise returns false.
1988 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
1989 or return dbh->errstr;
1990 $sth->execute($self->custnum)
1991 or return $sth->errstr;
1996 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1998 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1999 via a Business::OnlinePayment realtime gateway. See
2000 L<http://420.am/business-onlinepayment> for supported gateways.
2002 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2004 Available options are: I<amount>, I<reason>, I<paynum>
2006 Most gateways require a reference to an original payment transaction to refund,
2007 so you probably need to specify a I<paynum>.
2009 I<amount> defaults to the original amount of the payment if not specified.
2011 I<reason> specifies a reason for the refund.
2013 Implementation note: If I<amount> is unspecified or equal to the amount of the
2014 orignal payment, first an attempt is made to "void" the transaction via
2015 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2016 the normal attempt is made to "refund" ("credit") the transaction via the
2017 gateway is attempted.
2019 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2020 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2021 #if set, will override the value from the customer record.
2023 #If an I<invnum> is specified, this payment (if sucessful) is applied to the
2024 #specified invoice. If you don't specify an I<invnum> you might want to
2025 #call the B<apply_payments> method.
2029 #some false laziness w/realtime_bop, not enough to make it worth merging
2030 #but some useful small subs should be pulled out
2031 sub realtime_refund_bop {
2032 my( $self, $method, %options ) = @_;
2034 warn "$self $method refund\n";
2035 warn " $_ => $options{$_}\n" foreach keys %options;
2039 die "Real-time processing not enabled\n"
2040 unless $conf->exists('business-onlinepayment');
2041 eval "use Business::OnlinePayment";
2045 my $bop_config = 'business-onlinepayment';
2046 $bop_config .= '-ach'
2047 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2048 my ( $processor, $login, $password, $unused_action, @bop_options ) =
2049 $conf->config($bop_config);
2050 #$action ||= 'normal authorization';
2051 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2052 die "No real-time processor is enabled - ".
2053 "did you set the business-onlinepayment configuration value?\n"
2057 my $amount = $options{'amount'};
2058 my( $pay_processor, $auth, $order_number ) = ( '', '', '' );
2059 if ( $options{'paynum'} ) {
2060 warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
2061 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2062 or return "Unknown paynum $options{'paynum'}";
2063 $amount ||= $cust_pay->paid;
2064 $cust_pay->paybatch =~ /^(\w+):(\w*)(:(\w+))?$/
2065 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2066 $cust_pay->paybatch;
2067 ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
2068 return "processor of payment $options{'paynum'} $pay_processor does not".
2069 " match current processor $processor"
2070 unless $pay_processor eq $processor;
2072 return "neither amount nor paynum specified" unless $amount;
2077 'password' => $password,
2078 'order_number' => $order_number,
2079 'amount' => $amount,
2080 'referer' => 'http://cleanwhisker.420.am/',
2082 $content{authorization} = $auth
2083 if length($auth); #echeck/ACH transactions have an order # but no auth
2084 #(at least with authorize.net)
2086 #first try void if applicable
2087 if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2088 my $void = new Business::OnlinePayment( $processor, @bop_options );
2089 $void->content( 'action' => 'void', %content );
2091 if ( $void->is_success ) {
2092 my $error = $cust_pay->void($options{'reason'});
2094 # gah, even with transactions.
2095 my $e = 'WARNING: Card/ACH voided but database not updated - '.
2096 "error voiding payment: $error";
2105 my $address = $self->address1;
2106 $address .= ", ". $self->address2 if $self->address2;
2108 my($payname, $payfirst, $paylast);
2109 if ( $self->payname && $method ne 'ECHECK' ) {
2110 $payname = $self->payname;
2111 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2112 or return "Illegal payname $payname";
2113 ($payfirst, $paylast) = ($1, $2);
2115 $payfirst = $self->getfield('first');
2116 $paylast = $self->getfield('last');
2117 $payname = "$payfirst $paylast";
2120 if ( $method eq 'CC' ) {
2122 $content{card_number} = $self->payinfo;
2123 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2124 $content{expiration} = "$2/$1";
2126 #$content{cvv2} = $self->paycvv
2127 # if defined $self->dbdef_table->column('paycvv')
2128 # && length($self->paycvv);
2130 #$content{recurring_billing} = 'YES'
2131 # if qsearch('cust_pay', { 'custnum' => $self->custnum,
2132 # 'payby' => 'CARD',
2133 # 'payinfo' => $self->payinfo, } );
2135 } elsif ( $method eq 'ECHECK' ) {
2136 ( $content{account_number}, $content{routing_code} ) =
2137 split('@', $self->payinfo);
2138 $content{bank_name} = $self->payname;
2139 $content{account_type} = 'CHECKING';
2140 $content{account_name} = $payname;
2141 $content{customer_org} = $self->company ? 'B' : 'I';
2142 $content{customer_ssn} = $self->ss;
2143 } elsif ( $method eq 'LEC' ) {
2144 $content{phone} = $self->payinfo;
2148 my $refund = new Business::OnlinePayment( $processor, @bop_options );
2150 'action' => 'credit',
2151 'customer_id' => $self->custnum,
2152 'last_name' => $paylast,
2153 'first_name' => $payfirst,
2155 'address' => $address,
2156 'city' => $self->city,
2157 'state' => $self->state,
2158 'zip' => $self->zip,
2159 'country' => $self->country,
2164 return "$processor error: ". $refund->error_message
2165 unless $refund->is_success();
2167 my %method2payby = (
2173 my $paybatch = "$processor:". $refund->authorization;
2174 $paybatch .= ':'. $refund->order_number
2175 if $refund->can('order_number') && $refund->order_number;
2177 while ( $cust_pay && $cust_pay->unappled < $amount ) {
2178 my @cust_bill_pay = $cust_pay->cust_bill_pay;
2179 last unless @cust_bill_pay;
2180 my $cust_bill_pay = pop @cust_bill_pay;
2181 my $error = $cust_bill_pay->delete;
2185 my $cust_refund = new FS::cust_refund ( {
2186 'custnum' => $self->custnum,
2187 'paynum' => $options{'paynum'},
2188 'refund' => $amount,
2190 'payby' => $method2payby{$method},
2191 'payinfo' => $self->payinfo,
2192 'paybatch' => $paybatch,
2193 'reason' => $options{'reason'} || 'card or ACH refund',
2195 my $error = $cust_refund->insert;
2197 $cust_refund->paynum(''); #try again with no specific paynum
2198 my $error2 = $cust_refund->insert;
2200 # gah, even with transactions.
2201 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
2202 "error inserting refund ($processor): $error2".
2203 " (previously tried insert with paynum #$options{'paynum'}" .
2216 Returns the total owed for this customer on all invoices
2217 (see L<FS::cust_bill/owed>).
2223 $self->total_owed_date(2145859200); #12/31/2037
2226 =item total_owed_date TIME
2228 Returns the total owed for this customer on all invoices with date earlier than
2229 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
2230 see L<Time::Local> and L<Date::Parse> for conversion functions.
2234 sub total_owed_date {
2238 foreach my $cust_bill (
2239 grep { $_->_date <= $time }
2240 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2242 $total_bill += $cust_bill->owed;
2244 sprintf( "%.2f", $total_bill );
2247 =item apply_credits OPTION => VALUE ...
2249 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2250 to outstanding invoice balances in chronological order (or reverse
2251 chronological order if the I<order> option is set to B<newest>) and returns the
2252 value of any remaining unapplied credits available for refund (see
2253 L<FS::cust_refund>).
2261 return 0 unless $self->total_credited;
2263 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2264 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2266 my @invoices = $self->open_cust_bill;
2267 @invoices = sort { $b->_date <=> $a->_date } @invoices
2268 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2271 foreach my $cust_bill ( @invoices ) {
2274 if ( !defined($credit) || $credit->credited == 0) {
2275 $credit = pop @credits or last;
2278 if ($cust_bill->owed >= $credit->credited) {
2279 $amount=$credit->credited;
2281 $amount=$cust_bill->owed;
2284 my $cust_credit_bill = new FS::cust_credit_bill ( {
2285 'crednum' => $credit->crednum,
2286 'invnum' => $cust_bill->invnum,
2287 'amount' => $amount,
2289 my $error = $cust_credit_bill->insert;
2290 die $error if $error;
2292 redo if ($cust_bill->owed > 0);
2296 return $self->total_credited;
2299 =item apply_payments
2301 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2302 to outstanding invoice balances in chronological order.
2304 #and returns the value of any remaining unapplied payments.
2308 sub apply_payments {
2313 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
2314 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
2316 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
2317 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
2321 foreach my $cust_bill ( @invoices ) {
2324 if ( !defined($payment) || $payment->unapplied == 0 ) {
2325 $payment = pop @payments or last;
2328 if ( $cust_bill->owed >= $payment->unapplied ) {
2329 $amount = $payment->unapplied;
2331 $amount = $cust_bill->owed;
2334 my $cust_bill_pay = new FS::cust_bill_pay ( {
2335 'paynum' => $payment->paynum,
2336 'invnum' => $cust_bill->invnum,
2337 'amount' => $amount,
2339 my $error = $cust_bill_pay->insert;
2340 die $error if $error;
2342 redo if ( $cust_bill->owed > 0);
2346 return $self->total_unapplied_payments;
2349 =item total_credited
2351 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2352 customer. See L<FS::cust_credit/credited>.
2356 sub total_credited {
2358 my $total_credit = 0;
2359 foreach my $cust_credit ( qsearch('cust_credit', {
2360 'custnum' => $self->custnum,
2362 $total_credit += $cust_credit->credited;
2364 sprintf( "%.2f", $total_credit );
2367 =item total_unapplied_payments
2369 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2370 See L<FS::cust_pay/unapplied>.
2374 sub total_unapplied_payments {
2376 my $total_unapplied = 0;
2377 foreach my $cust_pay ( qsearch('cust_pay', {
2378 'custnum' => $self->custnum,
2380 $total_unapplied += $cust_pay->unapplied;
2382 sprintf( "%.2f", $total_unapplied );
2387 Returns the balance for this customer (total_owed minus total_credited
2388 minus total_unapplied_payments).
2395 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
2399 =item balance_date TIME
2401 Returns the balance for this customer, only considering invoices with date
2402 earlier than TIME (total_owed_date minus total_credited minus
2403 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
2404 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
2413 $self->total_owed_date($time)
2414 - $self->total_credited
2415 - $self->total_unapplied_payments
2419 =item paydate_monthyear
2421 Returns a two-element list consisting of the month and year of this customer's
2422 paydate (credit card expiration date for CARD customers)
2426 sub paydate_monthyear {
2428 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
2430 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
2437 =item payinfo_masked
2439 Returns a "masked" payinfo field with all but the last four characters replaced
2440 by 'x'es. Useful for displaying credit cards.
2444 sub payinfo_masked {
2446 my $payinfo = $self->payinfo;
2447 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
2450 =item invoicing_list [ ARRAYREF ]
2452 If an arguement is given, sets these email addresses as invoice recipients
2453 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
2454 (except as warnings), so use check_invoicing_list first.
2456 Returns a list of email addresses (with svcnum entries expanded).
2458 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
2459 check it without disturbing anything by passing nothing.
2461 This interface may change in the future.
2465 sub invoicing_list {
2466 my( $self, $arrayref ) = @_;
2468 my @cust_main_invoice;
2469 if ( $self->custnum ) {
2470 @cust_main_invoice =
2471 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2473 @cust_main_invoice = ();
2475 foreach my $cust_main_invoice ( @cust_main_invoice ) {
2476 #warn $cust_main_invoice->destnum;
2477 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2478 #warn $cust_main_invoice->destnum;
2479 my $error = $cust_main_invoice->delete;
2480 warn $error if $error;
2483 if ( $self->custnum ) {
2484 @cust_main_invoice =
2485 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2487 @cust_main_invoice = ();
2489 my %seen = map { $_->address => 1 } @cust_main_invoice;
2490 foreach my $address ( @{$arrayref} ) {
2491 next if exists $seen{$address} && $seen{$address};
2492 $seen{$address} = 1;
2493 my $cust_main_invoice = new FS::cust_main_invoice ( {
2494 'custnum' => $self->custnum,
2497 my $error = $cust_main_invoice->insert;
2498 warn $error if $error;
2501 if ( $self->custnum ) {
2503 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2509 =item check_invoicing_list ARRAYREF
2511 Checks these arguements as valid input for the invoicing_list method. If there
2512 is an error, returns the error, otherwise returns false.
2516 sub check_invoicing_list {
2517 my( $self, $arrayref ) = @_;
2518 foreach my $address ( @{$arrayref} ) {
2519 my $cust_main_invoice = new FS::cust_main_invoice ( {
2520 'custnum' => $self->custnum,
2523 my $error = $self->custnum
2524 ? $cust_main_invoice->check
2525 : $cust_main_invoice->checkdest
2527 return $error if $error;
2532 =item set_default_invoicing_list
2534 Sets the invoicing list to all accounts associated with this customer,
2535 overwriting any previous invoicing list.
2539 sub set_default_invoicing_list {
2541 $self->invoicing_list($self->all_emails);
2546 Returns the email addresses of all accounts provisioned for this customer.
2553 foreach my $cust_pkg ( $self->all_pkgs ) {
2554 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2556 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2557 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2559 $list{$_}=1 foreach map { $_->email } @svc_acct;
2564 =item invoicing_list_addpost
2566 Adds postal invoicing to this customer. If this customer is already configured
2567 to receive postal invoices, does nothing.
2571 sub invoicing_list_addpost {
2573 return if grep { $_ eq 'POST' } $self->invoicing_list;
2574 my @invoicing_list = $self->invoicing_list;
2575 push @invoicing_list, 'POST';
2576 $self->invoicing_list(\@invoicing_list);
2579 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2581 Returns an array of customers referred by this customer (referral_custnum set
2582 to this custnum). If DEPTH is given, recurses up to the given depth, returning
2583 customers referred by customers referred by this customer and so on, inclusive.
2584 The default behavior is DEPTH 1 (no recursion).
2588 sub referral_cust_main {
2590 my $depth = @_ ? shift : 1;
2591 my $exclude = @_ ? shift : {};
2594 map { $exclude->{$_->custnum}++; $_; }
2595 grep { ! $exclude->{ $_->custnum } }
2596 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2600 map { $_->referral_cust_main($depth-1, $exclude) }
2607 =item referral_cust_main_ncancelled
2609 Same as referral_cust_main, except only returns customers with uncancelled
2614 sub referral_cust_main_ncancelled {
2616 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2619 =item referral_cust_pkg [ DEPTH ]
2621 Like referral_cust_main, except returns a flat list of all unsuspended (and
2622 uncancelled) packages for each customer. The number of items in this list may
2623 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2627 sub referral_cust_pkg {
2629 my $depth = @_ ? shift : 1;
2631 map { $_->unsuspended_pkgs }
2632 grep { $_->unsuspended_pkgs }
2633 $self->referral_cust_main($depth);
2636 =item credit AMOUNT, REASON
2638 Applies a credit to this customer. If there is an error, returns the error,
2639 otherwise returns false.
2644 my( $self, $amount, $reason ) = @_;
2645 my $cust_credit = new FS::cust_credit {
2646 'custnum' => $self->custnum,
2647 'amount' => $amount,
2648 'reason' => $reason,
2650 $cust_credit->insert;
2653 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2655 Creates a one-time charge for this customer. If there is an error, returns
2656 the error, otherwise returns false.
2661 my ( $self, $amount ) = ( shift, shift );
2662 my $pkg = @_ ? shift : 'One-time charge';
2663 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2664 my $taxclass = @_ ? shift : '';
2666 local $SIG{HUP} = 'IGNORE';
2667 local $SIG{INT} = 'IGNORE';
2668 local $SIG{QUIT} = 'IGNORE';
2669 local $SIG{TERM} = 'IGNORE';
2670 local $SIG{TSTP} = 'IGNORE';
2671 local $SIG{PIPE} = 'IGNORE';
2673 my $oldAutoCommit = $FS::UID::AutoCommit;
2674 local $FS::UID::AutoCommit = 0;
2677 my $part_pkg = new FS::part_pkg ( {
2679 'comment' => $comment,
2680 #'setup' => $amount,
2683 'plandata' => "setup_fee=$amount",
2686 'taxclass' => $taxclass,
2689 my $error = $part_pkg->insert;
2691 $dbh->rollback if $oldAutoCommit;
2695 my $pkgpart = $part_pkg->pkgpart;
2696 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2697 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2698 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2699 $error = $type_pkgs->insert;
2701 $dbh->rollback if $oldAutoCommit;
2706 my $cust_pkg = new FS::cust_pkg ( {
2707 'custnum' => $self->custnum,
2708 'pkgpart' => $pkgpart,
2711 $error = $cust_pkg->insert;
2713 $dbh->rollback if $oldAutoCommit;
2717 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2724 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2730 sort { $a->_date <=> $b->_date }
2731 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2734 =item open_cust_bill
2736 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2741 sub open_cust_bill {
2743 grep { $_->owed > 0 } $self->cust_bill;
2748 Returns all the credits (see L<FS::cust_credit>) for this customer.
2754 sort { $a->_date <=> $b->_date }
2755 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
2760 Returns all the payments (see L<FS::cust_pay>) for this customer.
2766 sort { $a->_date <=> $b->_date }
2767 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
2772 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
2778 sort { $a->_date <=> $b->_date }
2779 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
2785 Returns all the refunds (see L<FS::cust_refund>) for this customer.
2791 sort { $a->_date <=> $b->_date }
2792 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
2795 =item select_for_update
2797 Selects this record with the SQL "FOR UPDATE" command. This can be useful as
2802 sub select_for_update {
2804 qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
2809 Returns a name string for this customer, either "Company (Last, First)" or
2816 my $name = $self->get('last'). ', '. $self->first;
2817 $name = $self->company. " ($name)" if $self->company;
2823 Returns a status string for this customer, currently:
2827 =item prospect - No packages have ever been ordered
2829 =item active - One or more recurring packages is active
2831 =item suspended - All non-cancelled recurring packages are suspended
2833 =item cancelled - All recurring packages are cancelled
2841 for my $status (qw( prospect active suspended cancelled )) {
2842 my $method = $status.'_sql';
2843 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
2844 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
2845 $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
2846 return $status if $sth->fetchrow_arrayref->[0];
2852 Returns a hex triplet color string for this customer's status.
2857 'prospect' => '000000',
2858 'active' => '00CC00',
2859 'suspended' => 'FF9900',
2860 'cancelled' => 'FF0000',
2864 $statuscolor{$self->status};
2869 =head1 CLASS METHODS
2875 Returns an SQL expression identifying prospective cust_main records (customers
2876 with no packages ever ordered)
2880 sub prospect_sql { "
2881 0 = ( SELECT COUNT(*) FROM cust_pkg
2882 WHERE cust_pkg.custnum = cust_main.custnum
2888 Returns an SQL expression identifying active cust_main records.
2893 0 < ( SELECT COUNT(*) FROM cust_pkg
2894 WHERE cust_pkg.custnum = cust_main.custnum
2895 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2896 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2903 Returns an SQL expression identifying suspended cust_main records.
2907 sub suspended_sql { susp_sql(@_); }
2909 0 < ( SELECT COUNT(*) FROM cust_pkg
2910 WHERE cust_pkg.custnum = cust_main.custnum
2911 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2913 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2914 WHERE cust_pkg.custnum = cust_main.custnum
2915 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2922 Returns an SQL expression identifying cancelled cust_main records.
2926 sub cancelled_sql { cancel_sql(@_); }
2928 0 < ( SELECT COUNT(*) FROM cust_pkg
2929 WHERE cust_pkg.custnum = cust_main.custnum
2931 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2932 WHERE cust_pkg.custnum = cust_main.custnum
2933 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2937 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
2939 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
2940 records. Currently, only I<last> or I<company> may be specified (the
2941 appropriate ship_ field is also searched if applicable).
2943 Additional options are the same as FS::Record::qsearch
2948 my( $self, $fuzzy, $hash, @opt) = @_;
2953 check_and_rebuild_fuzzyfiles();
2954 foreach my $field ( keys %$fuzzy ) {
2955 my $sub = \&{"all_$field"};
2957 $match{$_}=1 foreach ( amatch($fuzzy->{$field}, ['i'], @{ &$sub() } ) );
2959 foreach ( keys %match ) {
2960 push @cust_main, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
2961 push @cust_main, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt)
2962 if defined dbdef->table('cust_main')->column('ship_last');
2967 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
2979 =item check_and_rebuild_fuzzyfiles
2983 sub check_and_rebuild_fuzzyfiles {
2984 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2985 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
2986 or &rebuild_fuzzyfiles;
2989 =item rebuild_fuzzyfiles
2993 sub rebuild_fuzzyfiles {
2995 use Fcntl qw(:flock);
2997 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3001 open(LASTLOCK,">>$dir/cust_main.last")
3002 or die "can't open $dir/cust_main.last: $!";
3003 flock(LASTLOCK,LOCK_EX)
3004 or die "can't lock $dir/cust_main.last: $!";
3006 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
3008 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
3009 if defined dbdef->table('cust_main')->column('ship_last');
3011 open (LASTCACHE,">$dir/cust_main.last.tmp")
3012 or die "can't open $dir/cust_main.last.tmp: $!";
3013 print LASTCACHE join("\n", @all_last), "\n";
3014 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
3016 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
3021 open(COMPANYLOCK,">>$dir/cust_main.company")
3022 or die "can't open $dir/cust_main.company: $!";
3023 flock(COMPANYLOCK,LOCK_EX)
3024 or die "can't lock $dir/cust_main.company: $!";
3026 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
3028 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
3029 if defined dbdef->table('cust_main')->column('ship_last');
3031 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
3032 or die "can't open $dir/cust_main.company.tmp: $!";
3033 print COMPANYCACHE join("\n", @all_company), "\n";
3034 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
3036 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
3046 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3047 open(LASTCACHE,"<$dir/cust_main.last")
3048 or die "can't open $dir/cust_main.last: $!";
3049 my @array = map { chomp; $_; } <LASTCACHE>;
3059 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3060 open(COMPANYCACHE,"<$dir/cust_main.company")
3061 or die "can't open $dir/cust_main.last: $!";
3062 my @array = map { chomp; $_; } <COMPANYCACHE>;
3067 =item append_fuzzyfiles LASTNAME COMPANY
3071 sub append_fuzzyfiles {
3072 my( $last, $company ) = @_;
3074 &check_and_rebuild_fuzzyfiles;
3076 use Fcntl qw(:flock);
3078 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3082 open(LAST,">>$dir/cust_main.last")
3083 or die "can't open $dir/cust_main.last: $!";
3085 or die "can't lock $dir/cust_main.last: $!";
3087 print LAST "$last\n";
3090 or die "can't unlock $dir/cust_main.last: $!";
3096 open(COMPANY,">>$dir/cust_main.company")
3097 or die "can't open $dir/cust_main.company: $!";
3098 flock(COMPANY,LOCK_EX)
3099 or die "can't lock $dir/cust_main.company: $!";
3101 print COMPANY "$company\n";
3103 flock(COMPANY,LOCK_UN)
3104 or die "can't unlock $dir/cust_main.company: $!";
3118 #warn join('-',keys %$param);
3119 my $fh = $param->{filehandle};
3120 my $agentnum = $param->{agentnum};
3121 my $refnum = $param->{refnum};
3122 my $pkgpart = $param->{pkgpart};
3123 my @fields = @{$param->{fields}};
3125 eval "use Date::Parse;";
3127 eval "use Text::CSV_XS;";
3130 my $csv = new Text::CSV_XS;
3137 local $SIG{HUP} = 'IGNORE';
3138 local $SIG{INT} = 'IGNORE';
3139 local $SIG{QUIT} = 'IGNORE';
3140 local $SIG{TERM} = 'IGNORE';
3141 local $SIG{TSTP} = 'IGNORE';
3142 local $SIG{PIPE} = 'IGNORE';
3144 my $oldAutoCommit = $FS::UID::AutoCommit;
3145 local $FS::UID::AutoCommit = 0;
3148 #while ( $columns = $csv->getline($fh) ) {
3150 while ( defined($line=<$fh>) ) {
3152 $csv->parse($line) or do {
3153 $dbh->rollback if $oldAutoCommit;
3154 return "can't parse: ". $csv->error_input();
3157 my @columns = $csv->fields();
3158 #warn join('-',@columns);
3161 agentnum => $agentnum,
3163 country => $conf->config('countrydefault') || 'US',
3164 payby => 'BILL', #default
3165 paydate => '12/2037', #default
3167 my $billtime = time;
3168 my %cust_pkg = ( pkgpart => $pkgpart );
3169 foreach my $field ( @fields ) {
3170 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
3171 #$cust_pkg{$1} = str2time( shift @$columns );
3172 if ( $1 eq 'setup' ) {
3173 $billtime = str2time(shift @columns);
3175 $cust_pkg{$1} = str2time( shift @columns );
3178 #$cust_main{$field} = shift @$columns;
3179 $cust_main{$field} = shift @columns;
3183 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
3184 my $cust_main = new FS::cust_main ( \%cust_main );
3186 tie my %hash, 'Tie::RefHash'; #this part is important
3187 $hash{$cust_pkg} = [] if $pkgpart;
3188 my $error = $cust_main->insert( \%hash );
3191 $dbh->rollback if $oldAutoCommit;
3192 return "can't insert customer for $line: $error";
3195 #false laziness w/bill.cgi
3196 $error = $cust_main->bill( 'time' => $billtime );
3198 $dbh->rollback if $oldAutoCommit;
3199 return "can't bill customer for $line: $error";
3202 $cust_main->apply_payments;
3203 $cust_main->apply_credits;
3205 $error = $cust_main->collect();
3207 $dbh->rollback if $oldAutoCommit;
3208 return "can't collect customer for $line: $error";
3214 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3216 return "Empty file!" unless $imported;
3228 #warn join('-',keys %$param);
3229 my $fh = $param->{filehandle};
3230 my @fields = @{$param->{fields}};
3232 eval "use Date::Parse;";
3234 eval "use Text::CSV_XS;";
3237 my $csv = new Text::CSV_XS;
3244 local $SIG{HUP} = 'IGNORE';
3245 local $SIG{INT} = 'IGNORE';
3246 local $SIG{QUIT} = 'IGNORE';
3247 local $SIG{TERM} = 'IGNORE';
3248 local $SIG{TSTP} = 'IGNORE';
3249 local $SIG{PIPE} = 'IGNORE';
3251 my $oldAutoCommit = $FS::UID::AutoCommit;
3252 local $FS::UID::AutoCommit = 0;
3255 #while ( $columns = $csv->getline($fh) ) {
3257 while ( defined($line=<$fh>) ) {
3259 $csv->parse($line) or do {
3260 $dbh->rollback if $oldAutoCommit;
3261 return "can't parse: ". $csv->error_input();
3264 my @columns = $csv->fields();
3265 #warn join('-',@columns);
3268 foreach my $field ( @fields ) {
3269 $row{$field} = shift @columns;
3272 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
3273 unless ( $cust_main ) {
3274 $dbh->rollback if $oldAutoCommit;
3275 return "unknown custnum $row{'custnum'}";
3278 if ( $row{'amount'} > 0 ) {
3279 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
3281 $dbh->rollback if $oldAutoCommit;
3285 } elsif ( $row{'amount'} < 0 ) {
3286 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
3289 $dbh->rollback if $oldAutoCommit;
3299 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3301 return "Empty file!" unless $imported;
3313 The delete method should possibly take an FS::cust_main object reference
3314 instead of a scalar customer number.
3316 Bill and collect options should probably be passed as references instead of a
3319 There should probably be a configuration file with a list of allowed credit
3322 No multiple currency support (probably a larger project than just this module).
3324 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
3328 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
3329 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
3330 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.