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_binary('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".
515 " (or do you need to run dbdef-create?)\n" unless $child_pkey;
517 #false laziness w/Record::insert and only works on Pg
518 #refactor the proper last-inserted-id stuff out of Record::insert if this
519 # ever gets use for anything besides a quick kludge for one customer
520 my $default = dbdef->table($child_table)->column($child_pkey)->default;
521 $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
522 or return "can't parse $child_table.$child_pkey default value ".
523 " for sequence name: $default";
528 my @sel_columns = grep { $_ ne $primary_key }
529 dbdef->table($child_table)->columns;
530 my $sel_columns = join(', ', @sel_columns );
532 my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
533 my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) ';
534 my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
536 my $sel_st = "SELECT $sel_columns FROM $child_table".
537 " WHERE $primary_key = $sourceid";
540 my $sel_sth = dbh->prepare( $sel_st )
541 or return dbh->errstr;
543 $sel_sth->execute or return $sel_sth->errstr;
545 while ( my $row = $sel_sth->fetchrow_hashref ) {
547 warn " selected row: ".
548 join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n"
552 "INSERT INTO $child_table $ins_columns VALUES $placeholders";
553 my $ins_sth =dbh->prepare($statement)
554 or return dbh->errstr;
555 my @param = ( $destid, map $row->{$_}, @ins_columns );
556 warn " $statement: [ ". join(', ', @param). " ]\n"
558 $ins_sth->execute( @param )
559 or return $ins_sth->errstr;
561 #next unless keys %{ $child_tables{$child_table} };
562 next unless $sequence;
564 #another section of that laziness
565 my $seq_sql = "SELECT currval('$sequence')";
566 my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
567 $seq_sth->execute or return $seq_sth->errstr;
568 my $insertid = $seq_sth->fetchrow_arrayref->[0];
570 # don't drink soap! recurse! recurse! okay!
572 _copy_skel( $child_table,
573 $row->{$child_pkey}, #sourceid
575 %{ $child_tables{$child_table} },
577 return $error if $error;
587 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
589 Like the insert method on an existing record, this method orders a package
590 and included services atomicaly. Pass a Tie::RefHash data structure to this
591 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
592 be a better explanation of this, but until then, here's an example:
595 tie %hash, 'Tie::RefHash'; #this part is important
597 $cust_pkg => [ $svc_acct ],
600 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
602 Services can be new, in which case they are inserted, or existing unaudited
603 services, in which case they are linked to the newly-created package.
605 Currently available options are: I<depend_jobnum> and I<noexport>.
607 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
608 on the supplied jobnum (they will not run until the specific job completes).
609 This can be used to defer provisioning until some action completes (such
610 as running the customer's credit card successfully).
612 The I<noexport> option is deprecated. If I<noexport> is set true, no
613 provisioning jobs (exports) are scheduled. (You can schedule them later with
614 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
615 on the cust_main object is not recommended, as existing services will also be
622 my $cust_pkgs = shift;
625 my %svc_options = ();
626 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
627 if exists $options{'depend_jobnum'};
628 warn "$me order_pkgs called with options ".
629 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
632 local $SIG{HUP} = 'IGNORE';
633 local $SIG{INT} = 'IGNORE';
634 local $SIG{QUIT} = 'IGNORE';
635 local $SIG{TERM} = 'IGNORE';
636 local $SIG{TSTP} = 'IGNORE';
637 local $SIG{PIPE} = 'IGNORE';
639 my $oldAutoCommit = $FS::UID::AutoCommit;
640 local $FS::UID::AutoCommit = 0;
643 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
645 foreach my $cust_pkg ( keys %$cust_pkgs ) {
646 $cust_pkg->custnum( $self->custnum );
647 my $error = $cust_pkg->insert;
649 $dbh->rollback if $oldAutoCommit;
650 return "inserting cust_pkg (transaction rolled back): $error";
652 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
653 if ( $svc_something->svcnum ) {
654 my $old_cust_svc = $svc_something->cust_svc;
655 my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
656 $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
657 $error = $new_cust_svc->replace($old_cust_svc);
659 $svc_something->pkgnum( $cust_pkg->pkgnum );
660 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
661 $svc_something->seconds( $svc_something->seconds + $$seconds );
664 $error = $svc_something->insert(%svc_options);
667 $dbh->rollback if $oldAutoCommit;
668 #return "inserting svc_ (transaction rolled back): $error";
674 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
678 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF ]
680 Recharges this (existing) customer with the specified prepaid card (see
681 L<FS::prepay_credit>), specified either by I<identifier> or as an
682 FS::prepay_credit object. If there is an error, returns the error, otherwise
685 Optionally, two scalar references can be passed as well. They will have their
686 values filled in with the amount and number of seconds applied by this prepaid
691 sub recharge_prepay {
692 my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
694 local $SIG{HUP} = 'IGNORE';
695 local $SIG{INT} = 'IGNORE';
696 local $SIG{QUIT} = 'IGNORE';
697 local $SIG{TERM} = 'IGNORE';
698 local $SIG{TSTP} = 'IGNORE';
699 local $SIG{PIPE} = 'IGNORE';
701 my $oldAutoCommit = $FS::UID::AutoCommit;
702 local $FS::UID::AutoCommit = 0;
705 my( $amount, $seconds ) = ( 0, 0 );
707 my $error = $self->get_prepay($prepay_credit, \$amount, \$seconds)
708 || $self->increment_seconds($seconds)
709 || $self->insert_cust_pay_prepay( $amount,
711 ? $prepay_credit->identifier
716 $dbh->rollback if $oldAutoCommit;
720 if ( defined($amountref) ) { $$amountref = $amount; }
721 if ( defined($secondsref) ) { $$secondsref = $seconds; }
723 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
728 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ , AMOUNTREF, SECONDSREF
730 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
731 specified either by I<identifier> or as an FS::prepay_credit object.
733 References to I<amount> and I<seconds> scalars should be passed as arguments
734 and will be incremented by the values of the prepaid card.
736 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
737 check or set this customer's I<agentnum>.
739 If there is an error, returns the error, otherwise returns false.
745 my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
747 local $SIG{HUP} = 'IGNORE';
748 local $SIG{INT} = 'IGNORE';
749 local $SIG{QUIT} = 'IGNORE';
750 local $SIG{TERM} = 'IGNORE';
751 local $SIG{TSTP} = 'IGNORE';
752 local $SIG{PIPE} = 'IGNORE';
754 my $oldAutoCommit = $FS::UID::AutoCommit;
755 local $FS::UID::AutoCommit = 0;
758 unless ( ref($prepay_credit) ) {
760 my $identifier = $prepay_credit;
762 $prepay_credit = qsearchs(
764 { 'identifier' => $prepay_credit },
769 unless ( $prepay_credit ) {
770 $dbh->rollback if $oldAutoCommit;
771 return "Invalid prepaid card: ". $identifier;
776 if ( $prepay_credit->agentnum ) {
777 if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
778 $dbh->rollback if $oldAutoCommit;
779 return "prepaid card not valid for agent ". $self->agentnum;
781 $self->agentnum($prepay_credit->agentnum);
784 my $error = $prepay_credit->delete;
786 $dbh->rollback if $oldAutoCommit;
787 return "removing prepay_credit (transaction rolled back): $error";
790 $$amountref += $prepay_credit->amount;
791 $$secondsref += $prepay_credit->seconds;
793 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
798 =item increment_seconds SECONDS
800 Updates this customer's single or primary account (see L<FS::svc_acct>) by
801 the specified number of seconds. If there is an error, returns the error,
802 otherwise returns false.
806 sub increment_seconds {
807 my( $self, $seconds ) = @_;
808 warn "$me increment_seconds called: $seconds seconds\n"
811 my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
812 $self->ncancelled_pkgs;
815 return 'No packages with primary or single services found'.
816 ' to apply pre-paid time';
817 } elsif ( scalar(@cust_pkg) > 1 ) {
818 #maybe have a way to specify the package/account?
819 return 'Multiple packages found to apply pre-paid time';
822 my $cust_pkg = $cust_pkg[0];
823 warn " found package pkgnum ". $cust_pkg->pkgnum. "\n"
827 $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
830 return 'No account found to apply pre-paid time';
831 } elsif ( scalar(@cust_svc) > 1 ) {
832 return 'Multiple accounts found to apply pre-paid time';
835 my $svc_acct = $cust_svc[0]->svc_x;
836 warn " found service svcnum ". $svc_acct->pkgnum.
837 ' ('. $svc_acct->email. ")\n"
840 $svc_acct->increment_seconds($seconds);
844 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
846 Inserts a prepayment in the specified amount for this customer. An optional
847 second argument can specify the prepayment identifier for tracking purposes.
848 If there is an error, returns the error, otherwise returns false.
852 sub insert_cust_pay_prepay {
853 shift->insert_cust_pay('PREP', @_);
856 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
858 Inserts a cash payment in the specified amount for this customer. An optional
859 second argument can specify the payment identifier for tracking purposes.
860 If there is an error, returns the error, otherwise returns false.
864 sub insert_cust_pay_cash {
865 shift->insert_cust_pay('CASH', @_);
868 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
870 Inserts a Western Union payment in the specified amount for this customer. An
871 optional second argument can specify the prepayment identifier for tracking
872 purposes. If there is an error, returns the error, otherwise returns false.
876 sub insert_cust_pay_west {
877 shift->insert_cust_pay('WEST', @_);
880 sub insert_cust_pay {
881 my( $self, $payby, $amount ) = splice(@_, 0, 3);
882 my $payinfo = scalar(@_) ? shift : '';
884 my $cust_pay = new FS::cust_pay {
885 'custnum' => $self->custnum,
886 'paid' => sprintf('%.2f', $amount),
887 #'_date' => #date the prepaid card was purchased???
889 'payinfo' => $payinfo,
897 This method is deprecated. See the I<depend_jobnum> option to the insert and
898 order_pkgs methods for a better way to defer provisioning.
900 Re-schedules all exports by calling the B<reexport> method of all associated
901 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
902 otherwise returns false.
909 carp "WARNING: FS::cust_main::reexport is deprectated; ".
910 "use the depend_jobnum option to insert or order_pkgs to delay export";
912 local $SIG{HUP} = 'IGNORE';
913 local $SIG{INT} = 'IGNORE';
914 local $SIG{QUIT} = 'IGNORE';
915 local $SIG{TERM} = 'IGNORE';
916 local $SIG{TSTP} = 'IGNORE';
917 local $SIG{PIPE} = 'IGNORE';
919 my $oldAutoCommit = $FS::UID::AutoCommit;
920 local $FS::UID::AutoCommit = 0;
923 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
924 my $error = $cust_pkg->reexport;
926 $dbh->rollback if $oldAutoCommit;
931 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
936 =item delete NEW_CUSTNUM
938 This deletes the customer. If there is an error, returns the error, otherwise
941 This will completely remove all traces of the customer record. This is not
942 what you want when a customer cancels service; for that, cancel all of the
943 customer's packages (see L</cancel>).
945 If the customer has any uncancelled packages, you need to pass a new (valid)
946 customer number for those packages to be transferred to. Cancelled packages
947 will be deleted. Did I mention that this is NOT what you want when a customer
948 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
950 You can't delete a customer with invoices (see L<FS::cust_bill>),
951 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
952 refunds (see L<FS::cust_refund>).
959 local $SIG{HUP} = 'IGNORE';
960 local $SIG{INT} = 'IGNORE';
961 local $SIG{QUIT} = 'IGNORE';
962 local $SIG{TERM} = 'IGNORE';
963 local $SIG{TSTP} = 'IGNORE';
964 local $SIG{PIPE} = 'IGNORE';
966 my $oldAutoCommit = $FS::UID::AutoCommit;
967 local $FS::UID::AutoCommit = 0;
970 if ( $self->cust_bill ) {
971 $dbh->rollback if $oldAutoCommit;
972 return "Can't delete a customer with invoices";
974 if ( $self->cust_credit ) {
975 $dbh->rollback if $oldAutoCommit;
976 return "Can't delete a customer with credits";
978 if ( $self->cust_pay ) {
979 $dbh->rollback if $oldAutoCommit;
980 return "Can't delete a customer with payments";
982 if ( $self->cust_refund ) {
983 $dbh->rollback if $oldAutoCommit;
984 return "Can't delete a customer with refunds";
987 my @cust_pkg = $self->ncancelled_pkgs;
989 my $new_custnum = shift;
990 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
991 $dbh->rollback if $oldAutoCommit;
992 return "Invalid new customer number: $new_custnum";
994 foreach my $cust_pkg ( @cust_pkg ) {
995 my %hash = $cust_pkg->hash;
996 $hash{'custnum'} = $new_custnum;
997 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
998 my $error = $new_cust_pkg->replace($cust_pkg);
1000 $dbh->rollback if $oldAutoCommit;
1005 my @cancelled_cust_pkg = $self->all_pkgs;
1006 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1007 my $error = $cust_pkg->delete;
1009 $dbh->rollback if $oldAutoCommit;
1014 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
1015 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
1017 my $error = $cust_main_invoice->delete;
1019 $dbh->rollback if $oldAutoCommit;
1024 my $error = $self->SUPER::delete;
1026 $dbh->rollback if $oldAutoCommit;
1030 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1035 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
1037 Replaces the OLD_RECORD with this one in the database. If there is an error,
1038 returns the error, otherwise returns false.
1040 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1041 be set as the invoicing list (see L<"invoicing_list">). Errors return as
1042 expected and rollback the entire transaction; it is not necessary to call
1043 check_invoicing_list first. Here's an example:
1045 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1053 warn "$me replace called\n"
1056 local $SIG{HUP} = 'IGNORE';
1057 local $SIG{INT} = 'IGNORE';
1058 local $SIG{QUIT} = 'IGNORE';
1059 local $SIG{TERM} = 'IGNORE';
1060 local $SIG{TSTP} = 'IGNORE';
1061 local $SIG{PIPE} = 'IGNORE';
1063 # If the mask is blank then try to set it - if we can...
1064 if (!defined($self->getfield('paymask')) || $self->getfield('paymask') eq '') {
1065 $self->paymask($self->payinfo);
1068 # We absolutely have to have an old vs. new record to make this work.
1069 if (!defined($old)) {
1070 $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1073 my $curuser = $FS::CurrentUser::CurrentUser;
1074 if ( $self->payby eq 'COMP'
1075 && $self->payby ne $old->payby
1076 && ! $curuser->access_right('Complimentary customer')
1079 return "You are not permitted to create complimentary accounts.";
1082 local($ignore_expired_card) = 1
1083 if $old->payby =~ /^(CARD|DCRD)$/
1084 && $self->payby =~ /^(CARD|DCRD)$/
1085 && $old->payinfo eq $self->payinfo;
1087 my $oldAutoCommit = $FS::UID::AutoCommit;
1088 local $FS::UID::AutoCommit = 0;
1091 my $error = $self->SUPER::replace($old);
1094 $dbh->rollback if $oldAutoCommit;
1098 if ( @param ) { # INVOICING_LIST_ARYREF
1099 my $invoicing_list = shift @param;
1100 $error = $self->check_invoicing_list( $invoicing_list );
1102 $dbh->rollback if $oldAutoCommit;
1105 $self->invoicing_list( $invoicing_list );
1108 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
1109 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
1110 # card/check/lec info has changed, want to retry realtime_ invoice events
1111 my $error = $self->retry_realtime;
1113 $dbh->rollback if $oldAutoCommit;
1118 unless ( $import || $skip_fuzzyfiles ) {
1119 $error = $self->queue_fuzzyfiles_update;
1121 $dbh->rollback if $oldAutoCommit;
1122 return "updating fuzzy search cache: $error";
1126 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1131 =item queue_fuzzyfiles_update
1133 Used by insert & replace to update the fuzzy search cache
1137 sub queue_fuzzyfiles_update {
1140 local $SIG{HUP} = 'IGNORE';
1141 local $SIG{INT} = 'IGNORE';
1142 local $SIG{QUIT} = 'IGNORE';
1143 local $SIG{TERM} = 'IGNORE';
1144 local $SIG{TSTP} = 'IGNORE';
1145 local $SIG{PIPE} = 'IGNORE';
1147 my $oldAutoCommit = $FS::UID::AutoCommit;
1148 local $FS::UID::AutoCommit = 0;
1151 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1152 my $error = $queue->insert( map $self->getfield($_),
1153 qw(first last company)
1156 $dbh->rollback if $oldAutoCommit;
1157 return "queueing job (transaction rolled back): $error";
1160 if ( $self->ship_last ) {
1161 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1162 $error = $queue->insert( map $self->getfield("ship_$_"),
1163 qw(first last company)
1166 $dbh->rollback if $oldAutoCommit;
1167 return "queueing job (transaction rolled back): $error";
1171 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1178 Checks all fields to make sure this is a valid customer record. If there is
1179 an error, returns the error, otherwise returns false. Called by the insert
1180 and replace methods.
1187 warn "$me check BEFORE: \n". $self->_dump
1191 $self->ut_numbern('custnum')
1192 || $self->ut_number('agentnum')
1193 || $self->ut_textn('agent_custid')
1194 || $self->ut_number('refnum')
1195 || $self->ut_name('last')
1196 || $self->ut_name('first')
1197 || $self->ut_textn('company')
1198 || $self->ut_text('address1')
1199 || $self->ut_textn('address2')
1200 || $self->ut_text('city')
1201 || $self->ut_textn('county')
1202 || $self->ut_textn('state')
1203 || $self->ut_country('country')
1204 || $self->ut_anything('comments')
1205 || $self->ut_numbern('referral_custnum')
1207 #barf. need message catalogs. i18n. etc.
1208 $error .= "Please select an advertising source."
1209 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1210 return $error if $error;
1212 return "Unknown agent"
1213 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1215 return "Unknown refnum"
1216 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1218 return "Unknown referring custnum: ". $self->referral_custnum
1219 unless ! $self->referral_custnum
1220 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1222 if ( $self->ss eq '' ) {
1227 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1228 or return "Illegal social security number: ". $self->ss;
1229 $self->ss("$1-$2-$3");
1233 # bad idea to disable, causes billing to fail because of no tax rates later
1234 # unless ( $import ) {
1235 unless ( qsearch('cust_main_county', {
1236 'country' => $self->country,
1239 return "Unknown state/county/country: ".
1240 $self->state. "/". $self->county. "/". $self->country
1241 unless qsearch('cust_main_county',{
1242 'state' => $self->state,
1243 'county' => $self->county,
1244 'country' => $self->country,
1250 $self->ut_phonen('daytime', $self->country)
1251 || $self->ut_phonen('night', $self->country)
1252 || $self->ut_phonen('fax', $self->country)
1253 || $self->ut_zip('zip', $self->country)
1255 return $error if $error;
1258 last first company address1 address2 city county state zip
1259 country daytime night fax
1262 if ( defined $self->dbdef_table->column('ship_last') ) {
1263 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1265 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
1269 $self->ut_name('ship_last')
1270 || $self->ut_name('ship_first')
1271 || $self->ut_textn('ship_company')
1272 || $self->ut_text('ship_address1')
1273 || $self->ut_textn('ship_address2')
1274 || $self->ut_text('ship_city')
1275 || $self->ut_textn('ship_county')
1276 || $self->ut_textn('ship_state')
1277 || $self->ut_country('ship_country')
1279 return $error if $error;
1281 #false laziness with above
1282 unless ( qsearchs('cust_main_county', {
1283 'country' => $self->ship_country,
1286 return "Unknown ship_state/ship_county/ship_country: ".
1287 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1288 unless qsearch('cust_main_county',{
1289 'state' => $self->ship_state,
1290 'county' => $self->ship_county,
1291 'country' => $self->ship_country,
1297 $self->ut_phonen('ship_daytime', $self->ship_country)
1298 || $self->ut_phonen('ship_night', $self->ship_country)
1299 || $self->ut_phonen('ship_fax', $self->ship_country)
1300 || $self->ut_zip('ship_zip', $self->ship_country)
1302 return $error if $error;
1304 } else { # ship_ info eq billing info, so don't store dup info in database
1305 $self->setfield("ship_$_", '')
1306 foreach qw( last first company address1 address2 city county state zip
1307 country daytime night fax );
1311 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1312 or return "Illegal payby: ". $self->payby;
1314 $error = $self->ut_numbern('paystart_month')
1315 || $self->ut_numbern('paystart_year')
1316 || $self->ut_numbern('payissue')
1318 return $error if $error;
1320 if ( $self->payip eq '' ) {
1323 $error = $self->ut_ip('payip');
1324 return $error if $error;
1327 # If it is encrypted and the private key is not availaible then we can't
1328 # check the credit card.
1330 my $check_payinfo = 1;
1332 if ($self->is_encrypted($self->payinfo)) {
1338 if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1340 my $payinfo = $self->payinfo;
1341 $payinfo =~ s/\D//g;
1342 $payinfo =~ /^(\d{13,16})$/
1343 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1345 $self->payinfo($payinfo);
1347 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1349 return gettext('unknown_card_type')
1350 if cardtype($self->payinfo) eq "Unknown";
1352 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1354 return 'Banned credit card: banned on '.
1355 time2str('%a %h %o at %r', $ban->_date).
1356 ' by '. $ban->otaker.
1357 ' (ban# '. $ban->bannum. ')';
1360 if ( defined $self->dbdef_table->column('paycvv') ) {
1361 if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1362 if ( cardtype($self->payinfo) eq 'American Express card' ) {
1363 $self->paycvv =~ /^(\d{4})$/
1364 or return "CVV2 (CID) for American Express cards is four digits.";
1367 $self->paycvv =~ /^(\d{3})$/
1368 or return "CVV2 (CVC2/CID) is three digits.";
1376 my $cardtype = cardtype($payinfo);
1377 if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1379 return "Start date or issue number is required for $cardtype cards"
1380 unless $self->paystart_month && $self->paystart_year or $self->payissue;
1382 return "Start month must be between 1 and 12"
1383 if $self->paystart_month
1384 and $self->paystart_month < 1 || $self->paystart_month > 12;
1386 return "Start year must be 1990 or later"
1387 if $self->paystart_year
1388 and $self->paystart_year < 1990;
1390 return "Issue number must be beween 1 and 99"
1392 and $self->payissue < 1 || $self->payissue > 99;
1395 $self->paystart_month('');
1396 $self->paystart_year('');
1397 $self->payissue('');
1400 } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1402 my $payinfo = $self->payinfo;
1403 $payinfo =~ s/[^\d\@]//g;
1404 if ( $conf->exists('echeck-nonus') ) {
1405 $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1406 $payinfo = "$1\@$2";
1408 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1409 $payinfo = "$1\@$2";
1411 $self->payinfo($payinfo);
1412 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1414 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1416 return 'Banned ACH account: banned on '.
1417 time2str('%a %h %o at %r', $ban->_date).
1418 ' by '. $ban->otaker.
1419 ' (ban# '. $ban->bannum. ')';
1422 } elsif ( $self->payby eq 'LECB' ) {
1424 my $payinfo = $self->payinfo;
1425 $payinfo =~ s/\D//g;
1426 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1428 $self->payinfo($payinfo);
1429 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1431 } elsif ( $self->payby eq 'BILL' ) {
1433 $error = $self->ut_textn('payinfo');
1434 return "Illegal P.O. number: ". $self->payinfo if $error;
1435 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1437 } elsif ( $self->payby eq 'COMP' ) {
1439 my $curuser = $FS::CurrentUser::CurrentUser;
1440 if ( ! $self->custnum
1441 && ! $curuser->access_right('Complimentary customer')
1444 return "You are not permitted to create complimentary accounts."
1447 $error = $self->ut_textn('payinfo');
1448 return "Illegal comp account issuer: ". $self->payinfo if $error;
1449 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1451 } elsif ( $self->payby eq 'PREPAY' ) {
1453 my $payinfo = $self->payinfo;
1454 $payinfo =~ s/\W//g; #anything else would just confuse things
1455 $self->payinfo($payinfo);
1456 $error = $self->ut_alpha('payinfo');
1457 return "Illegal prepayment identifier: ". $self->payinfo if $error;
1458 return "Unknown prepayment identifier"
1459 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1460 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1464 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1465 return "Expiration date required"
1466 unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1470 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1471 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1472 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1473 ( $m, $y ) = ( $3, "20$2" );
1475 return "Illegal expiration date: ". $self->paydate;
1477 $self->paydate("$y-$m-01");
1478 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1479 return gettext('expired_card')
1481 && !$ignore_expired_card
1482 && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1485 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1486 ( ! $conf->exists('require_cardname')
1487 || $self->payby !~ /^(CARD|DCRD)$/ )
1489 $self->payname( $self->first. " ". $self->getfield('last') );
1491 $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1492 or return gettext('illegal_name'). " payname: ". $self->payname;
1496 foreach my $flag (qw( tax spool_cdr )) {
1497 $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1501 $self->otaker(getotaker) unless $self->otaker;
1503 warn "$me check AFTER: \n". $self->_dump
1506 $self->SUPER::check;
1511 Returns all packages (see L<FS::cust_pkg>) for this customer.
1517 if ( $self->{'_pkgnum'} ) {
1518 values %{ $self->{'_pkgnum'}->cache };
1520 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
1524 =item ncancelled_pkgs
1526 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
1530 sub ncancelled_pkgs {
1532 if ( $self->{'_pkgnum'} ) {
1533 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
1535 @{ [ # force list context
1536 qsearch( 'cust_pkg', {
1537 'custnum' => $self->custnum,
1540 qsearch( 'cust_pkg', {
1541 'custnum' => $self->custnum,
1548 =item suspended_pkgs
1550 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
1554 sub suspended_pkgs {
1556 grep { $_->susp } $self->ncancelled_pkgs;
1559 =item unflagged_suspended_pkgs
1561 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
1562 customer (thouse packages without the `manual_flag' set).
1566 sub unflagged_suspended_pkgs {
1568 return $self->suspended_pkgs
1569 unless dbdef->table('cust_pkg')->column('manual_flag');
1570 grep { ! $_->manual_flag } $self->suspended_pkgs;
1573 =item unsuspended_pkgs
1575 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1580 sub unsuspended_pkgs {
1582 grep { ! $_->susp } $self->ncancelled_pkgs;
1585 =item num_cancelled_pkgs
1587 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
1592 sub num_cancelled_pkgs {
1594 $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
1598 my( $self, $sql ) = @_;
1599 my $sth = dbh->prepare(
1600 "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? AND $sql"
1601 ) or die dbh->errstr;
1602 $sth->execute($self->custnum) or die $sth->errstr;
1603 $sth->fetchrow_arrayref->[0];
1608 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1609 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
1610 on success or a list of errors.
1616 grep { $_->unsuspend } $self->suspended_pkgs;
1621 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1623 Returns a list: an empty list on success or a list of errors.
1629 grep { $_->suspend } $self->unsuspended_pkgs;
1632 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1634 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1635 PKGPARTs (see L<FS::part_pkg>).
1637 Returns a list: an empty list on success or a list of errors.
1641 sub suspend_if_pkgpart {
1644 grep { $_->suspend }
1645 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1646 $self->unsuspended_pkgs;
1649 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1651 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1652 listed PKGPARTs (see L<FS::part_pkg>).
1654 Returns a list: an empty list on success or a list of errors.
1658 sub suspend_unless_pkgpart {
1661 grep { $_->suspend }
1662 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1663 $self->unsuspended_pkgs;
1666 =item cancel [ OPTION => VALUE ... ]
1668 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1670 Available options are: I<quiet>, I<reasonnum>, and I<ban>
1672 I<quiet> can be set true to supress email cancellation notices.
1674 # I<reasonnum> can be set to a cancellation reason (see L<FS::cancel_reason>)
1676 I<ban> can be set true to ban this customer's credit card or ACH information,
1679 Always returns a list: an empty list on success or a list of errors.
1687 if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
1689 #should try decryption (we might have the private key)
1690 # and if not maybe queue a job for the server that does?
1691 return ( "Can't (yet) ban encrypted credit cards" )
1692 if $self->is_encrypted($self->payinfo);
1694 my $ban = new FS::banned_pay $self->_banned_pay_hashref;
1695 my $error = $ban->insert;
1696 return ( $error ) if $error;
1700 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1703 sub _banned_pay_hashref {
1714 'payby' => $payby2ban{$self->payby},
1715 'payinfo' => md5_base64($self->payinfo),
1722 Returns the agent (see L<FS::agent>) for this customer.
1728 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1733 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1734 conjunction with the collect method.
1736 Options are passed as name-value pairs.
1738 Currently available options are:
1740 resetup - if set true, re-charges setup fees.
1742 time - bills the customer as if it were that time. Specified as a UNIX
1743 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1744 L<Date::Parse> for conversion functions. For example:
1748 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1751 If there is an error, returns the error, otherwise returns false.
1756 my( $self, %options ) = @_;
1757 return '' if $self->payby eq 'COMP';
1758 warn "$me bill customer ". $self->custnum. "\n"
1761 my $time = $options{'time'} || time;
1766 local $SIG{HUP} = 'IGNORE';
1767 local $SIG{INT} = 'IGNORE';
1768 local $SIG{QUIT} = 'IGNORE';
1769 local $SIG{TERM} = 'IGNORE';
1770 local $SIG{TSTP} = 'IGNORE';
1771 local $SIG{PIPE} = 'IGNORE';
1773 my $oldAutoCommit = $FS::UID::AutoCommit;
1774 local $FS::UID::AutoCommit = 0;
1777 $self->select_for_update; #mutex
1779 #create a new invoice
1780 #(we'll remove it later if it doesn't actually need to be generated [contains
1781 # no line items] and we're inside a transaciton so nothing else will see it)
1782 my $cust_bill = new FS::cust_bill ( {
1783 'custnum' => $self->custnum,
1785 #'charged' => $charged,
1788 $error = $cust_bill->insert;
1790 $dbh->rollback if $oldAutoCommit;
1791 return "can't create invoice for customer #". $self->custnum. ": $error";
1793 my $invnum = $cust_bill->invnum;
1796 # find the packages which are due for billing, find out how much they are
1797 # & generate invoice database.
1800 my( $total_setup, $total_recur ) = ( 0, 0 );
1802 my @precommit_hooks = ();
1804 foreach my $cust_pkg (
1805 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1808 #NO!! next if $cust_pkg->cancel;
1809 next if $cust_pkg->getfield('cancel');
1811 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
1813 #? to avoid use of uninitialized value errors... ?
1814 $cust_pkg->setfield('bill', '')
1815 unless defined($cust_pkg->bill);
1817 my $part_pkg = $cust_pkg->part_pkg;
1819 my %hash = $cust_pkg->hash;
1820 my $old_cust_pkg = new FS::cust_pkg \%hash;
1829 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1831 warn " bill setup\n" if $DEBUG > 1;
1833 $setup = eval { $cust_pkg->calc_setup( $time ) };
1835 $dbh->rollback if $oldAutoCommit;
1836 return "$@ running calc_setup for $cust_pkg\n";
1839 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1843 # bill recurring fee
1848 if ( $part_pkg->getfield('freq') ne '0' &&
1849 ! $cust_pkg->getfield('susp') &&
1850 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1853 warn " bill recur\n" if $DEBUG > 1;
1855 # XXX shared with $recur_prog
1856 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1858 #over two params! lets at least switch to a hashref for the rest...
1859 my %param = ( 'precommit_hooks' => \@precommit_hooks, );
1861 $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
1863 $dbh->rollback if $oldAutoCommit;
1864 return "$@ running calc_recur for $cust_pkg\n";
1867 #change this bit to use Date::Manip? CAREFUL with timezones (see
1868 # mailing list archive)
1869 my ($sec,$min,$hour,$mday,$mon,$year) =
1870 (localtime($sdate) )[0,1,2,3,4,5];
1872 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1873 # only for figuring next bill date, nothing else, so, reset $sdate again
1875 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1876 $cust_pkg->last_bill($sdate)
1877 if $cust_pkg->dbdef_table->column('last_bill');
1879 if ( $part_pkg->freq =~ /^\d+$/ ) {
1880 $mon += $part_pkg->freq;
1881 until ( $mon < 12 ) { $mon -= 12; $year++; }
1882 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1884 $mday += $weeks * 7;
1885 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1888 } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
1892 $dbh->rollback if $oldAutoCommit;
1893 return "unparsable frequency: ". $part_pkg->freq;
1895 $cust_pkg->setfield('bill',
1896 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1899 warn "\$setup is undefined" unless defined($setup);
1900 warn "\$recur is undefined" unless defined($recur);
1901 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1904 # If $cust_pkg has been modified, update it and create cust_bill_pkg records
1907 if ( $cust_pkg->modified ) {
1909 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
1912 $error=$cust_pkg->replace($old_cust_pkg);
1913 if ( $error ) { #just in case
1914 $dbh->rollback if $oldAutoCommit;
1915 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1918 $setup = sprintf( "%.2f", $setup );
1919 $recur = sprintf( "%.2f", $recur );
1920 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1921 $dbh->rollback if $oldAutoCommit;
1922 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1924 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1925 $dbh->rollback if $oldAutoCommit;
1926 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1929 if ( $setup != 0 || $recur != 0 ) {
1931 warn " charges (setup=$setup, recur=$recur); adding line items\n"
1933 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1934 'invnum' => $invnum,
1935 'pkgnum' => $cust_pkg->pkgnum,
1939 'edate' => $cust_pkg->bill,
1940 'details' => \@details,
1942 $error = $cust_bill_pkg->insert;
1944 $dbh->rollback if $oldAutoCommit;
1945 return "can't create invoice line item for invoice #$invnum: $error";
1947 $total_setup += $setup;
1948 $total_recur += $recur;
1954 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1957 ( $conf->exists('tax-ship_address') && length($self->ship_last) )
1960 my %taxhash = map { $_ => $self->get("$prefix$_") }
1961 qw( state county country );
1963 $taxhash{'taxclass'} = $part_pkg->taxclass;
1965 my @taxes = qsearch( 'cust_main_county', \%taxhash );
1968 $taxhash{'taxclass'} = '';
1969 @taxes = qsearch( 'cust_main_county', \%taxhash );
1972 #one more try at a whole-country tax rate
1974 $taxhash{$_} = '' foreach qw( state county );
1975 @taxes = qsearch( 'cust_main_county', \%taxhash );
1978 # maybe eliminate this entirely, along with all the 0% records
1980 $dbh->rollback if $oldAutoCommit;
1982 "fatal: can't find tax rate for state/county/country/taxclass ".
1983 join('/', ( map $self->get("$prefix$_"),
1984 qw(state county country)
1986 $part_pkg->taxclass ). "\n";
1989 foreach my $tax ( @taxes ) {
1991 my $taxable_charged = 0;
1992 $taxable_charged += $setup
1993 unless $part_pkg->setuptax =~ /^Y$/i
1994 || $tax->setuptax =~ /^Y$/i;
1995 $taxable_charged += $recur
1996 unless $part_pkg->recurtax =~ /^Y$/i
1997 || $tax->recurtax =~ /^Y$/i;
1998 next unless $taxable_charged;
2000 if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
2001 #my ($mon,$year) = (localtime($sdate) )[4,5];
2002 my ($mon,$year) = (localtime( $sdate || $cust_bill->_date ) )[4,5];
2004 my $freq = $part_pkg->freq || 1;
2005 if ( $freq !~ /(\d+)$/ ) {
2006 $dbh->rollback if $oldAutoCommit;
2007 return "daily/weekly package definitions not (yet?)".
2008 " compatible with monthly tax exemptions";
2010 my $taxable_per_month =
2011 sprintf("%.2f", $taxable_charged / $freq );
2013 #call the whole thing off if this customer has any old
2014 #exemption records...
2015 my @cust_tax_exempt =
2016 qsearch( 'cust_tax_exempt' => { custnum=> $self->custnum } );
2017 if ( @cust_tax_exempt ) {
2018 $dbh->rollback if $oldAutoCommit;
2020 'this customer still has old-style tax exemption records; '.
2021 'run bin/fs-migrate-cust_tax_exempt?';
2024 foreach my $which_month ( 1 .. $freq ) {
2026 #maintain the new exemption table now
2029 FROM cust_tax_exempt_pkg
2030 LEFT JOIN cust_bill_pkg USING ( billpkgnum )
2031 LEFT JOIN cust_bill USING ( invnum )
2037 my $sth = dbh->prepare($sql) or do {
2038 $dbh->rollback if $oldAutoCommit;
2039 return "fatal: can't lookup exising exemption: ". dbh->errstr;
2047 $dbh->rollback if $oldAutoCommit;
2048 return "fatal: can't lookup exising exemption: ". dbh->errstr;
2050 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
2052 my $remaining_exemption =
2053 $tax->exempt_amount - $existing_exemption;
2054 if ( $remaining_exemption > 0 ) {
2055 my $addl = $remaining_exemption > $taxable_per_month
2056 ? $taxable_per_month
2057 : $remaining_exemption;
2058 $taxable_charged -= $addl;
2060 my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
2061 'billpkgnum' => $cust_bill_pkg->billpkgnum,
2062 'taxnum' => $tax->taxnum,
2063 'year' => 1900+$year,
2065 'amount' => sprintf("%.2f", $addl ),
2067 $error = $cust_tax_exempt_pkg->insert;
2069 $dbh->rollback if $oldAutoCommit;
2070 return "fatal: can't insert cust_tax_exempt_pkg: $error";
2072 } # if $remaining_exemption > 0
2076 #until ( $mon < 12 ) { $mon -= 12; $year++; }
2077 until ( $mon < 13 ) { $mon -= 12; $year++; }
2079 } #foreach $which_month
2081 } #if $tax->exempt_amount
2083 $taxable_charged = sprintf( "%.2f", $taxable_charged);
2085 #$tax += $taxable_charged * $cust_main_county->tax / 100
2086 $tax{ $tax->taxname || 'Tax' } +=
2087 $taxable_charged * $tax->tax / 100
2089 } #foreach my $tax ( @taxes )
2091 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
2093 } #if $setup != 0 || $recur != 0
2095 } #if $cust_pkg->modified
2097 } #foreach my $cust_pkg
2099 unless ( $cust_bill->cust_bill_pkg ) {
2100 $cust_bill->delete; #don't create an invoice w/o line items
2101 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2105 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
2107 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
2108 my $tax = sprintf("%.2f", $tax{$taxname} );
2109 $charged = sprintf( "%.2f", $charged+$tax );
2111 my $cust_bill_pkg = new FS::cust_bill_pkg ({
2112 'invnum' => $invnum,
2118 'itemdesc' => $taxname,
2120 $error = $cust_bill_pkg->insert;
2122 $dbh->rollback if $oldAutoCommit;
2123 return "can't create invoice line item for invoice #$invnum: $error";
2125 $total_setup += $tax;
2129 $cust_bill->charged( sprintf( "%.2f", $total_setup + $total_recur ) );
2130 $error = $cust_bill->replace;
2132 $dbh->rollback if $oldAutoCommit;
2133 return "can't update charged for invoice #$invnum: $error";
2136 foreach my $hook ( @precommit_hooks ) {
2138 &{$hook}; #($self) ?
2141 $dbh->rollback if $oldAutoCommit;
2142 return "$@ running precommit hook $hook\n";
2146 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2150 =item collect OPTIONS
2152 (Attempt to) collect money for this customer's outstanding invoices (see
2153 L<FS::cust_bill>). Usually used after the bill method.
2155 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
2156 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
2157 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
2159 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
2160 and the invoice events web interface.
2162 If there is an error, returns the error, otherwise returns false.
2164 Options are passed as name-value pairs.
2166 Currently available options are:
2168 invoice_time - Use this time when deciding when to print invoices and
2169 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>
2170 for conversion functions.
2172 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
2175 quiet - set true to surpress email card/ACH decline notices.
2177 freq - "1d" for the traditional, daily events (the default), or "1m" for the
2180 payby - allows for one time override of normal customer billing method
2185 my( $self, %options ) = @_;
2186 my $invoice_time = $options{'invoice_time'} || time;
2189 local $SIG{HUP} = 'IGNORE';
2190 local $SIG{INT} = 'IGNORE';
2191 local $SIG{QUIT} = 'IGNORE';
2192 local $SIG{TERM} = 'IGNORE';
2193 local $SIG{TSTP} = 'IGNORE';
2194 local $SIG{PIPE} = 'IGNORE';
2196 my $oldAutoCommit = $FS::UID::AutoCommit;
2197 local $FS::UID::AutoCommit = 0;
2200 $self->select_for_update; #mutex
2202 my $balance = $self->balance;
2203 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
2205 unless ( $balance > 0 ) { #redundant?????
2206 $dbh->rollback if $oldAutoCommit; #hmm
2210 if ( exists($options{'retry_card'}) ) {
2211 carp 'retry_card option passed to collect is deprecated; use retry';
2212 $options{'retry'} ||= $options{'retry_card'};
2214 if ( exists($options{'retry'}) && $options{'retry'} ) {
2215 my $error = $self->retry_realtime;
2217 $dbh->rollback if $oldAutoCommit;
2223 if ( defined $options{'freq'} && $options{'freq'} eq '1m' ) {
2224 $extra_sql = " AND freq = '1m' ";
2226 $extra_sql = " AND ( freq = '1d' OR freq IS NULL OR freq = '' ) ";
2229 foreach my $cust_bill ( $self->open_cust_bill ) {
2231 # don't try to charge for the same invoice if it's already in a batch
2232 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
2234 last if $self->balance <= 0;
2236 warn " invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")\n"
2239 foreach my $part_bill_event (
2240 sort { $a->seconds <=> $b->seconds
2241 || $a->weight <=> $b->weight
2242 || $a->eventpart <=> $b->eventpart }
2243 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
2244 && ! qsearch( 'cust_bill_event', {
2245 'invnum' => $cust_bill->invnum,
2246 'eventpart' => $_->eventpart,
2251 'table' => 'part_bill_event',
2252 'hashref' => { 'payby' => (exists($options{'payby'})
2256 'disabled' => '', },
2257 'extra_sql' => $extra_sql,
2261 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
2262 || $self->balance <= 0; # or if balance<=0
2264 warn " calling invoice event (". $part_bill_event->eventcode. ")\n"
2266 my $cust_main = $self; #for callback
2270 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
2271 local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
2272 $error = eval $part_bill_event->eventcode;
2276 my $statustext = '';
2280 } elsif ( $error ) {
2282 $statustext = $error;
2287 #add cust_bill_event
2288 my $cust_bill_event = new FS::cust_bill_event {
2289 'invnum' => $cust_bill->invnum,
2290 'eventpart' => $part_bill_event->eventpart,
2291 #'_date' => $invoice_time,
2293 'status' => $status,
2294 'statustext' => $statustext,
2296 $error = $cust_bill_event->insert;
2298 #$dbh->rollback if $oldAutoCommit;
2299 #return "error: $error";
2301 # gah, even with transactions.
2302 $dbh->commit if $oldAutoCommit; #well.
2303 my $e = 'WARNING: Event run but database not updated - '.
2304 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
2305 ', eventpart '. $part_bill_event->eventpart.
2316 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2321 =item retry_realtime
2323 Schedules realtime credit card / electronic check / LEC billing events for
2324 for retry. Useful if card information has changed or manual retry is desired.
2325 The 'collect' method must be called to actually retry the transaction.
2327 Implementation details: For each of this customer's open invoices, changes
2328 the status of the first "done" (with statustext error) realtime processing
2333 sub retry_realtime {
2336 local $SIG{HUP} = 'IGNORE';
2337 local $SIG{INT} = 'IGNORE';
2338 local $SIG{QUIT} = 'IGNORE';
2339 local $SIG{TERM} = 'IGNORE';
2340 local $SIG{TSTP} = 'IGNORE';
2341 local $SIG{PIPE} = 'IGNORE';
2343 my $oldAutoCommit = $FS::UID::AutoCommit;
2344 local $FS::UID::AutoCommit = 0;
2347 foreach my $cust_bill (
2348 grep { $_->cust_bill_event }
2349 $self->open_cust_bill
2351 my @cust_bill_event =
2352 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
2354 #$_->part_bill_event->plan eq 'realtime-card'
2355 $_->part_bill_event->eventcode =~
2356 /\$cust_bill\->realtime_(card|ach|lec)/
2357 && $_->status eq 'done'
2360 $cust_bill->cust_bill_event;
2361 next unless @cust_bill_event;
2362 my $error = $cust_bill_event[0]->retry;
2364 $dbh->rollback if $oldAutoCommit;
2365 return "error scheduling invoice event for retry: $error";
2370 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2375 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
2377 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
2378 via a Business::OnlinePayment realtime gateway. See
2379 L<http://420.am/business-onlinepayment> for supported gateways.
2381 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2383 Available options are: I<description>, I<invnum>, I<quiet>
2385 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2386 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2387 if set, will override the value from the customer record.
2389 I<description> is a free-text field passed to the gateway. It defaults to
2390 "Internet services".
2392 If an I<invnum> is specified, this payment (if successful) is applied to the
2393 specified invoice. If you don't specify an I<invnum> you might want to
2394 call the B<apply_payments> method.
2396 I<quiet> can be set true to surpress email decline notices.
2398 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
2403 my( $self, $method, $amount, %options ) = @_;
2405 warn "$me realtime_bop: $method $amount\n";
2406 warn " $_ => $options{$_}\n" foreach keys %options;
2409 $options{'description'} ||= 'Internet services';
2411 eval "use Business::OnlinePayment";
2414 my $payinfo = exists($options{'payinfo'})
2415 ? $options{'payinfo'}
2423 if ( $options{'invnum'} ) {
2424 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
2425 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
2427 map { $_->part_pkg->taxclass }
2429 map { $_->cust_pkg }
2430 $cust_bill->cust_bill_pkg;
2431 unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
2432 #different taxclasses
2433 $taxclass = $taxclasses[0];
2437 #look for an agent gateway override first
2439 if ( $method eq 'CC' ) {
2440 $cardtype = cardtype($payinfo);
2441 } elsif ( $method eq 'ECHECK' ) {
2444 $cardtype = $method;
2448 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2449 cardtype => $cardtype,
2450 taxclass => $taxclass, } )
2451 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2453 taxclass => $taxclass, } )
2454 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2455 cardtype => $cardtype,
2457 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2459 taxclass => '', } );
2461 my $payment_gateway = '';
2462 my( $processor, $login, $password, $action, @bop_options );
2463 if ( $override ) { #use a payment gateway override
2465 $payment_gateway = $override->payment_gateway;
2467 $processor = $payment_gateway->gateway_module;
2468 $login = $payment_gateway->gateway_username;
2469 $password = $payment_gateway->gateway_password;
2470 $action = $payment_gateway->gateway_action;
2471 @bop_options = $payment_gateway->options;
2473 } else { #use the standard settings from the config
2475 ( $processor, $login, $password, $action, @bop_options ) =
2476 $self->default_payment_gateway($method);
2484 my $address = exists($options{'address1'})
2485 ? $options{'address1'}
2487 my $address2 = exists($options{'address2'})
2488 ? $options{'address2'}
2490 $address .= ", ". $address2 if length($address2);
2492 my $o_payname = exists($options{'payname'})
2493 ? $options{'payname'}
2495 my($payname, $payfirst, $paylast);
2496 if ( $o_payname && $method ne 'ECHECK' ) {
2497 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2498 or return "Illegal payname $payname";
2499 ($payfirst, $paylast) = ($1, $2);
2501 $payfirst = $self->getfield('first');
2502 $paylast = $self->getfield('last');
2503 $payname = "$payfirst $paylast";
2506 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
2507 if ( $conf->exists('emailinvoiceauto')
2508 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
2509 push @invoicing_list, $self->all_emails;
2512 my $email = ($conf->exists('business-onlinepayment-email-override'))
2513 ? $conf->config('business-onlinepayment-email-override')
2514 : $invoicing_list[0];
2518 my $payip = exists($options{'payip'})
2521 $content{customer_ip} = $payip
2524 if ( $method eq 'CC' ) {
2526 $content{card_number} = $payinfo;
2527 my $paydate = exists($options{'paydate'})
2528 ? $options{'paydate'}
2530 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2531 $content{expiration} = "$2/$1";
2533 my $paycvv = exists($options{'paycvv'})
2534 ? $options{'paycvv'}
2536 $content{cvv2} = $self->paycvv
2539 my $paystart_month = exists($options{'paystart_month'})
2540 ? $options{'paystart_month'}
2541 : $self->paystart_month;
2543 my $paystart_year = exists($options{'paystart_year'})
2544 ? $options{'paystart_year'}
2545 : $self->paystart_year;
2547 $content{card_start} = "$paystart_month/$paystart_year"
2548 if $paystart_month && $paystart_year;
2550 my $payissue = exists($options{'payissue'})
2551 ? $options{'payissue'}
2553 $content{issue_number} = $payissue if $payissue;
2555 $content{recurring_billing} = 'YES'
2556 if qsearch('cust_pay', { 'custnum' => $self->custnum,
2558 'payinfo' => $payinfo,
2561 } elsif ( $method eq 'ECHECK' ) {
2562 ( $content{account_number}, $content{routing_code} ) =
2563 split('@', $payinfo);
2564 $content{bank_name} = $o_payname;
2565 $content{account_type} = 'CHECKING';
2566 $content{account_name} = $payname;
2567 $content{customer_org} = $self->company ? 'B' : 'I';
2568 $content{customer_ssn} = exists($options{'ss'})
2571 } elsif ( $method eq 'LEC' ) {
2572 $content{phone} = $payinfo;
2576 # run transaction(s)
2579 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
2581 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
2582 $transaction->content(
2585 'password' => $password,
2586 'action' => $action1,
2587 'description' => $options{'description'},
2588 'amount' => $amount,
2589 'invoice_number' => $options{'invnum'},
2590 'customer_id' => $self->custnum,
2591 'last_name' => $paylast,
2592 'first_name' => $payfirst,
2594 'address' => $address,
2595 'city' => ( exists($options{'city'})
2598 'state' => ( exists($options{'state'})
2601 'zip' => ( exists($options{'zip'})
2604 'country' => ( exists($options{'country'})
2605 ? $options{'country'}
2607 'referer' => 'http://cleanwhisker.420.am/',
2609 'phone' => $self->daytime || $self->night,
2612 $transaction->submit();
2614 if ( $transaction->is_success() && $action2 ) {
2615 my $auth = $transaction->authorization;
2616 my $ordernum = $transaction->can('order_number')
2617 ? $transaction->order_number
2621 new Business::OnlinePayment( $processor, @bop_options );
2628 password => $password,
2629 order_number => $ordernum,
2631 authorization => $auth,
2632 description => $options{'description'},
2635 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
2636 transaction_sequence_num local_transaction_date
2637 local_transaction_time AVS_result_code )) {
2638 $capture{$field} = $transaction->$field() if $transaction->can($field);
2641 $capture->content( %capture );
2645 unless ( $capture->is_success ) {
2646 my $e = "Authorization successful but capture failed, custnum #".
2647 $self->custnum. ': '. $capture->result_code.
2648 ": ". $capture->error_message;
2656 # remove paycvv after initial transaction
2659 #false laziness w/misc/process/payment.cgi - check both to make sure working
2661 if ( defined $self->dbdef_table->column('paycvv')
2662 && length($self->paycvv)
2663 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
2665 my $error = $self->remove_cvv;
2667 warn "WARNING: error removing cvv: $error\n";
2675 if ( $transaction->is_success() ) {
2677 my %method2payby = (
2684 if ( $payment_gateway ) { # agent override
2685 $paybatch = $payment_gateway->gatewaynum. '-';
2688 $paybatch .= "$processor:". $transaction->authorization;
2690 $paybatch .= ':'. $transaction->order_number
2691 if $transaction->can('order_number')
2692 && length($transaction->order_number);
2694 my $cust_pay = new FS::cust_pay ( {
2695 'custnum' => $self->custnum,
2696 'invnum' => $options{'invnum'},
2699 'payby' => $method2payby{$method},
2700 'payinfo' => $payinfo,
2701 'paybatch' => $paybatch,
2703 my $error = $cust_pay->insert;
2705 $cust_pay->invnum(''); #try again with no specific invnum
2706 my $error2 = $cust_pay->insert;
2708 # gah, even with transactions.
2709 my $e = 'WARNING: Card/ACH debited but database not updated - '.
2710 "error inserting payment ($processor): $error2".
2711 " (previously tried insert with invnum #$options{'invnum'}" .
2717 return ''; #no error
2721 my $perror = "$processor error: ". $transaction->error_message;
2723 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
2724 && $conf->exists('emaildecline')
2725 && grep { $_ ne 'POST' } $self->invoicing_list
2726 && ! grep { $transaction->error_message =~ /$_/ }
2727 $conf->config('emaildecline-exclude')
2729 my @templ = $conf->config('declinetemplate');
2730 my $template = new Text::Template (
2732 SOURCE => [ map "$_\n", @templ ],
2733 ) or return "($perror) can't create template: $Text::Template::ERROR";
2734 $template->compile()
2735 or return "($perror) can't compile template: $Text::Template::ERROR";
2737 my $templ_hash = { error => $transaction->error_message };
2739 my $error = send_email(
2740 'from' => $conf->config('invoice_from'),
2741 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
2742 'subject' => 'Your payment could not be processed',
2743 'body' => [ $template->fill_in(HASH => $templ_hash) ],
2746 $perror .= " (also received error sending decline notification: $error)"
2756 =item default_payment_gateway
2760 sub default_payment_gateway {
2761 my( $self, $method ) = @_;
2763 die "Real-time processing not enabled\n"
2764 unless $conf->exists('business-onlinepayment');
2767 my $bop_config = 'business-onlinepayment';
2768 $bop_config .= '-ach'
2769 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2770 my ( $processor, $login, $password, $action, @bop_options ) =
2771 $conf->config($bop_config);
2772 $action ||= 'normal authorization';
2773 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2774 die "No real-time processor is enabled - ".
2775 "did you set the business-onlinepayment configuration value?\n"
2778 ( $processor, $login, $password, $action, @bop_options )
2783 Removes the I<paycvv> field from the database directly.
2785 If there is an error, returns the error, otherwise returns false.
2791 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2792 or return dbh->errstr;
2793 $sth->execute($self->custnum)
2794 or return $sth->errstr;
2799 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
2801 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
2802 via a Business::OnlinePayment realtime gateway. See
2803 L<http://420.am/business-onlinepayment> for supported gateways.
2805 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2807 Available options are: I<amount>, I<reason>, I<paynum>
2809 Most gateways require a reference to an original payment transaction to refund,
2810 so you probably need to specify a I<paynum>.
2812 I<amount> defaults to the original amount of the payment if not specified.
2814 I<reason> specifies a reason for the refund.
2816 Implementation note: If I<amount> is unspecified or equal to the amount of the
2817 orignal payment, first an attempt is made to "void" the transaction via
2818 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2819 the normal attempt is made to "refund" ("credit") the transaction via the
2820 gateway is attempted.
2822 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2823 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2824 #if set, will override the value from the customer record.
2826 #If an I<invnum> is specified, this payment (if successful) is applied to the
2827 #specified invoice. If you don't specify an I<invnum> you might want to
2828 #call the B<apply_payments> method.
2832 #some false laziness w/realtime_bop, not enough to make it worth merging
2833 #but some useful small subs should be pulled out
2834 sub realtime_refund_bop {
2835 my( $self, $method, %options ) = @_;
2837 warn "$me realtime_refund_bop: $method refund\n";
2838 warn " $_ => $options{$_}\n" foreach keys %options;
2841 eval "use Business::OnlinePayment";
2845 # look up the original payment and optionally a gateway for that payment
2849 my $amount = $options{'amount'};
2851 my( $processor, $login, $password, @bop_options ) ;
2852 my( $auth, $order_number ) = ( '', '', '' );
2854 if ( $options{'paynum'} ) {
2856 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
2857 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2858 or return "Unknown paynum $options{'paynum'}";
2859 $amount ||= $cust_pay->paid;
2861 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-]*)(:([\w\-]+))?$/
2862 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2863 $cust_pay->paybatch;
2864 my $gatewaynum = '';
2865 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
2867 if ( $gatewaynum ) { #gateway for the payment to be refunded
2869 my $payment_gateway =
2870 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
2871 die "payment gateway $gatewaynum not found"
2872 unless $payment_gateway;
2874 $processor = $payment_gateway->gateway_module;
2875 $login = $payment_gateway->gateway_username;
2876 $password = $payment_gateway->gateway_password;
2877 @bop_options = $payment_gateway->options;
2879 } else { #try the default gateway
2881 my( $conf_processor, $unused_action );
2882 ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
2883 $self->default_payment_gateway($method);
2885 return "processor of payment $options{'paynum'} $processor does not".
2886 " match default processor $conf_processor"
2887 unless $processor eq $conf_processor;
2892 } else { # didn't specify a paynum, so look for agent gateway overrides
2893 # like a normal transaction
2896 if ( $method eq 'CC' ) {
2897 $cardtype = cardtype($self->payinfo);
2898 } elsif ( $method eq 'ECHECK' ) {
2901 $cardtype = $method;
2904 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2905 cardtype => $cardtype,
2907 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2909 taxclass => '', } );
2911 if ( $override ) { #use a payment gateway override
2913 my $payment_gateway = $override->payment_gateway;
2915 $processor = $payment_gateway->gateway_module;
2916 $login = $payment_gateway->gateway_username;
2917 $password = $payment_gateway->gateway_password;
2918 #$action = $payment_gateway->gateway_action;
2919 @bop_options = $payment_gateway->options;
2921 } else { #use the standard settings from the config
2924 ( $processor, $login, $password, $unused_action, @bop_options ) =
2925 $self->default_payment_gateway($method);
2930 return "neither amount nor paynum specified" unless $amount;
2935 'password' => $password,
2936 'order_number' => $order_number,
2937 'amount' => $amount,
2938 'referer' => 'http://cleanwhisker.420.am/',
2940 $content{authorization} = $auth
2941 if length($auth); #echeck/ACH transactions have an order # but no auth
2942 #(at least with authorize.net)
2944 #first try void if applicable
2945 if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2946 warn " attempting void\n" if $DEBUG > 1;
2947 my $void = new Business::OnlinePayment( $processor, @bop_options );
2948 $void->content( 'action' => 'void', %content );
2950 if ( $void->is_success ) {
2951 my $error = $cust_pay->void($options{'reason'});
2953 # gah, even with transactions.
2954 my $e = 'WARNING: Card/ACH voided but database not updated - '.
2955 "error voiding payment: $error";
2959 warn " void successful\n" if $DEBUG > 1;
2964 warn " void unsuccessful, trying refund\n"
2968 my $address = $self->address1;
2969 $address .= ", ". $self->address2 if $self->address2;
2971 my($payname, $payfirst, $paylast);
2972 if ( $self->payname && $method ne 'ECHECK' ) {
2973 $payname = $self->payname;
2974 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2975 or return "Illegal payname $payname";
2976 ($payfirst, $paylast) = ($1, $2);
2978 $payfirst = $self->getfield('first');
2979 $paylast = $self->getfield('last');
2980 $payname = "$payfirst $paylast";
2983 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
2984 if ( $conf->exists('emailinvoiceauto')
2985 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
2986 push @invoicing_list, $self->all_emails;
2989 my $email = ($conf->exists('business-onlinepayment-email-override'))
2990 ? $conf->config('business-onlinepayment-email-override')
2991 : $invoicing_list[0];
2993 my $payip = exists($options{'payip'})
2996 $content{customer_ip} = $payip
3000 if ( $method eq 'CC' ) {
3003 $content{card_number} = $payinfo = $cust_pay->payinfo;
3004 #$self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
3005 #$content{expiration} = "$2/$1";
3007 $content{card_number} = $payinfo = $self->payinfo;
3008 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
3009 $content{expiration} = "$2/$1";
3012 } elsif ( $method eq 'ECHECK' ) {
3013 ( $content{account_number}, $content{routing_code} ) =
3014 split('@', $payinfo = $self->payinfo);
3015 $content{bank_name} = $self->payname;
3016 $content{account_type} = 'CHECKING';
3017 $content{account_name} = $payname;
3018 $content{customer_org} = $self->company ? 'B' : 'I';
3019 $content{customer_ssn} = $self->ss;
3020 } elsif ( $method eq 'LEC' ) {
3021 $content{phone} = $payinfo = $self->payinfo;
3025 my $refund = new Business::OnlinePayment( $processor, @bop_options );
3026 my %sub_content = $refund->content(
3027 'action' => 'credit',
3028 'customer_id' => $self->custnum,
3029 'last_name' => $paylast,
3030 'first_name' => $payfirst,
3032 'address' => $address,
3033 'city' => $self->city,
3034 'state' => $self->state,
3035 'zip' => $self->zip,
3036 'country' => $self->country,
3038 'phone' => $self->daytime || $self->night,
3041 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
3045 return "$processor error: ". $refund->error_message
3046 unless $refund->is_success();
3048 my %method2payby = (
3054 my $paybatch = "$processor:". $refund->authorization;
3055 $paybatch .= ':'. $refund->order_number
3056 if $refund->can('order_number') && $refund->order_number;
3058 while ( $cust_pay && $cust_pay->unappled < $amount ) {
3059 my @cust_bill_pay = $cust_pay->cust_bill_pay;
3060 last unless @cust_bill_pay;
3061 my $cust_bill_pay = pop @cust_bill_pay;
3062 my $error = $cust_bill_pay->delete;
3066 my $cust_refund = new FS::cust_refund ( {
3067 'custnum' => $self->custnum,
3068 'paynum' => $options{'paynum'},
3069 'refund' => $amount,
3071 'payby' => $method2payby{$method},
3072 'payinfo' => $payinfo,
3073 'paybatch' => $paybatch,
3074 'reason' => $options{'reason'} || 'card or ACH refund',
3076 my $error = $cust_refund->insert;
3078 $cust_refund->paynum(''); #try again with no specific paynum
3079 my $error2 = $cust_refund->insert;
3081 # gah, even with transactions.
3082 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
3083 "error inserting refund ($processor): $error2".
3084 " (previously tried insert with paynum #$options{'paynum'}" .
3097 Returns the total owed for this customer on all invoices
3098 (see L<FS::cust_bill/owed>).
3104 $self->total_owed_date(2145859200); #12/31/2037
3107 =item total_owed_date TIME
3109 Returns the total owed for this customer on all invoices with date earlier than
3110 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
3111 see L<Time::Local> and L<Date::Parse> for conversion functions.
3115 sub total_owed_date {
3119 foreach my $cust_bill (
3120 grep { $_->_date <= $time }
3121 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3123 $total_bill += $cust_bill->owed;
3125 sprintf( "%.2f", $total_bill );
3128 =item apply_credits OPTION => VALUE ...
3130 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
3131 to outstanding invoice balances in chronological order (or reverse
3132 chronological order if the I<order> option is set to B<newest>) and returns the
3133 value of any remaining unapplied credits available for refund (see
3134 L<FS::cust_refund>).
3142 return 0 unless $self->total_credited;
3144 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
3145 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
3147 my @invoices = $self->open_cust_bill;
3148 @invoices = sort { $b->_date <=> $a->_date } @invoices
3149 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
3152 foreach my $cust_bill ( @invoices ) {
3155 if ( !defined($credit) || $credit->credited == 0) {
3156 $credit = pop @credits or last;
3159 if ($cust_bill->owed >= $credit->credited) {
3160 $amount=$credit->credited;
3162 $amount=$cust_bill->owed;
3165 my $cust_credit_bill = new FS::cust_credit_bill ( {
3166 'crednum' => $credit->crednum,
3167 'invnum' => $cust_bill->invnum,
3168 'amount' => $amount,
3170 my $error = $cust_credit_bill->insert;
3171 die $error if $error;
3173 redo if ($cust_bill->owed > 0);
3177 return $self->total_credited;
3180 =item apply_payments
3182 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
3183 to outstanding invoice balances in chronological order.
3185 #and returns the value of any remaining unapplied payments.
3189 sub apply_payments {
3194 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
3195 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
3197 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
3198 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
3202 foreach my $cust_bill ( @invoices ) {
3205 if ( !defined($payment) || $payment->unapplied == 0 ) {
3206 $payment = pop @payments or last;
3209 if ( $cust_bill->owed >= $payment->unapplied ) {
3210 $amount = $payment->unapplied;
3212 $amount = $cust_bill->owed;
3215 my $cust_bill_pay = new FS::cust_bill_pay ( {
3216 'paynum' => $payment->paynum,
3217 'invnum' => $cust_bill->invnum,
3218 'amount' => $amount,
3220 my $error = $cust_bill_pay->insert;
3221 die $error if $error;
3223 redo if ( $cust_bill->owed > 0);
3227 return $self->total_unapplied_payments;
3230 =item total_credited
3232 Returns the total outstanding credit (see L<FS::cust_credit>) for this
3233 customer. See L<FS::cust_credit/credited>.
3237 sub total_credited {
3239 my $total_credit = 0;
3240 foreach my $cust_credit ( qsearch('cust_credit', {
3241 'custnum' => $self->custnum,
3243 $total_credit += $cust_credit->credited;
3245 sprintf( "%.2f", $total_credit );
3248 =item total_unapplied_payments
3250 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
3251 See L<FS::cust_pay/unapplied>.
3255 sub total_unapplied_payments {
3257 my $total_unapplied = 0;
3258 foreach my $cust_pay ( qsearch('cust_pay', {
3259 'custnum' => $self->custnum,
3261 $total_unapplied += $cust_pay->unapplied;
3263 sprintf( "%.2f", $total_unapplied );
3268 Returns the balance for this customer (total_owed minus total_credited
3269 minus total_unapplied_payments).
3276 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
3280 =item balance_date TIME
3282 Returns the balance for this customer, only considering invoices with date
3283 earlier than TIME (total_owed_date minus total_credited minus
3284 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
3285 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
3294 $self->total_owed_date($time)
3295 - $self->total_credited
3296 - $self->total_unapplied_payments
3300 =item in_transit_payments
3302 Returns the total of requests for payments for this customer pending in
3303 batches in transit to the bank. See L<FS::pay_batch> and L<FS::cust_pay_batch>
3307 sub in_transit_payments {
3309 my $in_transit_payments = 0;
3310 foreach my $pay_batch ( qsearch('pay_batch', {
3313 foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
3314 'batchnum' => $pay_batch->batchnum,
3315 'custnum' => $self->custnum,
3317 $in_transit_payments += $cust_pay_batch->amount;
3320 sprintf( "%.2f", $in_transit_payments );
3323 =item paydate_monthyear
3325 Returns a two-element list consisting of the month and year of this customer's
3326 paydate (credit card expiration date for CARD customers)
3330 sub paydate_monthyear {
3332 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
3334 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
3341 =item payinfo_masked
3343 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.
3345 Credit Cards - Mask all but the last four characters.
3346 Checks - Mask all but last 2 of account number and bank routing number.
3347 Others - Do nothing, return the unmasked string.
3351 sub payinfo_masked {
3353 return $self->paymask;
3356 =item invoicing_list [ ARRAYREF ]
3358 If an arguement is given, sets these email addresses as invoice recipients
3359 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
3360 (except as warnings), so use check_invoicing_list first.
3362 Returns a list of email addresses (with svcnum entries expanded).
3364 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
3365 check it without disturbing anything by passing nothing.
3367 This interface may change in the future.
3371 sub invoicing_list {
3372 my( $self, $arrayref ) = @_;
3375 my @cust_main_invoice;
3376 if ( $self->custnum ) {
3377 @cust_main_invoice =
3378 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3380 @cust_main_invoice = ();
3382 foreach my $cust_main_invoice ( @cust_main_invoice ) {
3383 #warn $cust_main_invoice->destnum;
3384 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
3385 #warn $cust_main_invoice->destnum;
3386 my $error = $cust_main_invoice->delete;
3387 warn $error if $error;
3390 if ( $self->custnum ) {
3391 @cust_main_invoice =
3392 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3394 @cust_main_invoice = ();
3396 my %seen = map { $_->address => 1 } @cust_main_invoice;
3397 foreach my $address ( @{$arrayref} ) {
3398 next if exists $seen{$address} && $seen{$address};
3399 $seen{$address} = 1;
3400 my $cust_main_invoice = new FS::cust_main_invoice ( {
3401 'custnum' => $self->custnum,
3404 my $error = $cust_main_invoice->insert;
3405 warn $error if $error;
3409 if ( $self->custnum ) {
3411 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3418 =item check_invoicing_list ARRAYREF
3420 Checks these arguements as valid input for the invoicing_list method. If there
3421 is an error, returns the error, otherwise returns false.
3425 sub check_invoicing_list {
3426 my( $self, $arrayref ) = @_;
3427 foreach my $address ( @{$arrayref} ) {
3429 if ($address eq 'FAX' and $self->getfield('fax') eq '') {
3430 return 'Can\'t add FAX invoice destination with a blank FAX number.';
3433 my $cust_main_invoice = new FS::cust_main_invoice ( {
3434 'custnum' => $self->custnum,
3437 my $error = $self->custnum
3438 ? $cust_main_invoice->check
3439 : $cust_main_invoice->checkdest
3441 return $error if $error;
3446 =item set_default_invoicing_list
3448 Sets the invoicing list to all accounts associated with this customer,
3449 overwriting any previous invoicing list.
3453 sub set_default_invoicing_list {
3455 $self->invoicing_list($self->all_emails);
3460 Returns the email addresses of all accounts provisioned for this customer.
3467 foreach my $cust_pkg ( $self->all_pkgs ) {
3468 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
3470 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3471 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3473 $list{$_}=1 foreach map { $_->email } @svc_acct;
3478 =item invoicing_list_addpost
3480 Adds postal invoicing to this customer. If this customer is already configured
3481 to receive postal invoices, does nothing.
3485 sub invoicing_list_addpost {
3487 return if grep { $_ eq 'POST' } $self->invoicing_list;
3488 my @invoicing_list = $self->invoicing_list;
3489 push @invoicing_list, 'POST';
3490 $self->invoicing_list(\@invoicing_list);
3493 =item invoicing_list_emailonly
3495 Returns the list of email invoice recipients (invoicing_list without non-email
3496 destinations such as POST and FAX).
3500 sub invoicing_list_emailonly {
3502 grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
3505 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3507 Returns an array of customers referred by this customer (referral_custnum set
3508 to this custnum). If DEPTH is given, recurses up to the given depth, returning
3509 customers referred by customers referred by this customer and so on, inclusive.
3510 The default behavior is DEPTH 1 (no recursion).
3514 sub referral_cust_main {
3516 my $depth = @_ ? shift : 1;
3517 my $exclude = @_ ? shift : {};
3520 map { $exclude->{$_->custnum}++; $_; }
3521 grep { ! $exclude->{ $_->custnum } }
3522 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3526 map { $_->referral_cust_main($depth-1, $exclude) }
3533 =item referral_cust_main_ncancelled
3535 Same as referral_cust_main, except only returns customers with uncancelled
3540 sub referral_cust_main_ncancelled {
3542 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3545 =item referral_cust_pkg [ DEPTH ]
3547 Like referral_cust_main, except returns a flat list of all unsuspended (and
3548 uncancelled) packages for each customer. The number of items in this list may
3549 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3553 sub referral_cust_pkg {
3555 my $depth = @_ ? shift : 1;
3557 map { $_->unsuspended_pkgs }
3558 grep { $_->unsuspended_pkgs }
3559 $self->referral_cust_main($depth);
3562 =item referring_cust_main
3564 Returns the single cust_main record for the customer who referred this customer
3565 (referral_custnum), or false.
3569 sub referring_cust_main {
3571 return '' unless $self->referral_custnum;
3572 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3575 =item credit AMOUNT, REASON
3577 Applies a credit to this customer. If there is an error, returns the error,
3578 otherwise returns false.
3583 my( $self, $amount, $reason ) = @_;
3584 my $cust_credit = new FS::cust_credit {
3585 'custnum' => $self->custnum,
3586 'amount' => $amount,
3587 'reason' => $reason,
3589 $cust_credit->insert;
3592 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3594 Creates a one-time charge for this customer. If there is an error, returns
3595 the error, otherwise returns false.
3600 my ( $self, $amount ) = ( shift, shift );
3601 my $pkg = @_ ? shift : 'One-time charge';
3602 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
3603 my $taxclass = @_ ? shift : '';
3605 local $SIG{HUP} = 'IGNORE';
3606 local $SIG{INT} = 'IGNORE';
3607 local $SIG{QUIT} = 'IGNORE';
3608 local $SIG{TERM} = 'IGNORE';
3609 local $SIG{TSTP} = 'IGNORE';
3610 local $SIG{PIPE} = 'IGNORE';
3612 my $oldAutoCommit = $FS::UID::AutoCommit;
3613 local $FS::UID::AutoCommit = 0;
3616 my $part_pkg = new FS::part_pkg ( {
3618 'comment' => $comment,
3619 #'setup' => $amount,
3622 'plandata' => "setup_fee=$amount",
3625 'taxclass' => $taxclass,
3628 my $error = $part_pkg->insert;
3630 $dbh->rollback if $oldAutoCommit;
3634 my $pkgpart = $part_pkg->pkgpart;
3635 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3636 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3637 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3638 $error = $type_pkgs->insert;
3640 $dbh->rollback if $oldAutoCommit;
3645 my $cust_pkg = new FS::cust_pkg ( {
3646 'custnum' => $self->custnum,
3647 'pkgpart' => $pkgpart,
3650 $error = $cust_pkg->insert;
3652 $dbh->rollback if $oldAutoCommit;
3656 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3663 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3669 sort { $a->_date <=> $b->_date }
3670 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3673 =item open_cust_bill
3675 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3680 sub open_cust_bill {
3682 grep { $_->owed > 0 } $self->cust_bill;
3687 Returns all the credits (see L<FS::cust_credit>) for this customer.
3693 sort { $a->_date <=> $b->_date }
3694 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3699 Returns all the payments (see L<FS::cust_pay>) for this customer.
3705 sort { $a->_date <=> $b->_date }
3706 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
3711 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3717 sort { $a->_date <=> $b->_date }
3718 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3724 Returns all the refunds (see L<FS::cust_refund>) for this customer.
3730 sort { $a->_date <=> $b->_date }
3731 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
3734 =item select_for_update
3736 Selects this record with the SQL "FOR UPDATE" command. This can be useful as
3741 sub select_for_update {
3743 qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
3748 Returns a name string for this customer, either "Company (Last, First)" or
3755 my $name = $self->contact;
3756 $name = $self->company. " ($name)" if $self->company;
3762 Returns a name string for this (service/shipping) contact, either
3763 "Company (Last, First)" or "Last, First".
3769 if ( $self->get('ship_last') ) {
3770 my $name = $self->ship_contact;
3771 $name = $self->ship_company. " ($name)" if $self->ship_company;
3780 Returns this customer's full (billing) contact name only, "Last, First"
3786 $self->get('last'). ', '. $self->first;
3791 Returns this customer's full (shipping) contact name only, "Last, First"
3797 $self->get('ship_last')
3798 ? $self->get('ship_last'). ', '. $self->ship_first
3804 Returns this customer's full country name
3810 code2country($self->country);
3815 Returns a status string for this customer, currently:
3819 =item prospect - No packages have ever been ordered
3821 =item active - One or more recurring packages is active
3823 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
3825 =item suspended - All non-cancelled recurring packages are suspended
3827 =item cancelled - All recurring packages are cancelled
3835 for my $status (qw( prospect active inactive suspended cancelled )) {
3836 my $method = $status.'_sql';
3837 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
3838 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
3839 $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
3840 return $status if $sth->fetchrow_arrayref->[0];
3846 Returns a hex triplet color string for this customer's status.
3850 use vars qw(%statuscolor);
3852 'prospect' => '7e0079', #'000000', #black? naw, purple
3853 'active' => '00CC00', #green
3854 'inactive' => '0000CC', #blue
3855 'suspended' => 'FF9900', #yellow
3856 'cancelled' => 'FF0000', #red
3861 $statuscolor{$self->status};
3866 =head1 CLASS METHODS
3872 Returns an SQL expression identifying prospective cust_main records (customers
3873 with no packages ever ordered)
3877 use vars qw($select_count_pkgs);
3878 $select_count_pkgs =
3879 "SELECT COUNT(*) FROM cust_pkg
3880 WHERE cust_pkg.custnum = cust_main.custnum";
3882 sub select_count_pkgs_sql {
3886 sub prospect_sql { "
3887 0 = ( $select_count_pkgs )
3892 Returns an SQL expression identifying active cust_main records (customers with
3893 no active recurring packages, but otherwise unsuspended/uncancelled).
3898 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
3904 Returns an SQL expression identifying inactive cust_main records (customers with
3905 active recurring packages).
3909 sub inactive_sql { "
3910 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
3912 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
3918 Returns an SQL expression identifying suspended cust_main records.
3923 sub suspended_sql { susp_sql(@_); }
3925 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
3927 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
3933 Returns an SQL expression identifying cancelled cust_main records.
3937 sub cancelled_sql { cancel_sql(@_); }
3940 my $recurring_sql = FS::cust_pkg->recurring_sql;
3941 #my $recurring_sql = "
3942 # '0' != ( select freq from part_pkg
3943 # where cust_pkg.pkgpart = part_pkg.pkgpart )
3947 0 < ( $select_count_pkgs )
3948 AND 0 = ( $select_count_pkgs AND $recurring_sql
3949 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
3955 =item uncancelled_sql
3957 Returns an SQL expression identifying un-cancelled cust_main records.
3961 sub uncancelled_sql { uncancel_sql(@_); }
3962 sub uncancel_sql { "
3963 ( 0 < ( $select_count_pkgs
3964 AND ( cust_pkg.cancel IS NULL
3965 OR cust_pkg.cancel = 0
3968 OR 0 = ( $select_count_pkgs )
3972 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
3974 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
3975 records. Currently, I<first>, I<last> and/or I<company> may be specified (the
3976 appropriate ship_ field is also searched).
3978 Additional options are the same as FS::Record::qsearch
3983 my( $self, $fuzzy, $hash, @opt) = @_;
3988 check_and_rebuild_fuzzyfiles();
3989 foreach my $field ( keys %$fuzzy ) {
3991 $match{$_}=1 foreach ( amatch( $fuzzy->{$field},
3993 @{ $self->all_X($field) }
3998 foreach ( keys %match ) {
3999 push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
4000 push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
4003 push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
4006 # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
4008 @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main;
4020 =item smart_search OPTION => VALUE ...
4022 Accepts the following options: I<search>, the string to search for. The string
4023 will be searched for as a customer number, phone number, name or company name,
4024 first searching for an exact match then fuzzy and substring matches (in some
4025 cases - see the source code for the exact heuristics used).
4027 Any additional options treated as an additional qualifier on the search
4030 Returns a (possibly empty) array of FS::cust_main objects.
4037 #here is the agent virtualization
4038 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
4042 my $search = delete $options{'search'};
4043 ( my $alphanum_search = $search ) =~ s/\W//g;
4045 if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search
4047 #false laziness w/Record::ut_phone
4048 my $phonen = "$1-$2-$3";
4049 $phonen .= " x$4" if $4;
4051 push @cust_main, qsearch( {
4052 'table' => 'cust_main',
4053 'hashref' => { %options },
4054 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
4056 join(' OR ', map "$_ = '$phonen'",
4057 qw( daytime night fax
4058 ship_daytime ship_night ship_fax )
4061 " AND $agentnums_sql", #agent virtualization
4064 unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
4065 #try looking for matches with extensions unless one was specified
4067 push @cust_main, qsearch( {
4068 'table' => 'cust_main',
4069 'hashref' => { %options },
4070 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
4072 join(' OR ', map "$_ LIKE '$phonen\%'",
4074 ship_daytime ship_night )
4077 " AND $agentnums_sql", #agent virtualization
4082 } elsif ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
4084 push @cust_main, qsearch( {
4085 'table' => 'cust_main',
4086 'hashref' => { 'custnum' => $1, %options },
4087 'extra_sql' => " AND $agentnums_sql", #agent virtualization
4090 } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
4092 my($company, $last, $first) = ( $1, $2, $3 );
4094 # "Company (Last, First)"
4095 #this is probably something a browser remembered,
4096 #so just do an exact search
4098 foreach my $prefix ( '', 'ship_' ) {
4099 push @cust_main, qsearch( {
4100 'table' => 'cust_main',
4101 'hashref' => { $prefix.'first' => $first,
4102 $prefix.'last' => $last,
4103 $prefix.'company' => $company,
4106 'extra_sql' => " AND $agentnums_sql",
4110 } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
4111 # try (ship_){last,company}
4115 # # remove "(Last, First)" in "Company (Last, First)", otherwise the
4116 # # full strings the browser remembers won't work
4117 # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name
4119 use Lingua::EN::NameParse;
4120 my $NameParse = new Lingua::EN::NameParse(
4122 allow_reversed => 1,
4125 my($last, $first) = ( '', '' );
4126 #maybe disable this too and just rely on NameParse?
4127 if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First
4129 ($last, $first) = ( $1, $2 );
4131 #} elsif ( $value =~ /^(.+)\s+(.+)$/ ) {
4132 } elsif ( ! $NameParse->parse($value) ) {
4134 my %name = $NameParse->components;
4135 $first = $name{'given_name_1'};
4136 $last = $name{'surname_1'};
4140 if ( $first && $last ) {
4142 my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) );
4145 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
4147 ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
4148 OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
4151 push @cust_main, qsearch( {
4152 'table' => 'cust_main',
4153 'hashref' => \%options,
4154 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
4157 # or it just be something that was typed in... (try that in a sec)
4161 my $q_value = dbh->quote($value);
4164 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
4165 $sql .= " ( LOWER(last) = $q_value
4166 OR LOWER(company) = $q_value
4167 OR LOWER(ship_last) = $q_value
4168 OR LOWER(ship_company) = $q_value
4171 push @cust_main, qsearch( {
4172 'table' => 'cust_main',
4173 'hashref' => \%options,
4174 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
4177 unless ( @cust_main ) { #no exact match, trying substring/fuzzy
4179 #still some false laziness w/ search/cust_main.cgi
4184 { 'company' => { op=>'ILIKE', value=>"%$value%" }, },
4185 { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
4188 if ( $first && $last ) {
4191 { 'first' => { op=>'ILIKE', value=>"%$first%" },
4192 'last' => { op=>'ILIKE', value=>"%$last%" },
4194 { 'ship_first' => { op=>'ILIKE', value=>"%$first%" },
4195 'ship_last' => { op=>'ILIKE', value=>"%$last%" },
4202 { 'last' => { op=>'ILIKE', value=>"%$value%" }, },
4203 { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, },
4207 foreach my $hashref ( @hashrefs ) {
4209 push @cust_main, qsearch( {
4210 'table' => 'cust_main',
4211 'hashref' => { %$hashref,
4214 'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton
4223 " AND $agentnums_sql", #extra_sql #agent virtualization
4226 if ( $first && $last ) {
4227 push @cust_main, FS::cust_main->fuzzy_search(
4228 { 'last' => $last, #fuzzy hashref
4229 'first' => $first }, #
4233 foreach my $field ( 'last', 'company' ) {
4235 FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
4240 #eliminate duplicates
4242 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
4250 =item check_and_rebuild_fuzzyfiles
4254 use vars qw(@fuzzyfields);
4255 @fuzzyfields = ( 'last', 'first', 'company' );
4257 sub check_and_rebuild_fuzzyfiles {
4258 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4259 rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
4262 =item rebuild_fuzzyfiles
4266 sub rebuild_fuzzyfiles {
4268 use Fcntl qw(:flock);
4270 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4271 mkdir $dir, 0700 unless -d $dir;
4273 foreach my $fuzzy ( @fuzzyfields ) {
4275 open(LOCK,">>$dir/cust_main.$fuzzy")
4276 or die "can't open $dir/cust_main.$fuzzy: $!";
4278 or die "can't lock $dir/cust_main.$fuzzy: $!";
4280 open (CACHE,">$dir/cust_main.$fuzzy.tmp")
4281 or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
4283 foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
4284 my $sth = dbh->prepare("SELECT $field FROM cust_main".
4285 " WHERE $field != '' AND $field IS NOT NULL");
4286 $sth->execute or die $sth->errstr;
4288 while ( my $row = $sth->fetchrow_arrayref ) {
4289 print CACHE $row->[0]. "\n";
4294 close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
4296 rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
4307 my( $self, $field ) = @_;
4308 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4309 open(CACHE,"<$dir/cust_main.$field")
4310 or die "can't open $dir/cust_main.$field: $!";
4311 my @array = map { chomp; $_; } <CACHE>;
4316 =item append_fuzzyfiles LASTNAME COMPANY
4320 sub append_fuzzyfiles {
4321 #my( $first, $last, $company ) = @_;
4323 &check_and_rebuild_fuzzyfiles;
4325 use Fcntl qw(:flock);
4327 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
4329 foreach my $field (qw( first last company )) {
4334 open(CACHE,">>$dir/cust_main.$field")
4335 or die "can't open $dir/cust_main.$field: $!";
4336 flock(CACHE,LOCK_EX)
4337 or die "can't lock $dir/cust_main.$field: $!";
4339 print CACHE "$value\n";
4341 flock(CACHE,LOCK_UN)
4342 or die "can't unlock $dir/cust_main.$field: $!";
4357 #warn join('-',keys %$param);
4358 my $fh = $param->{filehandle};
4359 my $agentnum = $param->{agentnum};
4361 my $refnum = $param->{refnum};
4362 my $pkgpart = $param->{pkgpart};
4364 #my @fields = @{$param->{fields}};
4365 my $format = $param->{'format'};
4368 if ( $format eq 'simple' ) {
4369 @fields = qw( cust_pkg.setup dayphone first last
4370 address1 address2 city state zip comments );
4372 } elsif ( $format eq 'extended' ) {
4373 @fields = qw( agent_custid refnum
4374 last first address1 address2 city state zip country
4376 ship_last ship_first ship_address1 ship_address2
4377 ship_city ship_state ship_zip ship_country
4378 payinfo paycvv paydate
4381 svc_acct.username svc_acct._password
4385 die "unknown format $format";
4388 eval "use Text::CSV_XS;";
4391 my $csv = new Text::CSV_XS;
4398 local $SIG{HUP} = 'IGNORE';
4399 local $SIG{INT} = 'IGNORE';
4400 local $SIG{QUIT} = 'IGNORE';
4401 local $SIG{TERM} = 'IGNORE';
4402 local $SIG{TSTP} = 'IGNORE';
4403 local $SIG{PIPE} = 'IGNORE';
4405 my $oldAutoCommit = $FS::UID::AutoCommit;
4406 local $FS::UID::AutoCommit = 0;
4409 #while ( $columns = $csv->getline($fh) ) {
4411 while ( defined($line=<$fh>) ) {
4413 $csv->parse($line) or do {
4414 $dbh->rollback if $oldAutoCommit;
4415 return "can't parse: ". $csv->error_input();
4418 my @columns = $csv->fields();
4419 #warn join('-',@columns);
4422 agentnum => $agentnum,
4424 country => $conf->config('countrydefault') || 'US',
4425 payby => $payby, #default
4426 paydate => '12/2037', #default
4428 my $billtime = time;
4429 my %cust_pkg = ( pkgpart => $pkgpart );
4431 foreach my $field ( @fields ) {
4433 if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|expire|cancel)$/ ) {
4435 #$cust_pkg{$1} = str2time( shift @$columns );
4436 if ( $1 eq 'pkgpart' ) {
4437 $cust_pkg{$1} = shift @columns;
4438 } elsif ( $1 eq 'setup' ) {
4439 $billtime = str2time(shift @columns);
4441 $cust_pkg{$1} = str2time( shift @columns );
4444 } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
4446 $svc_acct{$1} = shift @columns;
4450 #refnum interception
4451 if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
4453 my $referral = $columns[0];
4454 my $part_referral = new FS::part_referral {
4455 'referral' => $referral,
4456 'agentnum' => $agentnum,
4459 my $error = $part_referral->insert;
4461 $dbh->rollback if $oldAutoCommit;
4462 return "can't auto-insert advertising source: $referral: $error";
4464 $columns[0] = $part_referral->refnum;
4467 #$cust_main{$field} = shift @$columns;
4468 $cust_main{$field} = shift @columns;
4472 my $invoicing_list = $cust_main{'invoicing_list'}
4473 ? [ delete $cust_main{'invoicing_list'} ]
4476 my $cust_main = new FS::cust_main ( \%cust_main );
4479 tie my %hash, 'Tie::RefHash'; #this part is important
4481 if ( $cust_pkg{'pkgpart'} ) {
4482 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
4485 if ( $svc_acct{'username'} ) {
4486 $svc_acct{svcpart} = $cust_pkg->part_pkg->svcpart( 'svc_acct' );
4487 push @svc_acct, new FS::svc_acct ( \%svc_acct )
4490 $hash{$cust_pkg} = \@svc_acct;
4493 my $error = $cust_main->insert( \%hash, $invoicing_list );
4496 $dbh->rollback if $oldAutoCommit;
4497 return "can't insert customer for $line: $error";
4500 if ( $format eq 'simple' ) {
4502 #false laziness w/bill.cgi
4503 $error = $cust_main->bill( 'time' => $billtime );
4505 $dbh->rollback if $oldAutoCommit;
4506 return "can't bill customer for $line: $error";
4509 $cust_main->apply_payments;
4510 $cust_main->apply_credits;
4512 $error = $cust_main->collect();
4514 $dbh->rollback if $oldAutoCommit;
4515 return "can't collect customer for $line: $error";
4523 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4525 return "Empty file!" unless $imported;
4537 #warn join('-',keys %$param);
4538 my $fh = $param->{filehandle};
4539 my @fields = @{$param->{fields}};
4541 eval "use Text::CSV_XS;";
4544 my $csv = new Text::CSV_XS;
4551 local $SIG{HUP} = 'IGNORE';
4552 local $SIG{INT} = 'IGNORE';
4553 local $SIG{QUIT} = 'IGNORE';
4554 local $SIG{TERM} = 'IGNORE';
4555 local $SIG{TSTP} = 'IGNORE';
4556 local $SIG{PIPE} = 'IGNORE';
4558 my $oldAutoCommit = $FS::UID::AutoCommit;
4559 local $FS::UID::AutoCommit = 0;
4562 #while ( $columns = $csv->getline($fh) ) {
4564 while ( defined($line=<$fh>) ) {
4566 $csv->parse($line) or do {
4567 $dbh->rollback if $oldAutoCommit;
4568 return "can't parse: ". $csv->error_input();
4571 my @columns = $csv->fields();
4572 #warn join('-',@columns);
4575 foreach my $field ( @fields ) {
4576 $row{$field} = shift @columns;
4579 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
4580 unless ( $cust_main ) {
4581 $dbh->rollback if $oldAutoCommit;
4582 return "unknown custnum $row{'custnum'}";
4585 if ( $row{'amount'} > 0 ) {
4586 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
4588 $dbh->rollback if $oldAutoCommit;
4592 } elsif ( $row{'amount'} < 0 ) {
4593 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
4596 $dbh->rollback if $oldAutoCommit;
4606 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4608 return "Empty file!" unless $imported;
4620 The delete method should possibly take an FS::cust_main object reference
4621 instead of a scalar customer number.
4623 Bill and collect options should probably be passed as references instead of a
4626 There should probably be a configuration file with a list of allowed credit
4629 No multiple currency support (probably a larger project than just this module).
4631 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
4635 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
4636 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
4637 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.