4 use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf @encrypted_fields
5 $import $skip_fuzzyfiles $ignore_expired_card );
6 use vars qw( $realtime_bop_decline_quiet ); #ugh
11 eval "use Time::Local;";
12 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
13 if $] < 5.006 && !defined($Time::Local::VERSION);
14 #eval "use Time::Local qw(timelocal timelocal_nocheck);";
15 eval "use Time::Local qw(timelocal_nocheck);";
17 use Digest::MD5 qw(md5_base64);
21 use String::Approx qw(amatch);
22 use Business::CreditCard 0.28;
24 use FS::UID qw( getotaker dbh );
25 use FS::Record qw( qsearchs qsearch dbdef );
26 use FS::Misc qw( send_email );
27 use FS::Msgcat qw(gettext);
31 use FS::cust_bill_pkg;
33 use FS::cust_pay_void;
36 use FS::part_referral;
37 use FS::cust_main_county;
39 use FS::cust_main_invoice;
40 use FS::cust_credit_bill;
41 use FS::cust_bill_pay;
42 use FS::prepay_credit;
45 use FS::part_bill_event;
46 use FS::cust_bill_event;
47 use FS::cust_tax_exempt;
48 use FS::cust_tax_exempt_pkg;
50 use FS::payment_gateway;
51 use FS::agent_payment_gateway;
54 @ISA = qw( FS::Record );
56 @EXPORT_OK = qw( smart_search );
58 $realtime_bop_decline_quiet = 0;
60 # 1 is mostly method/subroutine entry and options
61 # 2 traces progress of some operations
62 # 3 is even more information including possibly sensitive data
64 $me = '[FS::cust_main]';
68 $ignore_expired_card = 0;
70 @encrypted_fields = ('payinfo', 'paycvv');
72 #ask FS::UID to run this stuff for us later
73 #$FS::UID::callback{'FS::cust_main'} = sub {
74 install_callback FS::UID sub {
76 #yes, need it for stuff below (prolly should be cached)
81 my ( $hashref, $cache ) = @_;
82 if ( exists $hashref->{'pkgnum'} ) {
83 #@{ $self->{'_pkgnum'} } = ();
84 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
85 $self->{'_pkgnum'} = $subcache;
86 #push @{ $self->{'_pkgnum'} },
87 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
93 FS::cust_main - Object methods for cust_main records
99 $record = new FS::cust_main \%hash;
100 $record = new FS::cust_main { 'column' => 'value' };
102 $error = $record->insert;
104 $error = $new_record->replace($old_record);
106 $error = $record->delete;
108 $error = $record->check;
110 @cust_pkg = $record->all_pkgs;
112 @cust_pkg = $record->ncancelled_pkgs;
114 @cust_pkg = $record->suspended_pkgs;
116 $error = $record->bill;
117 $error = $record->bill %options;
118 $error = $record->bill 'time' => $time;
120 $error = $record->collect;
121 $error = $record->collect %options;
122 $error = $record->collect 'invoice_time' => $time,
127 An FS::cust_main object represents a customer. FS::cust_main inherits from
128 FS::Record. The following fields are currently supported:
132 =item custnum - primary key (assigned automatically for new customers)
134 =item agentnum - agent (see L<FS::agent>)
136 =item refnum - Advertising source (see L<FS::part_referral>)
142 =item ss - social security number (optional)
144 =item company - (optional)
148 =item address2 - (optional)
152 =item county - (optional, see L<FS::cust_main_county>)
154 =item state - (see L<FS::cust_main_county>)
158 =item country - (see L<FS::cust_main_county>)
160 =item daytime - phone (optional)
162 =item night - phone (optional)
164 =item fax - phone (optional)
166 =item ship_first - name
168 =item ship_last - name
170 =item ship_company - (optional)
174 =item ship_address2 - (optional)
178 =item ship_county - (optional, see L<FS::cust_main_county>)
180 =item ship_state - (see L<FS::cust_main_county>)
184 =item ship_country - (see L<FS::cust_main_county>)
186 =item ship_daytime - phone (optional)
188 =item ship_night - phone (optional)
190 =item ship_fax - phone (optional)
194 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>)
198 Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
203 my($self,$payinfo) = @_;
204 if ( defined($payinfo) ) {
205 $self->paymask($payinfo);
206 $self->setfield('payinfo', $payinfo); # This is okay since we are the 'setter'
208 $payinfo = $self->getfield('payinfo'); # This is okay since we are the 'getter'
216 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
220 =item paymask - Masked payment type
226 Mask all but the last four characters.
230 Mask all but last 2 of account number and bank routing number.
234 Do nothing, return the unmasked string.
243 # If it doesn't exist then generate it
244 my $paymask=$self->getfield('paymask');
245 if (!defined($value) && (!defined($paymask) || $paymask eq '')) {
246 $value = $self->payinfo;
249 if ( defined($value) && !$self->is_encrypted($value)) {
250 my $payinfo = $value;
251 my $payby = $self->payby;
252 if ($payby eq 'CARD' || $payby eq 'DCRD') { # Credit Cards (Show last four)
253 $paymask = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
254 } elsif ($payby eq 'CHEK' ||
255 $payby eq 'DCHK' ) { # Checks (Show last 2 @ bank)
256 my( $account, $aba ) = split('@', $payinfo );
257 $paymask = 'x'x(length($account)-2). substr($account,(length($account)-2))."@".$aba;
258 } else { # Tie up loose ends
261 $self->setfield('paymask', $paymask); # This is okay since we are the 'setter'
262 } elsif (defined($value) && $self->is_encrypted($value)) {
268 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
270 =item paystart_month - start date month (maestro/solo cards only)
272 =item paystart_year - start date year (maestro/solo cards only)
274 =item payissue - issue number (maestro/solo cards only)
276 =item payname - name on card or billing name
278 =item payip - IP address from which payment information was received
280 =item tax - tax exempt, empty or `Y'
282 =item otaker - order taker (assigned automatically, see L<FS::UID>)
284 =item comments - comments (optional)
286 =item referral_custnum - referring customer number
288 =item spool_cdr - Enable individual CDR spooling, empty or `Y'
298 Creates a new customer. To add the customer to the database, see L<"insert">.
300 Note that this stores the hash reference, not a distinct copy of the hash it
301 points to. You can ask the object for a copy with the I<hash> method.
305 sub table { 'cust_main'; }
307 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
309 Adds this customer to the database. If there is an error, returns the error,
310 otherwise returns false.
312 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
313 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
314 are inserted atomicly, or the transaction is rolled back. Passing an empty
315 hash reference is equivalent to not supplying this parameter. There should be
316 a better explanation of this, but until then, here's an example:
319 tie %hash, 'Tie::RefHash'; #this part is important
321 $cust_pkg => [ $svc_acct ],
324 $cust_main->insert( \%hash );
326 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
327 be set as the invoicing list (see L<"invoicing_list">). Errors return as
328 expected and rollback the entire transaction; it is not necessary to call
329 check_invoicing_list first. The invoicing_list is set after the records in the
330 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
331 invoicing_list destination to the newly-created svc_acct. Here's an example:
333 $cust_main->insert( {}, [ $email, 'POST' ] );
335 Currently available options are: I<depend_jobnum> and I<noexport>.
337 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
338 on the supplied jobnum (they will not run until the specific job completes).
339 This can be used to defer provisioning until some action completes (such
340 as running the customer's credit card successfully).
342 The I<noexport> option is deprecated. If I<noexport> is set true, no
343 provisioning jobs (exports) are scheduled. (You can schedule them later with
344 the B<reexport> method.)
350 my $cust_pkgs = @_ ? shift : {};
351 my $invoicing_list = @_ ? shift : '';
353 warn "$me insert called with options ".
354 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
357 local $SIG{HUP} = 'IGNORE';
358 local $SIG{INT} = 'IGNORE';
359 local $SIG{QUIT} = 'IGNORE';
360 local $SIG{TERM} = 'IGNORE';
361 local $SIG{TSTP} = 'IGNORE';
362 local $SIG{PIPE} = 'IGNORE';
364 my $oldAutoCommit = $FS::UID::AutoCommit;
365 local $FS::UID::AutoCommit = 0;
368 my $prepay_identifier = '';
369 my( $amount, $seconds ) = ( 0, 0 );
371 if ( $self->payby eq 'PREPAY' ) {
373 $self->payby('BILL');
374 $prepay_identifier = $self->payinfo;
377 warn " looking up prepaid card $prepay_identifier\n"
380 my $error = $self->get_prepay($prepay_identifier, \$amount, \$seconds);
382 $dbh->rollback if $oldAutoCommit;
383 #return "error applying prepaid card (transaction rolled back): $error";
387 $payby = 'PREP' if $amount;
389 } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
392 $self->payby('BILL');
393 $amount = $self->paid;
397 warn " inserting $self\n"
400 my $error = $self->SUPER::insert;
402 $dbh->rollback if $oldAutoCommit;
403 #return "inserting cust_main record (transaction rolled back): $error";
407 warn " setting invoicing list\n"
410 if ( $invoicing_list ) {
411 $error = $self->check_invoicing_list( $invoicing_list );
413 $dbh->rollback if $oldAutoCommit;
414 return "checking invoicing_list (transaction rolled back): $error";
416 $self->invoicing_list( $invoicing_list );
419 if ( $conf->config('cust_main-skeleton_tables')
420 && $conf->config('cust_main-skeleton_custnum') ) {
422 warn " inserting skeleton records\n"
425 my $error = $self->start_copy_skel;
427 $dbh->rollback if $oldAutoCommit;
433 warn " ordering packages\n"
436 $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
438 $dbh->rollback if $oldAutoCommit;
443 $dbh->rollback if $oldAutoCommit;
444 return "No svc_acct record to apply pre-paid time";
448 warn " inserting initial $payby payment of $amount\n"
450 $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
452 $dbh->rollback if $oldAutoCommit;
453 return "inserting payment (transaction rolled back): $error";
457 unless ( $import || $skip_fuzzyfiles ) {
458 warn " queueing fuzzyfiles update\n"
460 $error = $self->queue_fuzzyfiles_update;
462 $dbh->rollback if $oldAutoCommit;
463 return "updating fuzzy search cache: $error";
467 warn " insert complete; committing transaction\n"
470 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
475 sub start_copy_skel {
478 #'mg_user_preference' => {},
479 #'mg_user_indicator_profile' => { 'mg_profile_indicator' => { 'mg_profile_details' }, },
480 #'mg_watchlist_header' => { 'mg_watchlist_details' },
481 #'mg_user_grid_header' => { 'mg_user_grid_details' },
482 #'mg_portfolio_header' => { 'mg_portfolio_trades' => { 'mg_portfolio_trades_positions' } },
483 my @tables = eval($conf->config('cust_main-skeleton_tables'));
486 _copy_skel( 'cust_main', #tablename
487 $conf->config('cust_main-skeleton_custnum'), #sourceid
488 $self->custnum, #destid
489 @tables, #child tables
493 #recursive subroutine, not a method
495 my( $table, $sourceid, $destid, %child_tables ) = @_;
497 my $dbdef_table = dbdef->table($table);
498 my $primary_key = $dbdef_table->primary_key
499 or return "$table has no primary key".
500 " (or do you need to run dbdef-create?)";
502 warn " _copy_skel: $table.$primary_key $sourceid to $destid for ".
503 join (', ', keys %child_tables). "\n"
506 foreach my $child_table ( keys %child_tables ) {
508 my $child_pkey = dbdef->table($child_table)->primary_key;
509 # or return "$table has no primary key".
510 # " (or do you need to run dbdef-create?)\n";
512 if ( keys %{ $child_tables{$child_table} } ) {
514 return "$child_table has no primary key\n" unless $child_pkey;
516 #false laziness w/Record::insert and only works on Pg
517 #refactor the proper last-inserted-id stuff out of Record::insert if this
518 # ever gets use for anything besides a quick kludge for one customer
519 my $default = dbdef->table($child_table)->column($child_pkey)->default;
520 $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
521 or return "can't parse $child_table.$child_pkey default value ".
522 " for sequence name: $default";
527 my @sel_columns = grep { $_ ne $primary_key } dbdef->table($table)->columns;
528 my $sel_columns = ' ( '. join(', ', @sel_columns ). ' ) ';
530 my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
531 my $ins_columns = ' ( ', join(', ', $primary_key, @ins_columns ). ' ) ',
532 my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
534 my $sel_sth = dbh->prepare( "SELECT $sel_columns FROM $child_table".
535 " WHERE $primary_key = $sourceid")
536 or return dbh->errstr;
538 $sel_sth->execute or return $sel_sth->errstr;
540 while ( my $row = $sel_sth->fetchrow_hashref ) {
543 dbh->prepare("INSERT INTO $child_table $ins_columns".
544 " VALUES $placeholders")
545 or return dbh->errstr;
546 $ins_sth->execute( $destid, map $row->{$_}, @ins_columns )
547 or return $ins_sth->errstr;
549 #next unless keys %{ $child_tables{$child_table} };
550 next unless $sequence;
552 #another section of that laziness
553 my $seq_sql = "SELECT currval('$sequence')";
554 my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
555 $seq_sth->execute or return $seq_sth->errstr;
556 my $insertid = $seq_sth->fetchrow_arrayref->[0];
558 # don't drink soap! recurse! recurse! okay!
560 _copy_skel( $child_table,
561 $row->{$child_pkey}, #sourceid
563 %{ $child_tables{$child_table} },
565 return $error if $error;
575 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
577 Like the insert method on an existing record, this method orders a package
578 and included services atomicaly. Pass a Tie::RefHash data structure to this
579 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
580 be a better explanation of this, but until then, here's an example:
583 tie %hash, 'Tie::RefHash'; #this part is important
585 $cust_pkg => [ $svc_acct ],
588 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
590 Services can be new, in which case they are inserted, or existing unaudited
591 services, in which case they are linked to the newly-created package.
593 Currently available options are: I<depend_jobnum> and I<noexport>.
595 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
596 on the supplied jobnum (they will not run until the specific job completes).
597 This can be used to defer provisioning until some action completes (such
598 as running the customer's credit card successfully).
600 The I<noexport> option is deprecated. If I<noexport> is set true, no
601 provisioning jobs (exports) are scheduled. (You can schedule them later with
602 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
603 on the cust_main object is not recommended, as existing services will also be
610 my $cust_pkgs = shift;
613 my %svc_options = ();
614 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
615 if exists $options{'depend_jobnum'};
616 warn "$me order_pkgs called with options ".
617 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
620 local $SIG{HUP} = 'IGNORE';
621 local $SIG{INT} = 'IGNORE';
622 local $SIG{QUIT} = 'IGNORE';
623 local $SIG{TERM} = 'IGNORE';
624 local $SIG{TSTP} = 'IGNORE';
625 local $SIG{PIPE} = 'IGNORE';
627 my $oldAutoCommit = $FS::UID::AutoCommit;
628 local $FS::UID::AutoCommit = 0;
631 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
633 foreach my $cust_pkg ( keys %$cust_pkgs ) {
634 $cust_pkg->custnum( $self->custnum );
635 my $error = $cust_pkg->insert;
637 $dbh->rollback if $oldAutoCommit;
638 return "inserting cust_pkg (transaction rolled back): $error";
640 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
641 if ( $svc_something->svcnum ) {
642 my $old_cust_svc = $svc_something->cust_svc;
643 my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
644 $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
645 $error = $new_cust_svc->replace($old_cust_svc);
647 $svc_something->pkgnum( $cust_pkg->pkgnum );
648 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
649 $svc_something->seconds( $svc_something->seconds + $$seconds );
652 $error = $svc_something->insert(%svc_options);
655 $dbh->rollback if $oldAutoCommit;
656 #return "inserting svc_ (transaction rolled back): $error";
662 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
666 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF ]
668 Recharges this (existing) customer with the specified prepaid card (see
669 L<FS::prepay_credit>), specified either by I<identifier> or as an
670 FS::prepay_credit object. If there is an error, returns the error, otherwise
673 Optionally, two scalar references can be passed as well. They will have their
674 values filled in with the amount and number of seconds applied by this prepaid
679 sub recharge_prepay {
680 my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
682 local $SIG{HUP} = 'IGNORE';
683 local $SIG{INT} = 'IGNORE';
684 local $SIG{QUIT} = 'IGNORE';
685 local $SIG{TERM} = 'IGNORE';
686 local $SIG{TSTP} = 'IGNORE';
687 local $SIG{PIPE} = 'IGNORE';
689 my $oldAutoCommit = $FS::UID::AutoCommit;
690 local $FS::UID::AutoCommit = 0;
693 my( $amount, $seconds ) = ( 0, 0 );
695 my $error = $self->get_prepay($prepay_credit, \$amount, \$seconds)
696 || $self->increment_seconds($seconds)
697 || $self->insert_cust_pay_prepay( $amount,
699 ? $prepay_credit->identifier
704 $dbh->rollback if $oldAutoCommit;
708 if ( defined($amountref) ) { $$amountref = $amount; }
709 if ( defined($secondsref) ) { $$secondsref = $seconds; }
711 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
716 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ , AMOUNTREF, SECONDSREF
718 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
719 specified either by I<identifier> or as an FS::prepay_credit object.
721 References to I<amount> and I<seconds> scalars should be passed as arguments
722 and will be incremented by the values of the prepaid card.
724 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
725 check or set this customer's I<agentnum>.
727 If there is an error, returns the error, otherwise returns false.
733 my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
735 local $SIG{HUP} = 'IGNORE';
736 local $SIG{INT} = 'IGNORE';
737 local $SIG{QUIT} = 'IGNORE';
738 local $SIG{TERM} = 'IGNORE';
739 local $SIG{TSTP} = 'IGNORE';
740 local $SIG{PIPE} = 'IGNORE';
742 my $oldAutoCommit = $FS::UID::AutoCommit;
743 local $FS::UID::AutoCommit = 0;
746 unless ( ref($prepay_credit) ) {
748 my $identifier = $prepay_credit;
750 $prepay_credit = qsearchs(
752 { 'identifier' => $prepay_credit },
757 unless ( $prepay_credit ) {
758 $dbh->rollback if $oldAutoCommit;
759 return "Invalid prepaid card: ". $identifier;
764 if ( $prepay_credit->agentnum ) {
765 if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
766 $dbh->rollback if $oldAutoCommit;
767 return "prepaid card not valid for agent ". $self->agentnum;
769 $self->agentnum($prepay_credit->agentnum);
772 my $error = $prepay_credit->delete;
774 $dbh->rollback if $oldAutoCommit;
775 return "removing prepay_credit (transaction rolled back): $error";
778 $$amountref += $prepay_credit->amount;
779 $$secondsref += $prepay_credit->seconds;
781 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
786 =item increment_seconds SECONDS
788 Updates this customer's single or primary account (see L<FS::svc_acct>) by
789 the specified number of seconds. If there is an error, returns the error,
790 otherwise returns false.
794 sub increment_seconds {
795 my( $self, $seconds ) = @_;
796 warn "$me increment_seconds called: $seconds seconds\n"
799 my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
800 $self->ncancelled_pkgs;
803 return 'No packages with primary or single services found'.
804 ' to apply pre-paid time';
805 } elsif ( scalar(@cust_pkg) > 1 ) {
806 #maybe have a way to specify the package/account?
807 return 'Multiple packages found to apply pre-paid time';
810 my $cust_pkg = $cust_pkg[0];
811 warn " found package pkgnum ". $cust_pkg->pkgnum. "\n"
815 $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
818 return 'No account found to apply pre-paid time';
819 } elsif ( scalar(@cust_svc) > 1 ) {
820 return 'Multiple accounts found to apply pre-paid time';
823 my $svc_acct = $cust_svc[0]->svc_x;
824 warn " found service svcnum ". $svc_acct->pkgnum.
825 ' ('. $svc_acct->email. ")\n"
828 $svc_acct->increment_seconds($seconds);
832 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
834 Inserts a prepayment in the specified amount for this customer. An optional
835 second argument can specify the prepayment identifier for tracking purposes.
836 If there is an error, returns the error, otherwise returns false.
840 sub insert_cust_pay_prepay {
841 shift->insert_cust_pay('PREP', @_);
844 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
846 Inserts a cash payment in the specified amount for this customer. An optional
847 second argument can specify the payment identifier for tracking purposes.
848 If there is an error, returns the error, otherwise returns false.
852 sub insert_cust_pay_cash {
853 shift->insert_cust_pay('CASH', @_);
856 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
858 Inserts a Western Union payment in the specified amount for this customer. An
859 optional second argument can specify the prepayment identifier for tracking
860 purposes. If there is an error, returns the error, otherwise returns false.
864 sub insert_cust_pay_west {
865 shift->insert_cust_pay('WEST', @_);
868 sub insert_cust_pay {
869 my( $self, $payby, $amount ) = splice(@_, 0, 3);
870 my $payinfo = scalar(@_) ? shift : '';
872 my $cust_pay = new FS::cust_pay {
873 'custnum' => $self->custnum,
874 'paid' => sprintf('%.2f', $amount),
875 #'_date' => #date the prepaid card was purchased???
877 'payinfo' => $payinfo,
885 This method is deprecated. See the I<depend_jobnum> option to the insert and
886 order_pkgs methods for a better way to defer provisioning.
888 Re-schedules all exports by calling the B<reexport> method of all associated
889 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
890 otherwise returns false.
897 carp "WARNING: FS::cust_main::reexport is deprectated; ".
898 "use the depend_jobnum option to insert or order_pkgs to delay export";
900 local $SIG{HUP} = 'IGNORE';
901 local $SIG{INT} = 'IGNORE';
902 local $SIG{QUIT} = 'IGNORE';
903 local $SIG{TERM} = 'IGNORE';
904 local $SIG{TSTP} = 'IGNORE';
905 local $SIG{PIPE} = 'IGNORE';
907 my $oldAutoCommit = $FS::UID::AutoCommit;
908 local $FS::UID::AutoCommit = 0;
911 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
912 my $error = $cust_pkg->reexport;
914 $dbh->rollback if $oldAutoCommit;
919 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
924 =item delete NEW_CUSTNUM
926 This deletes the customer. If there is an error, returns the error, otherwise
929 This will completely remove all traces of the customer record. This is not
930 what you want when a customer cancels service; for that, cancel all of the
931 customer's packages (see L</cancel>).
933 If the customer has any uncancelled packages, you need to pass a new (valid)
934 customer number for those packages to be transferred to. Cancelled packages
935 will be deleted. Did I mention that this is NOT what you want when a customer
936 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
938 You can't delete a customer with invoices (see L<FS::cust_bill>),
939 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
940 refunds (see L<FS::cust_refund>).
947 local $SIG{HUP} = 'IGNORE';
948 local $SIG{INT} = 'IGNORE';
949 local $SIG{QUIT} = 'IGNORE';
950 local $SIG{TERM} = 'IGNORE';
951 local $SIG{TSTP} = 'IGNORE';
952 local $SIG{PIPE} = 'IGNORE';
954 my $oldAutoCommit = $FS::UID::AutoCommit;
955 local $FS::UID::AutoCommit = 0;
958 if ( $self->cust_bill ) {
959 $dbh->rollback if $oldAutoCommit;
960 return "Can't delete a customer with invoices";
962 if ( $self->cust_credit ) {
963 $dbh->rollback if $oldAutoCommit;
964 return "Can't delete a customer with credits";
966 if ( $self->cust_pay ) {
967 $dbh->rollback if $oldAutoCommit;
968 return "Can't delete a customer with payments";
970 if ( $self->cust_refund ) {
971 $dbh->rollback if $oldAutoCommit;
972 return "Can't delete a customer with refunds";
975 my @cust_pkg = $self->ncancelled_pkgs;
977 my $new_custnum = shift;
978 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
979 $dbh->rollback if $oldAutoCommit;
980 return "Invalid new customer number: $new_custnum";
982 foreach my $cust_pkg ( @cust_pkg ) {
983 my %hash = $cust_pkg->hash;
984 $hash{'custnum'} = $new_custnum;
985 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
986 my $error = $new_cust_pkg->replace($cust_pkg);
988 $dbh->rollback if $oldAutoCommit;
993 my @cancelled_cust_pkg = $self->all_pkgs;
994 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
995 my $error = $cust_pkg->delete;
997 $dbh->rollback if $oldAutoCommit;
1002 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
1003 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
1005 my $error = $cust_main_invoice->delete;
1007 $dbh->rollback if $oldAutoCommit;
1012 my $error = $self->SUPER::delete;
1014 $dbh->rollback if $oldAutoCommit;
1018 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1023 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
1025 Replaces the OLD_RECORD with this one in the database. If there is an error,
1026 returns the error, otherwise returns false.
1028 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1029 be set as the invoicing list (see L<"invoicing_list">). Errors return as
1030 expected and rollback the entire transaction; it is not necessary to call
1031 check_invoicing_list first. Here's an example:
1033 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1041 warn "$me replace called\n"
1044 local $SIG{HUP} = 'IGNORE';
1045 local $SIG{INT} = 'IGNORE';
1046 local $SIG{QUIT} = 'IGNORE';
1047 local $SIG{TERM} = 'IGNORE';
1048 local $SIG{TSTP} = 'IGNORE';
1049 local $SIG{PIPE} = 'IGNORE';
1051 # If the mask is blank then try to set it - if we can...
1052 if (!defined($self->getfield('paymask')) || $self->getfield('paymask') eq '') {
1053 $self->paymask($self->payinfo);
1056 # We absolutely have to have an old vs. new record to make this work.
1057 if (!defined($old)) {
1058 $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1061 my $curuser = $FS::CurrentUser::CurrentUser;
1062 if ( $self->payby eq 'COMP'
1063 && $self->payby ne $old->payby
1064 && ! $curuser->access_right('Complimentary customer')
1067 return "You are not permitted to create complimentary accounts.";
1070 local($ignore_expired_card) = 1
1071 if $old->payby =~ /^(CARD|DCRD)$/
1072 && $self->payby =~ /^(CARD|DCRD)$/
1073 && $old->payinfo eq $self->payinfo;
1075 my $oldAutoCommit = $FS::UID::AutoCommit;
1076 local $FS::UID::AutoCommit = 0;
1079 my $error = $self->SUPER::replace($old);
1082 $dbh->rollback if $oldAutoCommit;
1086 if ( @param ) { # INVOICING_LIST_ARYREF
1087 my $invoicing_list = shift @param;
1088 $error = $self->check_invoicing_list( $invoicing_list );
1090 $dbh->rollback if $oldAutoCommit;
1093 $self->invoicing_list( $invoicing_list );
1096 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
1097 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
1098 # card/check/lec info has changed, want to retry realtime_ invoice events
1099 my $error = $self->retry_realtime;
1101 $dbh->rollback if $oldAutoCommit;
1106 unless ( $import || $skip_fuzzyfiles ) {
1107 $error = $self->queue_fuzzyfiles_update;
1109 $dbh->rollback if $oldAutoCommit;
1110 return "updating fuzzy search cache: $error";
1114 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1119 =item queue_fuzzyfiles_update
1121 Used by insert & replace to update the fuzzy search cache
1125 sub queue_fuzzyfiles_update {
1128 local $SIG{HUP} = 'IGNORE';
1129 local $SIG{INT} = 'IGNORE';
1130 local $SIG{QUIT} = 'IGNORE';
1131 local $SIG{TERM} = 'IGNORE';
1132 local $SIG{TSTP} = 'IGNORE';
1133 local $SIG{PIPE} = 'IGNORE';
1135 my $oldAutoCommit = $FS::UID::AutoCommit;
1136 local $FS::UID::AutoCommit = 0;
1139 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1140 my $error = $queue->insert( map $self->getfield($_),
1141 qw(first last company)
1144 $dbh->rollback if $oldAutoCommit;
1145 return "queueing job (transaction rolled back): $error";
1148 if ( $self->ship_last ) {
1149 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1150 $error = $queue->insert( map $self->getfield("ship_$_"),
1151 qw(first last company)
1154 $dbh->rollback if $oldAutoCommit;
1155 return "queueing job (transaction rolled back): $error";
1159 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1166 Checks all fields to make sure this is a valid customer record. If there is
1167 an error, returns the error, otherwise returns false. Called by the insert
1168 and replace methods.
1175 warn "$me check BEFORE: \n". $self->_dump
1179 $self->ut_numbern('custnum')
1180 || $self->ut_number('agentnum')
1181 || $self->ut_textn('agent_custid')
1182 || $self->ut_number('refnum')
1183 || $self->ut_name('last')
1184 || $self->ut_name('first')
1185 || $self->ut_textn('company')
1186 || $self->ut_text('address1')
1187 || $self->ut_textn('address2')
1188 || $self->ut_text('city')
1189 || $self->ut_textn('county')
1190 || $self->ut_textn('state')
1191 || $self->ut_country('country')
1192 || $self->ut_anything('comments')
1193 || $self->ut_numbern('referral_custnum')
1195 #barf. need message catalogs. i18n. etc.
1196 $error .= "Please select an advertising source."
1197 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1198 return $error if $error;
1200 return "Unknown agent"
1201 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1203 return "Unknown refnum"
1204 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1206 return "Unknown referring custnum: ". $self->referral_custnum
1207 unless ! $self->referral_custnum
1208 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1210 if ( $self->ss eq '' ) {
1215 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1216 or return "Illegal social security number: ". $self->ss;
1217 $self->ss("$1-$2-$3");
1221 # bad idea to disable, causes billing to fail because of no tax rates later
1222 # unless ( $import ) {
1223 unless ( qsearch('cust_main_county', {
1224 'country' => $self->country,
1227 return "Unknown state/county/country: ".
1228 $self->state. "/". $self->county. "/". $self->country
1229 unless qsearch('cust_main_county',{
1230 'state' => $self->state,
1231 'county' => $self->county,
1232 'country' => $self->country,
1238 $self->ut_phonen('daytime', $self->country)
1239 || $self->ut_phonen('night', $self->country)
1240 || $self->ut_phonen('fax', $self->country)
1241 || $self->ut_zip('zip', $self->country)
1243 return $error if $error;
1246 last first company address1 address2 city county state zip
1247 country daytime night fax
1250 if ( defined $self->dbdef_table->column('ship_last') ) {
1251 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1253 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
1257 $self->ut_name('ship_last')
1258 || $self->ut_name('ship_first')
1259 || $self->ut_textn('ship_company')
1260 || $self->ut_text('ship_address1')
1261 || $self->ut_textn('ship_address2')
1262 || $self->ut_text('ship_city')
1263 || $self->ut_textn('ship_county')
1264 || $self->ut_textn('ship_state')
1265 || $self->ut_country('ship_country')
1267 return $error if $error;
1269 #false laziness with above
1270 unless ( qsearchs('cust_main_county', {
1271 'country' => $self->ship_country,
1274 return "Unknown ship_state/ship_county/ship_country: ".
1275 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1276 unless qsearch('cust_main_county',{
1277 'state' => $self->ship_state,
1278 'county' => $self->ship_county,
1279 'country' => $self->ship_country,
1285 $self->ut_phonen('ship_daytime', $self->ship_country)
1286 || $self->ut_phonen('ship_night', $self->ship_country)
1287 || $self->ut_phonen('ship_fax', $self->ship_country)
1288 || $self->ut_zip('ship_zip', $self->ship_country)
1290 return $error if $error;
1292 } else { # ship_ info eq billing info, so don't store dup info in database
1293 $self->setfield("ship_$_", '')
1294 foreach qw( last first company address1 address2 city county state zip
1295 country daytime night fax );
1299 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1300 or return "Illegal payby: ". $self->payby;
1302 $error = $self->ut_numbern('paystart_month')
1303 || $self->ut_numbern('paystart_year')
1304 || $self->ut_numbern('payissue')
1306 return $error if $error;
1308 if ( $self->payip eq '' ) {
1311 $error = $self->ut_ip('payip');
1312 return $error if $error;
1315 # If it is encrypted and the private key is not availaible then we can't
1316 # check the credit card.
1318 my $check_payinfo = 1;
1320 if ($self->is_encrypted($self->payinfo)) {
1326 if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1328 my $payinfo = $self->payinfo;
1329 $payinfo =~ s/\D//g;
1330 $payinfo =~ /^(\d{13,16})$/
1331 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1333 $self->payinfo($payinfo);
1335 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1337 return gettext('unknown_card_type')
1338 if cardtype($self->payinfo) eq "Unknown";
1340 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1342 return 'Banned credit card: banned on '.
1343 time2str('%a %h %o at %r', $ban->_date).
1344 ' by '. $ban->otaker.
1345 ' (ban# '. $ban->bannum. ')';
1348 if ( defined $self->dbdef_table->column('paycvv') ) {
1349 if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1350 if ( cardtype($self->payinfo) eq 'American Express card' ) {
1351 $self->paycvv =~ /^(\d{4})$/
1352 or return "CVV2 (CID) for American Express cards is four digits.";
1355 $self->paycvv =~ /^(\d{3})$/
1356 or return "CVV2 (CVC2/CID) is three digits.";
1364 my $cardtype = cardtype($payinfo);
1365 if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1367 return "Start date or issue number is required for $cardtype cards"
1368 unless $self->paystart_month && $self->paystart_year or $self->payissue;
1370 return "Start month must be between 1 and 12"
1371 if $self->paystart_month
1372 and $self->paystart_month < 1 || $self->paystart_month > 12;
1374 return "Start year must be 1990 or later"
1375 if $self->paystart_year
1376 and $self->paystart_year < 1990;
1378 return "Issue number must be beween 1 and 99"
1380 and $self->payissue < 1 || $self->payissue > 99;
1383 $self->paystart_month('');
1384 $self->paystart_year('');
1385 $self->payissue('');
1388 } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1390 my $payinfo = $self->payinfo;
1391 $payinfo =~ s/[^\d\@]//g;
1392 if ( $conf->exists('echeck-nonus') ) {
1393 $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1394 $payinfo = "$1\@$2";
1396 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1397 $payinfo = "$1\@$2";
1399 $self->payinfo($payinfo);
1400 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1402 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1404 return 'Banned ACH account: banned on '.
1405 time2str('%a %h %o at %r', $ban->_date).
1406 ' by '. $ban->otaker.
1407 ' (ban# '. $ban->bannum. ')';
1410 } elsif ( $self->payby eq 'LECB' ) {
1412 my $payinfo = $self->payinfo;
1413 $payinfo =~ s/\D//g;
1414 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1416 $self->payinfo($payinfo);
1417 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1419 } elsif ( $self->payby eq 'BILL' ) {
1421 $error = $self->ut_textn('payinfo');
1422 return "Illegal P.O. number: ". $self->payinfo if $error;
1423 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1425 } elsif ( $self->payby eq 'COMP' ) {
1427 my $curuser = $FS::CurrentUser::CurrentUser;
1428 if ( ! $self->custnum
1429 && ! $curuser->access_right('Complimentary customer')
1432 return "You are not permitted to create complimentary accounts."
1435 $error = $self->ut_textn('payinfo');
1436 return "Illegal comp account issuer: ". $self->payinfo if $error;
1437 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1439 } elsif ( $self->payby eq 'PREPAY' ) {
1441 my $payinfo = $self->payinfo;
1442 $payinfo =~ s/\W//g; #anything else would just confuse things
1443 $self->payinfo($payinfo);
1444 $error = $self->ut_alpha('payinfo');
1445 return "Illegal prepayment identifier: ". $self->payinfo if $error;
1446 return "Unknown prepayment identifier"
1447 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1448 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1452 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1453 return "Expiration date required"
1454 unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1458 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1459 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1460 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1461 ( $m, $y ) = ( $3, "20$2" );
1463 return "Illegal expiration date: ". $self->paydate;
1465 $self->paydate("$y-$m-01");
1466 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1467 return gettext('expired_card')
1469 && !$ignore_expired_card
1470 && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1473 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1474 ( ! $conf->exists('require_cardname')
1475 || $self->payby !~ /^(CARD|DCRD)$/ )
1477 $self->payname( $self->first. " ". $self->getfield('last') );
1479 $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1480 or return gettext('illegal_name'). " payname: ". $self->payname;
1484 foreach my $flag (qw( tax spool_cdr )) {
1485 $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1489 $self->otaker(getotaker) unless $self->otaker;
1491 warn "$me check AFTER: \n". $self->_dump
1494 $self->SUPER::check;
1499 Returns all packages (see L<FS::cust_pkg>) for this customer.
1505 if ( $self->{'_pkgnum'} ) {
1506 values %{ $self->{'_pkgnum'}->cache };
1508 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
1512 =item ncancelled_pkgs
1514 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
1518 sub ncancelled_pkgs {
1520 if ( $self->{'_pkgnum'} ) {
1521 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
1523 @{ [ # force list context
1524 qsearch( 'cust_pkg', {
1525 'custnum' => $self->custnum,
1528 qsearch( 'cust_pkg', {
1529 'custnum' => $self->custnum,
1536 =item suspended_pkgs
1538 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
1542 sub suspended_pkgs {
1544 grep { $_->susp } $self->ncancelled_pkgs;
1547 =item unflagged_suspended_pkgs
1549 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
1550 customer (thouse packages without the `manual_flag' set).
1554 sub unflagged_suspended_pkgs {
1556 return $self->suspended_pkgs
1557 unless dbdef->table('cust_pkg')->column('manual_flag');
1558 grep { ! $_->manual_flag } $self->suspended_pkgs;
1561 =item unsuspended_pkgs
1563 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1568 sub unsuspended_pkgs {
1570 grep { ! $_->susp } $self->ncancelled_pkgs;
1573 =item num_cancelled_pkgs
1575 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
1580 sub num_cancelled_pkgs {
1582 $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
1586 my( $self, $sql ) = @_;
1587 my $sth = dbh->prepare(
1588 "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? AND $sql"
1589 ) or die dbh->errstr;
1590 $sth->execute($self->custnum) or die $sth->errstr;
1591 $sth->fetchrow_arrayref->[0];
1596 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1597 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
1598 on success or a list of errors.
1604 grep { $_->unsuspend } $self->suspended_pkgs;
1609 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1611 Returns a list: an empty list on success or a list of errors.
1617 grep { $_->suspend } $self->unsuspended_pkgs;
1620 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1622 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1623 PKGPARTs (see L<FS::part_pkg>).
1625 Returns a list: an empty list on success or a list of errors.
1629 sub suspend_if_pkgpart {
1632 grep { $_->suspend }
1633 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1634 $self->unsuspended_pkgs;
1637 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1639 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1640 listed PKGPARTs (see L<FS::part_pkg>).
1642 Returns a list: an empty list on success or a list of errors.
1646 sub suspend_unless_pkgpart {
1649 grep { $_->suspend }
1650 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1651 $self->unsuspended_pkgs;
1654 =item cancel [ OPTION => VALUE ... ]
1656 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1658 Available options are: I<quiet>, I<reasonnum>, and I<ban>
1660 I<quiet> can be set true to supress email cancellation notices.
1662 # I<reasonnum> can be set to a cancellation reason (see L<FS::cancel_reason>)
1664 I<ban> can be set true to ban this customer's credit card or ACH information,
1667 Always returns a list: an empty list on success or a list of errors.
1675 if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
1677 #should try decryption (we might have the private key)
1678 # and if not maybe queue a job for the server that does?
1679 return ( "Can't (yet) ban encrypted credit cards" )
1680 if $self->is_encrypted($self->payinfo);
1682 my $ban = new FS::banned_pay $self->_banned_pay_hashref;
1683 my $error = $ban->insert;
1684 return ( $error ) if $error;
1688 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1691 sub _banned_pay_hashref {
1702 'payby' => $payby2ban{$self->payby},
1703 'payinfo' => md5_base64($self->payinfo),
1710 Returns the agent (see L<FS::agent>) for this customer.
1716 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1721 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1722 conjunction with the collect method.
1724 Options are passed as name-value pairs.
1726 Currently available options are:
1728 resetup - if set true, re-charges setup fees.
1730 time - bills the customer as if it were that time. Specified as a UNIX
1731 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1732 L<Date::Parse> for conversion functions. For example:
1736 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1739 If there is an error, returns the error, otherwise returns false.
1744 my( $self, %options ) = @_;
1745 return '' if $self->payby eq 'COMP';
1746 warn "$me bill customer ". $self->custnum. "\n"
1749 my $time = $options{'time'} || time;
1754 local $SIG{HUP} = 'IGNORE';
1755 local $SIG{INT} = 'IGNORE';
1756 local $SIG{QUIT} = 'IGNORE';
1757 local $SIG{TERM} = 'IGNORE';
1758 local $SIG{TSTP} = 'IGNORE';
1759 local $SIG{PIPE} = 'IGNORE';
1761 my $oldAutoCommit = $FS::UID::AutoCommit;
1762 local $FS::UID::AutoCommit = 0;
1765 $self->select_for_update; #mutex
1767 #create a new invoice
1768 #(we'll remove it later if it doesn't actually need to be generated [contains
1769 # no line items] and we're inside a transaciton so nothing else will see it)
1770 my $cust_bill = new FS::cust_bill ( {
1771 'custnum' => $self->custnum,
1773 #'charged' => $charged,
1776 $error = $cust_bill->insert;
1778 $dbh->rollback if $oldAutoCommit;
1779 return "can't create invoice for customer #". $self->custnum. ": $error";
1781 my $invnum = $cust_bill->invnum;
1784 # find the packages which are due for billing, find out how much they are
1785 # & generate invoice database.
1788 my( $total_setup, $total_recur ) = ( 0, 0 );
1790 my @precommit_hooks = ();
1792 foreach my $cust_pkg (
1793 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1796 #NO!! next if $cust_pkg->cancel;
1797 next if $cust_pkg->getfield('cancel');
1799 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
1801 #? to avoid use of uninitialized value errors... ?
1802 $cust_pkg->setfield('bill', '')
1803 unless defined($cust_pkg->bill);
1805 my $part_pkg = $cust_pkg->part_pkg;
1807 my %hash = $cust_pkg->hash;
1808 my $old_cust_pkg = new FS::cust_pkg \%hash;
1817 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1819 warn " bill setup\n" if $DEBUG > 1;
1821 $setup = eval { $cust_pkg->calc_setup( $time ) };
1823 $dbh->rollback if $oldAutoCommit;
1824 return "$@ running calc_setup for $cust_pkg\n";
1827 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1831 # bill recurring fee
1836 if ( $part_pkg->getfield('freq') ne '0' &&
1837 ! $cust_pkg->getfield('susp') &&
1838 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1841 warn " bill recur\n" if $DEBUG > 1;
1843 # XXX shared with $recur_prog
1844 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1846 #over two params! lets at least switch to a hashref for the rest...
1847 my %param = ( 'precommit_hooks' => \@precommit_hooks, );
1849 $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
1851 $dbh->rollback if $oldAutoCommit;
1852 return "$@ running calc_recur for $cust_pkg\n";
1855 #change this bit to use Date::Manip? CAREFUL with timezones (see
1856 # mailing list archive)
1857 my ($sec,$min,$hour,$mday,$mon,$year) =
1858 (localtime($sdate) )[0,1,2,3,4,5];
1860 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1861 # only for figuring next bill date, nothing else, so, reset $sdate again
1863 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1864 $cust_pkg->last_bill($sdate)
1865 if $cust_pkg->dbdef_table->column('last_bill');
1867 if ( $part_pkg->freq =~ /^\d+$/ ) {
1868 $mon += $part_pkg->freq;
1869 until ( $mon < 12 ) { $mon -= 12; $year++; }
1870 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1872 $mday += $weeks * 7;
1873 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1876 } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
1880 $dbh->rollback if $oldAutoCommit;
1881 return "unparsable frequency: ". $part_pkg->freq;
1883 $cust_pkg->setfield('bill',
1884 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1887 warn "\$setup is undefined" unless defined($setup);
1888 warn "\$recur is undefined" unless defined($recur);
1889 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1892 # If $cust_pkg has been modified, update it and create cust_bill_pkg records
1895 if ( $cust_pkg->modified ) {
1897 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
1900 $error=$cust_pkg->replace($old_cust_pkg);
1901 if ( $error ) { #just in case
1902 $dbh->rollback if $oldAutoCommit;
1903 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1906 $setup = sprintf( "%.2f", $setup );
1907 $recur = sprintf( "%.2f", $recur );
1908 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1909 $dbh->rollback if $oldAutoCommit;
1910 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1912 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1913 $dbh->rollback if $oldAutoCommit;
1914 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1917 if ( $setup != 0 || $recur != 0 ) {
1919 warn " charges (setup=$setup, recur=$recur); adding line items\n"
1921 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1922 'invnum' => $invnum,
1923 'pkgnum' => $cust_pkg->pkgnum,
1927 'edate' => $cust_pkg->bill,
1928 'details' => \@details,
1930 $error = $cust_bill_pkg->insert;
1932 $dbh->rollback if $oldAutoCommit;
1933 return "can't create invoice line item for invoice #$invnum: $error";
1935 $total_setup += $setup;
1936 $total_recur += $recur;
1942 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1945 ( $conf->exists('tax-ship_address') && length($self->ship_last) )
1948 my %taxhash = map { $_ => $self->get("$prefix$_") }
1949 qw( state county country );
1951 $taxhash{'taxclass'} = $part_pkg->taxclass;
1953 my @taxes = qsearch( 'cust_main_county', \%taxhash );
1956 $taxhash{'taxclass'} = '';
1957 @taxes = qsearch( 'cust_main_county', \%taxhash );
1960 #one more try at a whole-country tax rate
1962 $taxhash{$_} = '' foreach qw( state county );
1963 @taxes = qsearch( 'cust_main_county', \%taxhash );
1966 # maybe eliminate this entirely, along with all the 0% records
1968 $dbh->rollback if $oldAutoCommit;
1970 "fatal: can't find tax rate for state/county/country/taxclass ".
1971 join('/', ( map $self->get("$prefix$_"),
1972 qw(state county country)
1974 $part_pkg->taxclass ). "\n";
1977 foreach my $tax ( @taxes ) {
1979 my $taxable_charged = 0;
1980 $taxable_charged += $setup
1981 unless $part_pkg->setuptax =~ /^Y$/i
1982 || $tax->setuptax =~ /^Y$/i;
1983 $taxable_charged += $recur
1984 unless $part_pkg->recurtax =~ /^Y$/i
1985 || $tax->recurtax =~ /^Y$/i;
1986 next unless $taxable_charged;
1988 if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
1989 #my ($mon,$year) = (localtime($sdate) )[4,5];
1990 my ($mon,$year) = (localtime( $sdate || $cust_bill->_date ) )[4,5];
1992 my $freq = $part_pkg->freq || 1;
1993 if ( $freq !~ /(\d+)$/ ) {
1994 $dbh->rollback if $oldAutoCommit;
1995 return "daily/weekly package definitions not (yet?)".
1996 " compatible with monthly tax exemptions";
1998 my $taxable_per_month =
1999 sprintf("%.2f", $taxable_charged / $freq );
2001 #call the whole thing off if this customer has any old
2002 #exemption records...
2003 my @cust_tax_exempt =
2004 qsearch( 'cust_tax_exempt' => { custnum=> $self->custnum } );
2005 if ( @cust_tax_exempt ) {
2006 $dbh->rollback if $oldAutoCommit;
2008 'this customer still has old-style tax exemption records; '.
2009 'run bin/fs-migrate-cust_tax_exempt?';
2012 foreach my $which_month ( 1 .. $freq ) {
2014 #maintain the new exemption table now
2017 FROM cust_tax_exempt_pkg
2018 LEFT JOIN cust_bill_pkg USING ( billpkgnum )
2019 LEFT JOIN cust_bill USING ( invnum )
2025 my $sth = dbh->prepare($sql) or do {
2026 $dbh->rollback if $oldAutoCommit;
2027 return "fatal: can't lookup exising exemption: ". dbh->errstr;
2035 $dbh->rollback if $oldAutoCommit;
2036 return "fatal: can't lookup exising exemption: ". dbh->errstr;
2038 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
2040 my $remaining_exemption =
2041 $tax->exempt_amount - $existing_exemption;
2042 if ( $remaining_exemption > 0 ) {
2043 my $addl = $remaining_exemption > $taxable_per_month
2044 ? $taxable_per_month
2045 : $remaining_exemption;
2046 $taxable_charged -= $addl;
2048 my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
2049 'billpkgnum' => $cust_bill_pkg->billpkgnum,
2050 'taxnum' => $tax->taxnum,
2051 'year' => 1900+$year,
2053 'amount' => sprintf("%.2f", $addl ),
2055 $error = $cust_tax_exempt_pkg->insert;
2057 $dbh->rollback if $oldAutoCommit;
2058 return "fatal: can't insert cust_tax_exempt_pkg: $error";
2060 } # if $remaining_exemption > 0
2064 #until ( $mon < 12 ) { $mon -= 12; $year++; }
2065 until ( $mon < 13 ) { $mon -= 12; $year++; }
2067 } #foreach $which_month
2069 } #if $tax->exempt_amount
2071 $taxable_charged = sprintf( "%.2f", $taxable_charged);
2073 #$tax += $taxable_charged * $cust_main_county->tax / 100
2074 $tax{ $tax->taxname || 'Tax' } +=
2075 $taxable_charged * $tax->tax / 100
2077 } #foreach my $tax ( @taxes )
2079 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
2081 } #if $setup != 0 || $recur != 0
2083 } #if $cust_pkg->modified
2085 } #foreach my $cust_pkg
2087 unless ( $cust_bill->cust_bill_pkg ) {
2088 $cust_bill->delete; #don't create an invoice w/o line items
2089 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2093 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
2095 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
2096 my $tax = sprintf("%.2f", $tax{$taxname} );
2097 $charged = sprintf( "%.2f", $charged+$tax );
2099 my $cust_bill_pkg = new FS::cust_bill_pkg ({
2100 'invnum' => $invnum,
2106 'itemdesc' => $taxname,
2108 $error = $cust_bill_pkg->insert;
2110 $dbh->rollback if $oldAutoCommit;
2111 return "can't create invoice line item for invoice #$invnum: $error";
2113 $total_setup += $tax;
2117 $cust_bill->charged( sprintf( "%.2f", $total_setup + $total_recur ) );
2118 $error = $cust_bill->replace;
2120 $dbh->rollback if $oldAutoCommit;
2121 return "can't update charged for invoice #$invnum: $error";
2124 foreach my $hook ( @precommit_hooks ) {
2126 &{$hook}; #($self) ?
2129 $dbh->rollback if $oldAutoCommit;
2130 return "$@ running precommit hook $hook\n";
2134 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2138 =item collect OPTIONS
2140 (Attempt to) collect money for this customer's outstanding invoices (see
2141 L<FS::cust_bill>). Usually used after the bill method.
2143 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
2144 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
2145 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
2147 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
2148 and the invoice events web interface.
2150 If there is an error, returns the error, otherwise returns false.
2152 Options are passed as name-value pairs.
2154 Currently available options are:
2156 invoice_time - Use this time when deciding when to print invoices and
2157 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>
2158 for conversion functions.
2160 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
2163 quiet - set true to surpress email card/ACH decline notices.
2165 freq - "1d" for the traditional, daily events (the default), or "1m" for the
2168 payby - allows for one time override of normal customer billing method
2173 my( $self, %options ) = @_;
2174 my $invoice_time = $options{'invoice_time'} || time;
2177 local $SIG{HUP} = 'IGNORE';
2178 local $SIG{INT} = 'IGNORE';
2179 local $SIG{QUIT} = 'IGNORE';
2180 local $SIG{TERM} = 'IGNORE';
2181 local $SIG{TSTP} = 'IGNORE';
2182 local $SIG{PIPE} = 'IGNORE';
2184 my $oldAutoCommit = $FS::UID::AutoCommit;
2185 local $FS::UID::AutoCommit = 0;
2188 $self->select_for_update; #mutex
2190 my $balance = $self->balance;
2191 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
2193 unless ( $balance > 0 ) { #redundant?????
2194 $dbh->rollback if $oldAutoCommit; #hmm
2198 if ( exists($options{'retry_card'}) ) {
2199 carp 'retry_card option passed to collect is deprecated; use retry';
2200 $options{'retry'} ||= $options{'retry_card'};
2202 if ( exists($options{'retry'}) && $options{'retry'} ) {
2203 my $error = $self->retry_realtime;
2205 $dbh->rollback if $oldAutoCommit;
2211 if ( defined $options{'freq'} && $options{'freq'} eq '1m' ) {
2212 $extra_sql = " AND freq = '1m' ";
2214 $extra_sql = " AND ( freq = '1d' OR freq IS NULL OR freq = '' ) ";
2217 foreach my $cust_bill ( $self->open_cust_bill ) {
2219 # don't try to charge for the same invoice if it's already in a batch
2220 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
2222 last if $self->balance <= 0;
2224 warn " invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")\n"
2227 foreach my $part_bill_event (
2228 sort { $a->seconds <=> $b->seconds
2229 || $a->weight <=> $b->weight
2230 || $a->eventpart <=> $b->eventpart }
2231 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
2232 && ! qsearch( 'cust_bill_event', {
2233 'invnum' => $cust_bill->invnum,
2234 'eventpart' => $_->eventpart,
2239 'table' => 'part_bill_event',
2240 'hashref' => { 'payby' => (exists($options{'payby'})
2244 'disabled' => '', },
2245 'extra_sql' => $extra_sql,
2249 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
2250 || $self->balance <= 0; # or if balance<=0
2252 warn " calling invoice event (". $part_bill_event->eventcode. ")\n"
2254 my $cust_main = $self; #for callback
2258 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
2259 local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
2260 $error = eval $part_bill_event->eventcode;
2264 my $statustext = '';
2268 } elsif ( $error ) {
2270 $statustext = $error;
2275 #add cust_bill_event
2276 my $cust_bill_event = new FS::cust_bill_event {
2277 'invnum' => $cust_bill->invnum,
2278 'eventpart' => $part_bill_event->eventpart,
2279 #'_date' => $invoice_time,
2281 'status' => $status,
2282 'statustext' => $statustext,
2284 $error = $cust_bill_event->insert;
2286 #$dbh->rollback if $oldAutoCommit;
2287 #return "error: $error";
2289 # gah, even with transactions.
2290 $dbh->commit if $oldAutoCommit; #well.
2291 my $e = 'WARNING: Event run but database not updated - '.
2292 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
2293 ', eventpart '. $part_bill_event->eventpart.
2304 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2309 =item retry_realtime
2311 Schedules realtime credit card / electronic check / LEC billing events for
2312 for retry. Useful if card information has changed or manual retry is desired.
2313 The 'collect' method must be called to actually retry the transaction.
2315 Implementation details: For each of this customer's open invoices, changes
2316 the status of the first "done" (with statustext error) realtime processing
2321 sub retry_realtime {
2324 local $SIG{HUP} = 'IGNORE';
2325 local $SIG{INT} = 'IGNORE';
2326 local $SIG{QUIT} = 'IGNORE';
2327 local $SIG{TERM} = 'IGNORE';
2328 local $SIG{TSTP} = 'IGNORE';
2329 local $SIG{PIPE} = 'IGNORE';
2331 my $oldAutoCommit = $FS::UID::AutoCommit;
2332 local $FS::UID::AutoCommit = 0;
2335 foreach my $cust_bill (
2336 grep { $_->cust_bill_event }
2337 $self->open_cust_bill
2339 my @cust_bill_event =
2340 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
2342 #$_->part_bill_event->plan eq 'realtime-card'
2343 $_->part_bill_event->eventcode =~
2344 /\$cust_bill\->realtime_(card|ach|lec)/
2345 && $_->status eq 'done'
2348 $cust_bill->cust_bill_event;
2349 next unless @cust_bill_event;
2350 my $error = $cust_bill_event[0]->retry;
2352 $dbh->rollback if $oldAutoCommit;
2353 return "error scheduling invoice event for retry: $error";
2358 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2363 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
2365 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
2366 via a Business::OnlinePayment realtime gateway. See
2367 L<http://420.am/business-onlinepayment> for supported gateways.
2369 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2371 Available options are: I<description>, I<invnum>, I<quiet>
2373 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2374 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2375 if set, will override the value from the customer record.
2377 I<description> is a free-text field passed to the gateway. It defaults to
2378 "Internet services".
2380 If an I<invnum> is specified, this payment (if successful) is applied to the
2381 specified invoice. If you don't specify an I<invnum> you might want to
2382 call the B<apply_payments> method.
2384 I<quiet> can be set true to surpress email decline notices.
2386 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
2391 my( $self, $method, $amount, %options ) = @_;
2393 warn "$me realtime_bop: $method $amount\n";
2394 warn " $_ => $options{$_}\n" foreach keys %options;
2397 $options{'description'} ||= 'Internet services';
2399 eval "use Business::OnlinePayment";
2402 my $payinfo = exists($options{'payinfo'})
2403 ? $options{'payinfo'}
2411 if ( $options{'invnum'} ) {
2412 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
2413 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
2415 map { $_->part_pkg->taxclass }
2417 map { $_->cust_pkg }
2418 $cust_bill->cust_bill_pkg;
2419 unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
2420 #different taxclasses
2421 $taxclass = $taxclasses[0];
2425 #look for an agent gateway override first
2427 if ( $method eq 'CC' ) {
2428 $cardtype = cardtype($payinfo);
2429 } elsif ( $method eq 'ECHECK' ) {
2432 $cardtype = $method;
2436 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2437 cardtype => $cardtype,
2438 taxclass => $taxclass, } )
2439 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2441 taxclass => $taxclass, } )
2442 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2443 cardtype => $cardtype,
2445 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2447 taxclass => '', } );
2449 my $payment_gateway = '';
2450 my( $processor, $login, $password, $action, @bop_options );
2451 if ( $override ) { #use a payment gateway override
2453 $payment_gateway = $override->payment_gateway;
2455 $processor = $payment_gateway->gateway_module;
2456 $login = $payment_gateway->gateway_username;
2457 $password = $payment_gateway->gateway_password;
2458 $action = $payment_gateway->gateway_action;
2459 @bop_options = $payment_gateway->options;
2461 } else { #use the standard settings from the config
2463 ( $processor, $login, $password, $action, @bop_options ) =
2464 $self->default_payment_gateway($method);
2472 my $address = exists($options{'address1'})
2473 ? $options{'address1'}
2475 my $address2 = exists($options{'address2'})
2476 ? $options{'address2'}
2478 $address .= ", ". $address2 if length($address2);
2480 my $o_payname = exists($options{'payname'})
2481 ? $options{'payname'}
2483 my($payname, $payfirst, $paylast);
2484 if ( $o_payname && $method ne 'ECHECK' ) {
2485 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2486 or return "Illegal payname $payname";
2487 ($payfirst, $paylast) = ($1, $2);
2489 $payfirst = $self->getfield('first');
2490 $paylast = $self->getfield('last');
2491 $payname = "$payfirst $paylast";
2494 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
2495 if ( $conf->exists('emailinvoiceauto')
2496 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
2497 push @invoicing_list, $self->all_emails;
2500 my $email = ($conf->exists('business-onlinepayment-email-override'))
2501 ? $conf->config('business-onlinepayment-email-override')
2502 : $invoicing_list[0];
2506 my $payip = exists($options{'payip'})
2509 $content{customer_ip} = $payip
2512 if ( $method eq 'CC' ) {
2514 $content{card_number} = $payinfo;
2515 my $paydate = exists($options{'paydate'})
2516 ? $options{'paydate'}
2518 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2519 $content{expiration} = "$2/$1";
2521 my $paycvv = exists($options{'paycvv'})
2522 ? $options{'paycvv'}
2524 $content{cvv2} = $self->paycvv
2527 my $paystart_month = exists($options{'paystart_month'})
2528 ? $options{'paystart_month'}
2529 : $self->paystart_month;
2531 my $paystart_year = exists($options{'paystart_year'})
2532 ? $options{'paystart_year'}
2533 : $self->paystart_year;
2535 $content{card_start} = "$paystart_month/$paystart_year"
2536 if $paystart_month && $paystart_year;
2538 my $payissue = exists($options{'payissue'})
2539 ? $options{'payissue'}
2541 $content{issue_number} = $payissue if $payissue;
2543 $content{recurring_billing} = 'YES'
2544 if qsearch('cust_pay', { 'custnum' => $self->custnum,
2546 'payinfo' => $payinfo,
2549 } elsif ( $method eq 'ECHECK' ) {
2550 ( $content{account_number}, $content{routing_code} ) =
2551 split('@', $payinfo);
2552 $content{bank_name} = $o_payname;
2553 $content{account_type} = 'CHECKING';
2554 $content{account_name} = $payname;
2555 $content{customer_org} = $self->company ? 'B' : 'I';
2556 $content{customer_ssn} = exists($options{'ss'})
2559 } elsif ( $method eq 'LEC' ) {
2560 $content{phone} = $payinfo;
2564 # run transaction(s)
2567 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
2569 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
2570 $transaction->content(
2573 'password' => $password,
2574 'action' => $action1,
2575 'description' => $options{'description'},
2576 'amount' => $amount,
2577 'invoice_number' => $options{'invnum'},
2578 'customer_id' => $self->custnum,
2579 'last_name' => $paylast,
2580 'first_name' => $payfirst,
2582 'address' => $address,
2583 'city' => ( exists($options{'city'})
2586 'state' => ( exists($options{'state'})
2589 'zip' => ( exists($options{'zip'})
2592 'country' => ( exists($options{'country'})
2593 ? $options{'country'}
2595 'referer' => 'http://cleanwhisker.420.am/',
2597 'phone' => $self->daytime || $self->night,
2600 $transaction->submit();
2602 if ( $transaction->is_success() && $action2 ) {
2603 my $auth = $transaction->authorization;
2604 my $ordernum = $transaction->can('order_number')
2605 ? $transaction->order_number
2609 new Business::OnlinePayment( $processor, @bop_options );
2616 password => $password,
2617 order_number => $ordernum,
2619 authorization => $auth,
2620 description => $options{'description'},
2623 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
2624 transaction_sequence_num local_transaction_date
2625 local_transaction_time AVS_result_code )) {
2626 $capture{$field} = $transaction->$field() if $transaction->can($field);
2629 $capture->content( %capture );
2633 unless ( $capture->is_success ) {
2634 my $e = "Authorization successful but capture failed, custnum #".
2635 $self->custnum. ': '. $capture->result_code.
2636 ": ". $capture->error_message;
2644 # remove paycvv after initial transaction
2647 #false laziness w/misc/process/payment.cgi - check both to make sure working
2649 if ( defined $self->dbdef_table->column('paycvv')
2650 && length($self->paycvv)
2651 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
2653 my $error = $self->remove_cvv;
2655 warn "WARNING: error removing cvv: $error\n";
2663 if ( $transaction->is_success() ) {
2665 my %method2payby = (
2672 if ( $payment_gateway ) { # agent override
2673 $paybatch = $payment_gateway->gatewaynum. '-';
2676 $paybatch .= "$processor:". $transaction->authorization;
2678 $paybatch .= ':'. $transaction->order_number
2679 if $transaction->can('order_number')
2680 && length($transaction->order_number);
2682 my $cust_pay = new FS::cust_pay ( {
2683 'custnum' => $self->custnum,
2684 'invnum' => $options{'invnum'},
2687 'payby' => $method2payby{$method},
2688 'payinfo' => $payinfo,
2689 'paybatch' => $paybatch,
2691 my $error = $cust_pay->insert;
2693 $cust_pay->invnum(''); #try again with no specific invnum
2694 my $error2 = $cust_pay->insert;
2696 # gah, even with transactions.
2697 my $e = 'WARNING: Card/ACH debited but database not updated - '.
2698 "error inserting payment ($processor): $error2".
2699 " (previously tried insert with invnum #$options{'invnum'}" .
2705 return ''; #no error
2709 my $perror = "$processor error: ". $transaction->error_message;
2711 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
2712 && $conf->exists('emaildecline')
2713 && grep { $_ ne 'POST' } $self->invoicing_list
2714 && ! grep { $transaction->error_message =~ /$_/ }
2715 $conf->config('emaildecline-exclude')
2717 my @templ = $conf->config('declinetemplate');
2718 my $template = new Text::Template (
2720 SOURCE => [ map "$_\n", @templ ],
2721 ) or return "($perror) can't create template: $Text::Template::ERROR";
2722 $template->compile()
2723 or return "($perror) can't compile template: $Text::Template::ERROR";
2725 my $templ_hash = { error => $transaction->error_message };
2727 my $error = send_email(
2728 'from' => $conf->config('invoice_from'),
2729 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
2730 'subject' => 'Your payment could not be processed',
2731 'body' => [ $template->fill_in(HASH => $templ_hash) ],
2734 $perror .= " (also received error sending decline notification: $error)"
2744 =item default_payment_gateway
2748 sub default_payment_gateway {
2749 my( $self, $method ) = @_;
2751 die "Real-time processing not enabled\n"
2752 unless $conf->exists('business-onlinepayment');
2755 my $bop_config = 'business-onlinepayment';
2756 $bop_config .= '-ach'
2757 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2758 my ( $processor, $login, $password, $action, @bop_options ) =
2759 $conf->config($bop_config);
2760 $action ||= 'normal authorization';
2761 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2762 die "No real-time processor is enabled - ".
2763 "did you set the business-onlinepayment configuration value?\n"
2766 ( $processor, $login, $password, $action, @bop_options )
2771 Removes the I<paycvv> field from the database directly.
2773 If there is an error, returns the error, otherwise returns false.
2779 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2780 or return dbh->errstr;
2781 $sth->execute($self->custnum)
2782 or return $sth->errstr;
2787 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
2789 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
2790 via a Business::OnlinePayment realtime gateway. See
2791 L<http://420.am/business-onlinepayment> for supported gateways.
2793 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2795 Available options are: I<amount>, I<reason>, I<paynum>
2797 Most gateways require a reference to an original payment transaction to refund,
2798 so you probably need to specify a I<paynum>.
2800 I<amount> defaults to the original amount of the payment if not specified.
2802 I<reason> specifies a reason for the refund.
2804 Implementation note: If I<amount> is unspecified or equal to the amount of the
2805 orignal payment, first an attempt is made to "void" the transaction via
2806 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2807 the normal attempt is made to "refund" ("credit") the transaction via the
2808 gateway is attempted.
2810 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2811 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2812 #if set, will override the value from the customer record.
2814 #If an I<invnum> is specified, this payment (if successful) is applied to the
2815 #specified invoice. If you don't specify an I<invnum> you might want to
2816 #call the B<apply_payments> method.
2820 #some false laziness w/realtime_bop, not enough to make it worth merging
2821 #but some useful small subs should be pulled out
2822 sub realtime_refund_bop {
2823 my( $self, $method, %options ) = @_;
2825 warn "$me realtime_refund_bop: $method refund\n";
2826 warn " $_ => $options{$_}\n" foreach keys %options;
2829 eval "use Business::OnlinePayment";
2833 # look up the original payment and optionally a gateway for that payment
2837 my $amount = $options{'amount'};
2839 my( $processor, $login, $password, @bop_options ) ;
2840 my( $auth, $order_number ) = ( '', '', '' );
2842 if ( $options{'paynum'} ) {
2844 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
2845 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2846 or return "Unknown paynum $options{'paynum'}";
2847 $amount ||= $cust_pay->paid;
2849 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-]*)(:([\w\-]+))?$/
2850 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2851 $cust_pay->paybatch;
2852 my $gatewaynum = '';
2853 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
2855 if ( $gatewaynum ) { #gateway for the payment to be refunded
2857 my $payment_gateway =
2858 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
2859 die "payment gateway $gatewaynum not found"
2860 unless $payment_gateway;
2862 $processor = $payment_gateway->gateway_module;
2863 $login = $payment_gateway->gateway_username;
2864 $password = $payment_gateway->gateway_password;
2865 @bop_options = $payment_gateway->options;
2867 } else { #try the default gateway
2869 my( $conf_processor, $unused_action );
2870 ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
2871 $self->default_payment_gateway($method);
2873 return "processor of payment $options{'paynum'} $processor does not".
2874 " match default processor $conf_processor"
2875 unless $processor eq $conf_processor;
2880 } else { # didn't specify a paynum, so look for agent gateway overrides
2881 # like a normal transaction
2884 if ( $method eq 'CC' ) {
2885 $cardtype = cardtype($self->payinfo);
2886 } elsif ( $method eq 'ECHECK' ) {
2889 $cardtype = $method;
2892 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2893 cardtype => $cardtype,
2895 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2897 taxclass => '', } );
2899 if ( $override ) { #use a payment gateway override
2901 my $payment_gateway = $override->payment_gateway;
2903 $processor = $payment_gateway->gateway_module;
2904 $login = $payment_gateway->gateway_username;
2905 $password = $payment_gateway->gateway_password;
2906 #$action = $payment_gateway->gateway_action;
2907 @bop_options = $payment_gateway->options;
2909 } else { #use the standard settings from the config
2912 ( $processor, $login, $password, $unused_action, @bop_options ) =
2913 $self->default_payment_gateway($method);
2918 return "neither amount nor paynum specified" unless $amount;
2923 'password' => $password,
2924 'order_number' => $order_number,
2925 'amount' => $amount,
2926 'referer' => 'http://cleanwhisker.420.am/',
2928 $content{authorization} = $auth
2929 if length($auth); #echeck/ACH transactions have an order # but no auth
2930 #(at least with authorize.net)
2932 #first try void if applicable
2933 if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2934 warn " attempting void\n" if $DEBUG > 1;
2935 my $void = new Business::OnlinePayment( $processor, @bop_options );
2936 $void->content( 'action' => 'void', %content );
2938 if ( $void->is_success ) {
2939 my $error = $cust_pay->void($options{'reason'});
2941 # gah, even with transactions.
2942 my $e = 'WARNING: Card/ACH voided but database not updated - '.
2943 "error voiding payment: $error";
2947 warn " void successful\n" if $DEBUG > 1;
2952 warn " void unsuccessful, trying refund\n"
2956 my $address = $self->address1;
2957 $address .= ", ". $self->address2 if $self->address2;
2959 my($payname, $payfirst, $paylast);
2960 if ( $self->payname && $method ne 'ECHECK' ) {
2961 $payname = $self->payname;
2962 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2963 or return "Illegal payname $payname";
2964 ($payfirst, $paylast) = ($1, $2);
2966 $payfirst = $self->getfield('first');
2967 $paylast = $self->getfield('last');
2968 $payname = "$payfirst $paylast";
2971 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
2972 if ( $conf->exists('emailinvoiceauto')
2973 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
2974 push @invoicing_list, $self->all_emails;
2977 my $email = ($conf->exists('business-onlinepayment-email-override'))
2978 ? $conf->config('business-onlinepayment-email-override')
2979 : $invoicing_list[0];
2981 my $payip = exists($options{'payip'})
2984 $content{customer_ip} = $payip
2988 if ( $method eq 'CC' ) {
2991 $content{card_number} = $payinfo = $cust_pay->payinfo;
2992 #$self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2993 #$content{expiration} = "$2/$1";
2995 $content{card_number} = $payinfo = $self->payinfo;
2996 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2997 $content{expiration} = "$2/$1";
3000 } elsif ( $method eq 'ECHECK' ) {
3001 ( $content{account_number}, $content{routing_code} ) =
3002 split('@', $payinfo = $self->payinfo);
3003 $content{bank_name} = $self->payname;
3004 $content{account_type} = 'CHECKING';
3005 $content{account_name} = $payname;
3006 $content{customer_org} = $self->company ? 'B' : 'I';
3007 $content{customer_ssn} = $self->ss;
3008 } elsif ( $method eq 'LEC' ) {
3009 $content{phone} = $payinfo = $self->payinfo;
3013 my $refund = new Business::OnlinePayment( $processor, @bop_options );
3014 my %sub_content = $refund->content(
3015 'action' => 'credit',
3016 'customer_id' => $self->custnum,
3017 'last_name' => $paylast,
3018 'first_name' => $payfirst,
3020 'address' => $address,
3021 'city' => $self->city,
3022 'state' => $self->state,
3023 'zip' => $self->zip,
3024 'country' => $self->country,
3026 'phone' => $self->daytime || $self->night,
3029 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
3033 return "$processor error: ". $refund->error_message
3034 unless $refund->is_success();
3036 my %method2payby = (
3042 my $paybatch = "$processor:". $refund->authorization;
3043 $paybatch .= ':'. $refund->order_number
3044 if $refund->can('order_number') && $refund->order_number;
3046 while ( $cust_pay && $cust_pay->unappled < $amount ) {
3047 my @cust_bill_pay = $cust_pay->cust_bill_pay;
3048 last unless @cust_bill_pay;
3049 my $cust_bill_pay = pop @cust_bill_pay;
3050 my $error = $cust_bill_pay->delete;
3054 my $cust_refund = new FS::cust_refund ( {
3055 'custnum' => $self->custnum,
3056 'paynum' => $options{'paynum'},
3057 'refund' => $amount,
3059 'payby' => $method2payby{$method},
3060 'payinfo' => $payinfo,
3061 'paybatch' => $paybatch,
3062 'reason' => $options{'reason'} || 'card or ACH refund',
3064 my $error = $cust_refund->insert;
3066 $cust_refund->paynum(''); #try again with no specific paynum
3067 my $error2 = $cust_refund->insert;
3069 # gah, even with transactions.
3070 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
3071 "error inserting refund ($processor): $error2".
3072 " (previously tried insert with paynum #$options{'paynum'}" .
3085 Returns the total owed for this customer on all invoices
3086 (see L<FS::cust_bill/owed>).
3092 $self->total_owed_date(2145859200); #12/31/2037
3095 =item total_owed_date TIME
3097 Returns the total owed for this customer on all invoices with date earlier than
3098 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
3099 see L<Time::Local> and L<Date::Parse> for conversion functions.
3103 sub total_owed_date {
3107 foreach my $cust_bill (
3108 grep { $_->_date <= $time }
3109 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3111 $total_bill += $cust_bill->owed;
3113 sprintf( "%.2f", $total_bill );
3116 =item apply_credits OPTION => VALUE ...
3118 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
3119 to outstanding invoice balances in chronological order (or reverse
3120 chronological order if the I<order> option is set to B<newest>) and returns the
3121 value of any remaining unapplied credits available for refund (see
3122 L<FS::cust_refund>).
3130 return 0 unless $self->total_credited;
3132 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
3133 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
3135 my @invoices = $self->open_cust_bill;
3136 @invoices = sort { $b->_date <=> $a->_date } @invoices
3137 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
3140 foreach my $cust_bill ( @invoices ) {
3143 if ( !defined($credit) || $credit->credited == 0) {
3144 $credit = pop @credits or last;
3147 if ($cust_bill->owed >= $credit->credited) {
3148 $amount=$credit->credited;
3150 $amount=$cust_bill->owed;
3153 my $cust_credit_bill = new FS::cust_credit_bill ( {
3154 'crednum' => $credit->crednum,
3155 'invnum' => $cust_bill->invnum,
3156 'amount' => $amount,
3158 my $error = $cust_credit_bill->insert;
3159 die $error if $error;
3161 redo if ($cust_bill->owed > 0);
3165 return $self->total_credited;
3168 =item apply_payments
3170 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
3171 to outstanding invoice balances in chronological order.
3173 #and returns the value of any remaining unapplied payments.
3177 sub apply_payments {
3182 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
3183 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
3185 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
3186 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
3190 foreach my $cust_bill ( @invoices ) {
3193 if ( !defined($payment) || $payment->unapplied == 0 ) {
3194 $payment = pop @payments or last;
3197 if ( $cust_bill->owed >= $payment->unapplied ) {
3198 $amount = $payment->unapplied;
3200 $amount = $cust_bill->owed;
3203 my $cust_bill_pay = new FS::cust_bill_pay ( {
3204 'paynum' => $payment->paynum,
3205 'invnum' => $cust_bill->invnum,
3206 'amount' => $amount,
3208 my $error = $cust_bill_pay->insert;
3209 die $error if $error;
3211 redo if ( $cust_bill->owed > 0);
3215 return $self->total_unapplied_payments;
3218 =item total_credited
3220 Returns the total outstanding credit (see L<FS::cust_credit>) for this
3221 customer. See L<FS::cust_credit/credited>.
3225 sub total_credited {
3227 my $total_credit = 0;
3228 foreach my $cust_credit ( qsearch('cust_credit', {
3229 'custnum' => $self->custnum,
3231 $total_credit += $cust_credit->credited;
3233 sprintf( "%.2f", $total_credit );
3236 =item total_unapplied_payments
3238 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
3239 See L<FS::cust_pay/unapplied>.
3243 sub total_unapplied_payments {
3245 my $total_unapplied = 0;
3246 foreach my $cust_pay ( qsearch('cust_pay', {
3247 'custnum' => $self->custnum,
3249 $total_unapplied += $cust_pay->unapplied;
3251 sprintf( "%.2f", $total_unapplied );
3256 Returns the balance for this customer (total_owed minus total_credited
3257 minus total_unapplied_payments).
3264 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
3268 =item balance_date TIME
3270 Returns the balance for this customer, only considering invoices with date
3271 earlier than TIME (total_owed_date minus total_credited minus
3272 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
3273 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
3282 $self->total_owed_date($time)
3283 - $self->total_credited
3284 - $self->total_unapplied_payments
3288 =item in_transit_payments
3290 Returns the total of requests for payments for this customer pending in
3291 batches in transit to the bank. See L<FS::pay_batch> and L<FS::cust_pay_batch>
3295 sub in_transit_payments {
3297 my $in_transit_payments = 0;
3298 foreach my $pay_batch ( qsearch('pay_batch', {
3301 foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
3302 'batchnum' => $pay_batch->batchnum,
3303 'custnum' => $self->custnum,
3305 $in_transit_payments += $cust_pay_batch->amount;
3308 sprintf( "%.2f", $in_transit_payments );
3311 =item paydate_monthyear
3313 Returns a two-element list consisting of the month and year of this customer's
3314 paydate (credit card expiration date for CARD customers)
3318 sub paydate_monthyear {
3320 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
3322 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
3329 =item payinfo_masked
3331 Returns a "masked" payinfo field appropriate to the payment type. Masked characters are replaced by 'x'es. Use this to display publicly accessable account Information.
3333 Credit Cards - Mask all but the last four characters.
3334 Checks - Mask all but last 2 of account number and bank routing number.
3335 Others - Do nothing, return the unmasked string.
3339 sub payinfo_masked {
3341 return $self->paymask;
3344 =item invoicing_list [ ARRAYREF ]
3346 If an arguement is given, sets these email addresses as invoice recipients
3347 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
3348 (except as warnings), so use check_invoicing_list first.
3350 Returns a list of email addresses (with svcnum entries expanded).
3352 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
3353 check it without disturbing anything by passing nothing.
3355 This interface may change in the future.
3359 sub invoicing_list {
3360 my( $self, $arrayref ) = @_;
3363 my @cust_main_invoice;
3364 if ( $self->custnum ) {
3365 @cust_main_invoice =
3366 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3368 @cust_main_invoice = ();
3370 foreach my $cust_main_invoice ( @cust_main_invoice ) {
3371 #warn $cust_main_invoice->destnum;
3372 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
3373 #warn $cust_main_invoice->destnum;
3374 my $error = $cust_main_invoice->delete;
3375 warn $error if $error;
3378 if ( $self->custnum ) {
3379 @cust_main_invoice =
3380 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3382 @cust_main_invoice = ();
3384 my %seen = map { $_->address => 1 } @cust_main_invoice;
3385 foreach my $address ( @{$arrayref} ) {
3386 next if exists $seen{$address} && $seen{$address};
3387 $seen{$address} = 1;
3388 my $cust_main_invoice = new FS::cust_main_invoice ( {
3389 'custnum' => $self->custnum,
3392 my $error = $cust_main_invoice->insert;
3393 warn $error if $error;
3397 if ( $self->custnum ) {
3399 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3406 =item check_invoicing_list ARRAYREF
3408 Checks these arguements as valid input for the invoicing_list method. If there
3409 is an error, returns the error, otherwise returns false.
3413 sub check_invoicing_list {
3414 my( $self, $arrayref ) = @_;
3415 foreach my $address ( @{$arrayref} ) {
3417 if ($address eq 'FAX' and $self->getfield('fax') eq '') {
3418 return 'Can\'t add FAX invoice destination with a blank FAX number.';
3421 my $cust_main_invoice = new FS::cust_main_invoice ( {
3422 'custnum' => $self->custnum,
3425 my $error = $self->custnum
3426 ? $cust_main_invoice->check
3427 : $cust_main_invoice->checkdest
3429 return $error if $error;
3434 =item set_default_invoicing_list
3436 Sets the invoicing list to all accounts associated with this customer,
3437 overwriting any previous invoicing list.
3441 sub set_default_invoicing_list {
3443 $self->invoicing_list($self->all_emails);
3448 Returns the email addresses of all accounts provisioned for this customer.
3455 foreach my $cust_pkg ( $self->all_pkgs ) {
3456 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
3458 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3459 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3461 $list{$_}=1 foreach map { $_->email } @svc_acct;
3466 =item invoicing_list_addpost
3468 Adds postal invoicing to this customer. If this customer is already configured
3469 to receive postal invoices, does nothing.
3473 sub invoicing_list_addpost {
3475 return if grep { $_ eq 'POST' } $self->invoicing_list;
3476 my @invoicing_list = $self->invoicing_list;
3477 push @invoicing_list, 'POST';
3478 $self->invoicing_list(\@invoicing_list);
3481 =item invoicing_list_emailonly
3483 Returns the list of email invoice recipients (invoicing_list without non-email
3484 destinations such as POST and FAX).
3488 sub invoicing_list_emailonly {
3490 grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
3493 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3495 Returns an array of customers referred by this customer (referral_custnum set
3496 to this custnum). If DEPTH is given, recurses up to the given depth, returning
3497 customers referred by customers referred by this customer and so on, inclusive.
3498 The default behavior is DEPTH 1 (no recursion).
3502 sub referral_cust_main {
3504 my $depth = @_ ? shift : 1;
3505 my $exclude = @_ ? shift : {};
3508 map { $exclude->{$_->custnum}++; $_; }
3509 grep { ! $exclude->{ $_->custnum } }
3510 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3514 map { $_->referral_cust_main($depth-1, $exclude) }
3521 =item referral_cust_main_ncancelled
3523 Same as referral_cust_main, except only returns customers with uncancelled
3528 sub referral_cust_main_ncancelled {
3530 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3533 =item referral_cust_pkg [ DEPTH ]
3535 Like referral_cust_main, except returns a flat list of all unsuspended (and
3536 uncancelled) packages for each customer. The number of items in this list may
3537 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3541 sub referral_cust_pkg {
3543 my $depth = @_ ? shift : 1;
3545 map { $_->unsuspended_pkgs }
3546 grep { $_->unsuspended_pkgs }
3547 $self->referral_cust_main($depth);
3550 =item referring_cust_main
3552 Returns the single cust_main record for the customer who referred this customer
3553 (referral_custnum), or false.
3557 sub referring_cust_main {
3559 return '' unless $self->referral_custnum;
3560 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3563 =item credit AMOUNT, REASON
3565 Applies a credit to this customer. If there is an error, returns the error,
3566 otherwise returns false.
3571 my( $self, $amount, $reason ) = @_;
3572 my $cust_credit = new FS::cust_credit {
3573 'custnum' => $self->custnum,
3574 'amount' => $amount,
3575 'reason' => $reason,
3577 $cust_credit->insert;
3580 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3582 Creates a one-time charge for this customer. If there is an error, returns
3583 the error, otherwise returns false.
3588 my ( $self, $amount ) = ( shift, shift );
3589 my $pkg = @_ ? shift : 'One-time charge';
3590 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
3591 my $taxclass = @_ ? shift : '';
3593 local $SIG{HUP} = 'IGNORE';
3594 local $SIG{INT} = 'IGNORE';
3595 local $SIG{QUIT} = 'IGNORE';
3596 local $SIG{TERM} = 'IGNORE';
3597 local $SIG{TSTP} = 'IGNORE';
3598 local $SIG{PIPE} = 'IGNORE';
3600 my $oldAutoCommit = $FS::UID::AutoCommit;
3601 local $FS::UID::AutoCommit = 0;
3604 my $part_pkg = new FS::part_pkg ( {
3606 'comment' => $comment,
3607 #'setup' => $amount,
3610 'plandata' => "setup_fee=$amount",
3613 'taxclass' => $taxclass,
3616 my $error = $part_pkg->insert;
3618 $dbh->rollback if $oldAutoCommit;
3622 my $pkgpart = $part_pkg->pkgpart;
3623 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3624 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3625 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3626 $error = $type_pkgs->insert;
3628 $dbh->rollback if $oldAutoCommit;
3633 my $cust_pkg = new FS::cust_pkg ( {
3634 'custnum' => $self->custnum,
3635 'pkgpart' => $pkgpart,
3638 $error = $cust_pkg->insert;
3640 $dbh->rollback if $oldAutoCommit;
3644 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3651 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3657 sort { $a->_date <=> $b->_date }
3658 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3661 =item open_cust_bill
3663 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3668 sub open_cust_bill {
3670 grep { $_->owed > 0 } $self->cust_bill;
3675 Returns all the credits (see L<FS::cust_credit>) for this customer.
3681 sort { $a->_date <=> $b->_date }
3682 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3687 Returns all the payments (see L<FS::cust_pay>) for this customer.
3693 sort { $a->_date <=> $b->_date }
3694 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
3699 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3705 sort { $a->_date <=> $b->_date }
3706 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3712 Returns all the refunds (see L<FS::cust_refund>) for this customer.
3718 sort { $a->_date <=> $b->_date }
3719 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
3722 =item select_for_update
3724 Selects this record with the SQL "FOR UPDATE" command. This can be useful as
3729 sub select_for_update {
3731 qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
3736 Returns a name string for this customer, either "Company (Last, First)" or
3743 my $name = $self->contact;
3744 $name = $self->company. " ($name)" if $self->company;
3750 Returns a name string for this (service/shipping) contact, either
3751 "Company (Last, First)" or "Last, First".
3757 if ( $self->get('ship_last') ) {
3758 my $name = $self->ship_contact;
3759 $name = $self->ship_company. " ($name)" if $self->ship_company;
3768 Returns this customer's full (billing) contact name only, "Last, First"
3774 $self->get('last'). ', '. $self->first;
3779 Returns this customer's full (shipping) contact name only, "Last, First"
3785 $self->get('ship_last')
3786 ? $self->get('ship_last'). ', '. $self->ship_first
3792 Returns this customer's full country name
3798 code2country($self->country);
3803 Returns a status string for this customer, currently:
3807 =item prospect - No packages have ever been ordered
3809 =item active - One or more recurring packages is active
3811 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
3813 =item suspended - All non-cancelled recurring packages are suspended
3815 =item cancelled - All recurring packages are cancelled
3823 for my $status (qw( prospect active inactive suspended cancelled )) {
3824 my $method = $status.'_sql';
3825 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
3826 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
3827 $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
3828 return $status if $sth->fetchrow_arrayref->[0];
3834 Returns a hex triplet color string for this customer's status.
3838 use vars qw(%statuscolor);
3840 'prospect' => '7e0079', #'000000', #black? naw, purple
3841 'active' => '00CC00', #green
3842 'inactive' => '0000CC', #blue
3843 'suspended' => 'FF9900', #yellow
3844 'cancelled' => 'FF0000', #red
3849 $statuscolor{$self->status};
3854 =head1 CLASS METHODS
3860 Returns an SQL expression identifying prospective cust_main records (customers
3861 with no packages ever ordered)
3865 use vars qw($select_count_pkgs);
3866 $select_count_pkgs =
3867 "SELECT COUNT(*) FROM cust_pkg
3868 WHERE cust_pkg.custnum = cust_main.custnum";
3870 sub select_count_pkgs_sql {
3874 sub prospect_sql { "
3875 0 = ( $select_count_pkgs )
3880 Returns an SQL expression identifying active cust_main records (customers with
3881 no active recurring packages, but otherwise unsuspended/uncancelled).
3886 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
3892 Returns an SQL expression identifying inactive cust_main records (customers with
3893 active recurring packages).
3897 sub inactive_sql { "
3898 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
3900 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
3906 Returns an SQL expression identifying suspended cust_main records.
3911 sub suspended_sql { susp_sql(@_); }
3913 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
3915 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
3921 Returns an SQL expression identifying cancelled cust_main records.
3925 sub cancelled_sql { cancel_sql(@_); }
3928 my $recurring_sql = FS::cust_pkg->recurring_sql;
3929 #my $recurring_sql = "
3930 # '0' != ( select freq from part_pkg
3931 # where cust_pkg.pkgpart = part_pkg.pkgpart )
3935 0 < ( $select_count_pkgs )
3936 AND 0 = ( $select_count_pkgs AND $recurring_sql
3937 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
3943 =item uncancelled_sql
3945 Returns an SQL expression identifying un-cancelled cust_main records.
3949 sub uncancelled_sql { uncancel_sql(@_); }
3950 sub uncancel_sql { "
3951 ( 0 < ( $select_count_pkgs
3952 AND ( cust_pkg.cancel IS NULL
3953 OR cust_pkg.cancel = 0
3956 OR 0 = ( $select_count_pkgs )
3960 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
3962 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
3963 records. Currently, I<first>, I<last> and/or I<company> may be specified (the
3964 appropriate ship_ field is also searched).
3966 Additional options are the same as FS::Record::qsearch
3971 my( $self, $fuzzy, $hash, @opt) = @_;
3976 check_and_rebuild_fuzzyfiles();
3977 foreach my $field ( keys %$fuzzy ) {
3979 $match{$_}=1 foreach ( amatch( $fuzzy->{$field},
3981 @{ $self->all_X($field) }
3986 foreach ( keys %match ) {
3987 push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
3988 push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
3991 push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
3994 # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
3996 @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main;
4008 =item smart_search OPTION => VALUE ...
4010 Accepts the following options: I<search>, the string to search for. The string
4011 will be searched for as a customer number, phone number, name or company name,
4012 first searching for an exact match then fuzzy and substring matches (in some
4013 cases - see the source code for the exact heuristics used).
4015 Any additional options treated as an additional qualifier on the search
4018 Returns a (possibly empty) array of FS::cust_main objects.
4025 #here is the agent virtualization
4026 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
4030 my $search = delete $options{'search'};
4031 ( my $alphanum_search = $search ) =~ s/\W//g;
4033 if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search
4035 #false laziness w/Record::ut_phone
4036 my $phonen = "$1-$2-$3";
4037 $phonen .= " x$4" if $4;
4039 push @cust_main, qsearch( {
4040 'table' => 'cust_main',
4041 'hashref' => { %options },
4042 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
4044 join(' OR ', map "$_ = '$phonen'",
4045 qw( daytime night fax
4046 ship_daytime ship_night ship_fax )
4049 " AND $agentnums_sql", #agent virtualization
4052 unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
4053 #try looking for matches with extensions unless one was specified
4055 push @cust_main, qsearch( {
4056 'table' => 'cust_main',
4057 'hashref' => { %options },
4058 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
4060 join(' OR ', map "$_ LIKE '$phonen\%'",
4062 ship_daytime ship_night )
4065 " AND $agentnums_sql", #agent virtualization
4070 } elsif ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
4072 push @cust_main, qsearch( {
4073 'table' => 'cust_main',
4074 'hashref' => { 'custnum' => $1, %options },
4075 'extra_sql' => " AND $agentnums_sql", #agent virtualization
4078 } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
4080 my($company, $last, $first) = ( $1, $2, $3 );
4082 # "Company (Last, First)"
4083 #this is probably something a browser remembered,
4084 #so just do an exact search
4086 foreach my $prefix ( '', 'ship_' ) {
4087 push @cust_main, qsearch( {
4088 'table' => 'cust_main',
4089 'hashref' => { $prefix.'first' => $first,
4090 $prefix.'last' => $last,
4091 $prefix.'company' => $company,
4094 'extra_sql' => " AND $agentnums_sql",
4098 } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
4099 # try (ship_){last,company}
4103 # # remove "(Last, First)" in "Company (Last, First)", otherwise the
4104 # # full strings the browser remembers won't work
4105 # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name
4107 use Lingua::EN::NameParse;
4108 my $NameParse = new Lingua::EN::NameParse(
4110 allow_reversed => 1,
4113 my($last, $first) = ( '', '' );
4114 #maybe disable this too and just rely on NameParse?
4115 if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First
4117 ($last, $first) = ( $1, $2 );
4119 #} elsif ( $value =~ /^(.+)\s+(.+)$/ ) {
4120 } elsif ( ! $NameParse->parse($value) ) {
4122 my %name = $NameParse->components;
4123 $first = $name{'given_name_1'};
4124 $last = $name{'surname_1'};
4128 if ( $first && $last ) {
4130 my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) );
4133 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
4135 ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
4136 OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
4139 push @cust_main, qsearch( {
4140 'table' => 'cust_main',
4141 'hashref' => \%options,
4142 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
4145 # or it just be something that was typed in... (try that in a sec)
4149 my $q_value = dbh->quote($value);
4152 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
4153 $sql .= " ( LOWER(last) = $q_value
4154 OR LOWER(company) = $q_value
4155 OR LOWER(ship_last) = $q_value
4156 OR LOWER(ship_company) = $q_value
4159 push @cust_main, qsearch( {
4160 'table' => 'cust_main',
4161 'hashref' => \%options,
4162 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
4165 unless ( @cust_main ) { #no exact match, trying substring/fuzzy
4167 #still some false laziness w/ search/cust_main.cgi
4172 { 'company' => { op=>'ILIKE', value=>"%$value%" }, },
4173 { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
4176 if ( $first && $last ) {
4179 { 'first' => { op=>'ILIKE', value=>"%$first%" },
4180 'last' => { op=>'ILIKE', value=>"%$last%" },
4182 { 'ship_first' => { op=>'ILIKE', value=>"%$first%" },
4183 'ship_last' => { op=>'ILIKE', value=>"%$last%" },
4190 { 'last' => { op=>'ILIKE', value=>"%$value%" }, },
4191 { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, },
4195 foreach my $hashref ( @hashrefs ) {
4197 push @cust_main, qsearch( {
4198 'table' => 'cust_main',
4199 'hashref' => { %$hashref,
4202 'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton
4211 " AND $agentnums_sql", #extra_sql #agent virtualization
4214 if ( $first && $last ) {
4215 push @cust_main, FS::cust_main->fuzzy_search(
4216 { 'last' => $last, #fuzzy hashref
4217 'first' => $first }, #
4221 foreach my $field ( 'last', 'company' ) {
4223 FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
4228 #eliminate duplicates
4230 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
4238 =item check_and_rebuild_fuzzyfiles
4242 use vars qw(@fuzzyfields);
4243 @fuzzyfields = ( 'last', 'first', 'company' );
4245 sub check_and_rebuild_fuzzyfiles {
4246 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4247 rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
4250 =item rebuild_fuzzyfiles
4254 sub rebuild_fuzzyfiles {
4256 use Fcntl qw(:flock);
4258 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4259 mkdir $dir, 0700 unless -d $dir;
4261 foreach my $fuzzy ( @fuzzyfields ) {
4263 open(LOCK,">>$dir/cust_main.$fuzzy")
4264 or die "can't open $dir/cust_main.$fuzzy: $!";
4266 or die "can't lock $dir/cust_main.$fuzzy: $!";
4268 open (CACHE,">$dir/cust_main.$fuzzy.tmp")
4269 or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
4271 foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
4272 my $sth = dbh->prepare("SELECT $field FROM cust_main".
4273 " WHERE $field != '' AND $field IS NOT NULL");
4274 $sth->execute or die $sth->errstr;
4276 while ( my $row = $sth->fetchrow_arrayref ) {
4277 print CACHE $row->[0]. "\n";
4282 close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
4284 rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
4295 my( $self, $field ) = @_;
4296 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4297 open(CACHE,"<$dir/cust_main.$field")
4298 or die "can't open $dir/cust_main.$field: $!";
4299 my @array = map { chomp; $_; } <CACHE>;
4304 =item append_fuzzyfiles LASTNAME COMPANY
4308 sub append_fuzzyfiles {
4309 #my( $first, $last, $company ) = @_;
4311 &check_and_rebuild_fuzzyfiles;
4313 use Fcntl qw(:flock);
4315 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4317 foreach my $field (qw( first last company )) {
4322 open(CACHE,">>$dir/cust_main.$field")
4323 or die "can't open $dir/cust_main.$field: $!";
4324 flock(CACHE,LOCK_EX)
4325 or die "can't lock $dir/cust_main.$field: $!";
4327 print CACHE "$value\n";
4329 flock(CACHE,LOCK_UN)
4330 or die "can't unlock $dir/cust_main.$field: $!";
4345 #warn join('-',keys %$param);
4346 my $fh = $param->{filehandle};
4347 my $agentnum = $param->{agentnum};
4349 my $refnum = $param->{refnum};
4350 my $pkgpart = $param->{pkgpart};
4352 #my @fields = @{$param->{fields}};
4353 my $format = $param->{'format'};
4356 if ( $format eq 'simple' ) {
4357 @fields = qw( cust_pkg.setup dayphone first last
4358 address1 address2 city state zip comments );
4360 } elsif ( $format eq 'extended' ) {
4361 @fields = qw( agent_custid refnum
4362 last first address1 address2 city state zip country
4364 ship_last ship_first ship_address1 ship_address2
4365 ship_city ship_state ship_zip ship_country
4366 payinfo paycvv paydate
4369 svc_acct.username svc_acct._password
4373 die "unknown format $format";
4376 eval "use Text::CSV_XS;";
4379 my $csv = new Text::CSV_XS;
4386 local $SIG{HUP} = 'IGNORE';
4387 local $SIG{INT} = 'IGNORE';
4388 local $SIG{QUIT} = 'IGNORE';
4389 local $SIG{TERM} = 'IGNORE';
4390 local $SIG{TSTP} = 'IGNORE';
4391 local $SIG{PIPE} = 'IGNORE';
4393 my $oldAutoCommit = $FS::UID::AutoCommit;
4394 local $FS::UID::AutoCommit = 0;
4397 #while ( $columns = $csv->getline($fh) ) {
4399 while ( defined($line=<$fh>) ) {
4401 $csv->parse($line) or do {
4402 $dbh->rollback if $oldAutoCommit;
4403 return "can't parse: ". $csv->error_input();
4406 my @columns = $csv->fields();
4407 #warn join('-',@columns);
4410 agentnum => $agentnum,
4412 country => $conf->config('countrydefault') || 'US',
4413 payby => $payby, #default
4414 paydate => '12/2037', #default
4416 my $billtime = time;
4417 my %cust_pkg = ( pkgpart => $pkgpart );
4419 foreach my $field ( @fields ) {
4421 if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|expire|cancel)$/ ) {
4423 #$cust_pkg{$1} = str2time( shift @$columns );
4424 if ( $1 eq 'pkgpart' ) {
4425 $cust_pkg{$1} = shift @columns;
4426 } elsif ( $1 eq 'setup' ) {
4427 $billtime = str2time(shift @columns);
4429 $cust_pkg{$1} = str2time( shift @columns );
4432 } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
4434 $svc_acct{$1} = shift @columns;
4438 #refnum interception
4439 if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
4441 my $referral = $columns[0];
4442 my $part_referral = new FS::part_referral {
4443 'referral' => $referral,
4444 'agentnum' => $agentnum,
4447 my $error = $part_referral->insert;
4449 $dbh->rollback if $oldAutoCommit;
4450 return "can't auto-insert advertising source: $referral: $error";
4452 $columns[0] = $part_referral->refnum;
4455 #$cust_main{$field} = shift @$columns;
4456 $cust_main{$field} = shift @columns;
4460 my $invoicing_list = $cust_main{'invoicing_list'}
4461 ? [ delete $cust_main{'invoicing_list'} ]
4464 my $cust_main = new FS::cust_main ( \%cust_main );
4467 tie my %hash, 'Tie::RefHash'; #this part is important
4469 if ( $cust_pkg{'pkgpart'} ) {
4470 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
4473 if ( $svc_acct{'username'} ) {
4474 $svc_acct{svcpart} = $cust_pkg->part_pkg->svcpart( 'svc_acct' );
4475 push @svc_acct, new FS::svc_acct ( \%svc_acct )
4478 $hash{$cust_pkg} = \@svc_acct;
4481 my $error = $cust_main->insert( \%hash, $invoicing_list );
4484 $dbh->rollback if $oldAutoCommit;
4485 return "can't insert customer for $line: $error";
4488 if ( $format eq 'simple' ) {
4490 #false laziness w/bill.cgi
4491 $error = $cust_main->bill( 'time' => $billtime );
4493 $dbh->rollback if $oldAutoCommit;
4494 return "can't bill customer for $line: $error";
4497 $cust_main->apply_payments;
4498 $cust_main->apply_credits;
4500 $error = $cust_main->collect();
4502 $dbh->rollback if $oldAutoCommit;
4503 return "can't collect customer for $line: $error";
4511 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4513 return "Empty file!" unless $imported;
4525 #warn join('-',keys %$param);
4526 my $fh = $param->{filehandle};
4527 my @fields = @{$param->{fields}};
4529 eval "use Text::CSV_XS;";
4532 my $csv = new Text::CSV_XS;
4539 local $SIG{HUP} = 'IGNORE';
4540 local $SIG{INT} = 'IGNORE';
4541 local $SIG{QUIT} = 'IGNORE';
4542 local $SIG{TERM} = 'IGNORE';
4543 local $SIG{TSTP} = 'IGNORE';
4544 local $SIG{PIPE} = 'IGNORE';
4546 my $oldAutoCommit = $FS::UID::AutoCommit;
4547 local $FS::UID::AutoCommit = 0;
4550 #while ( $columns = $csv->getline($fh) ) {
4552 while ( defined($line=<$fh>) ) {
4554 $csv->parse($line) or do {
4555 $dbh->rollback if $oldAutoCommit;
4556 return "can't parse: ". $csv->error_input();
4559 my @columns = $csv->fields();
4560 #warn join('-',@columns);
4563 foreach my $field ( @fields ) {
4564 $row{$field} = shift @columns;
4567 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
4568 unless ( $cust_main ) {
4569 $dbh->rollback if $oldAutoCommit;
4570 return "unknown custnum $row{'custnum'}";
4573 if ( $row{'amount'} > 0 ) {
4574 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
4576 $dbh->rollback if $oldAutoCommit;
4580 } elsif ( $row{'amount'} < 0 ) {
4581 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
4584 $dbh->rollback if $oldAutoCommit;
4594 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4596 return "Empty file!" unless $imported;
4608 The delete method should possibly take an FS::cust_main object reference
4609 instead of a scalar customer number.
4611 Bill and collect options should probably be passed as references instead of a
4614 There should probably be a configuration file with a list of allowed credit
4617 No multiple currency support (probably a larger project than just this module).
4619 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
4623 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
4624 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
4625 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.