5 use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf @encrypted_fields
6 $import $skip_fuzzyfiles $ignore_expired_card @paytypes);
7 use vars qw( $realtime_bop_decline_quiet ); #ugh
11 use Scalar::Util qw( blessed );
12 use Time::Local qw(timelocal);
15 use Digest::MD5 qw(md5_base64);
18 use File::Temp qw( tempfile );
19 use String::Approx qw(amatch);
20 use Business::CreditCard 0.28;
22 use FS::UID qw( getotaker dbh driver_name );
23 use FS::Record qw( qsearchs qsearch dbdef );
24 use FS::Misc qw( generate_email send_email generate_ps do_print );
25 use FS::Msgcat qw(gettext);
30 use FS::cust_bill_pkg;
31 use FS::cust_bill_pkg_display;
32 use FS::cust_bill_pkg_tax_location;
34 use FS::cust_pay_pending;
35 use FS::cust_pay_void;
36 use FS::cust_pay_batch;
39 use FS::part_referral;
40 use FS::cust_main_county;
41 use FS::cust_location;
43 use FS::cust_tax_location;
44 use FS::part_pkg_taxrate;
46 use FS::cust_main_invoice;
47 use FS::cust_credit_bill;
48 use FS::cust_bill_pay;
49 use FS::prepay_credit;
53 use FS::part_event_condition;
56 use FS::payment_gateway;
57 use FS::agent_payment_gateway;
59 use FS::payinfo_Mixin;
62 @ISA = qw( FS::payinfo_Mixin FS::Record );
64 @EXPORT_OK = qw( smart_search );
66 $realtime_bop_decline_quiet = 0;
68 # 1 is mostly method/subroutine entry and options
69 # 2 traces progress of some operations
70 # 3 is even more information including possibly sensitive data
72 $me = '[FS::cust_main]';
76 $ignore_expired_card = 0;
78 @encrypted_fields = ('payinfo', 'paycvv');
79 sub nohistory_fields { ('paycvv'); }
81 @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
83 #ask FS::UID to run this stuff for us later
84 #$FS::UID::callback{'FS::cust_main'} = sub {
85 install_callback FS::UID sub {
87 #yes, need it for stuff below (prolly should be cached)
92 my ( $hashref, $cache ) = @_;
93 if ( exists $hashref->{'pkgnum'} ) {
94 #@{ $self->{'_pkgnum'} } = ();
95 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
96 $self->{'_pkgnum'} = $subcache;
97 #push @{ $self->{'_pkgnum'} },
98 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
104 FS::cust_main - Object methods for cust_main records
110 $record = new FS::cust_main \%hash;
111 $record = new FS::cust_main { 'column' => 'value' };
113 $error = $record->insert;
115 $error = $new_record->replace($old_record);
117 $error = $record->delete;
119 $error = $record->check;
121 @cust_pkg = $record->all_pkgs;
123 @cust_pkg = $record->ncancelled_pkgs;
125 @cust_pkg = $record->suspended_pkgs;
127 $error = $record->bill;
128 $error = $record->bill %options;
129 $error = $record->bill 'time' => $time;
131 $error = $record->collect;
132 $error = $record->collect %options;
133 $error = $record->collect 'invoice_time' => $time,
138 An FS::cust_main object represents a customer. FS::cust_main inherits from
139 FS::Record. The following fields are currently supported:
145 Primary key (assigned automatically for new customers)
149 Agent (see L<FS::agent>)
153 Advertising source (see L<FS::part_referral>)
165 Cocial security number (optional)
181 (optional, see L<FS::cust_main_county>)
185 (see L<FS::cust_main_county>)
191 (see L<FS::cust_main_county>)
227 (optional, see L<FS::cust_main_county>)
231 (see L<FS::cust_main_county>)
237 (see L<FS::cust_main_county>)
253 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
257 Payment Information (See L<FS::payinfo_Mixin> for data format)
261 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
265 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
269 Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
273 Start date month (maestro/solo cards only)
277 Start date year (maestro/solo cards only)
281 Issue number (maestro/solo cards only)
285 Name on card or billing name
289 IP address from which payment information was received
293 Tax exempt, empty or `Y'
297 Order taker (assigned automatically, see L<FS::UID>)
303 =item referral_custnum
305 Referring customer number
309 Enable individual CDR spooling, empty or `Y'
313 A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
317 Discourage individual CDR printing, empty or `Y'
327 Creates a new customer. To add the customer to the database, see L<"insert">.
329 Note that this stores the hash reference, not a distinct copy of the hash it
330 points to. You can ask the object for a copy with the I<hash> method.
334 sub table { 'cust_main'; }
336 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
338 Adds this customer to the database. If there is an error, returns the error,
339 otherwise returns false.
341 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
342 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
343 are inserted atomicly, or the transaction is rolled back. Passing an empty
344 hash reference is equivalent to not supplying this parameter. There should be
345 a better explanation of this, but until then, here's an example:
348 tie %hash, 'Tie::RefHash'; #this part is important
350 $cust_pkg => [ $svc_acct ],
353 $cust_main->insert( \%hash );
355 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
356 be set as the invoicing list (see L<"invoicing_list">). Errors return as
357 expected and rollback the entire transaction; it is not necessary to call
358 check_invoicing_list first. The invoicing_list is set after the records in the
359 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
360 invoicing_list destination to the newly-created svc_acct. Here's an example:
362 $cust_main->insert( {}, [ $email, 'POST' ] );
364 Currently available options are: I<depend_jobnum> and I<noexport>.
366 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
367 on the supplied jobnum (they will not run until the specific job completes).
368 This can be used to defer provisioning until some action completes (such
369 as running the customer's credit card successfully).
371 The I<noexport> option is deprecated. If I<noexport> is set true, no
372 provisioning jobs (exports) are scheduled. (You can schedule them later with
373 the B<reexport> method.)
379 my $cust_pkgs = @_ ? shift : {};
380 my $invoicing_list = @_ ? shift : '';
382 warn "$me insert called with options ".
383 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
386 local $SIG{HUP} = 'IGNORE';
387 local $SIG{INT} = 'IGNORE';
388 local $SIG{QUIT} = 'IGNORE';
389 local $SIG{TERM} = 'IGNORE';
390 local $SIG{TSTP} = 'IGNORE';
391 local $SIG{PIPE} = 'IGNORE';
393 my $oldAutoCommit = $FS::UID::AutoCommit;
394 local $FS::UID::AutoCommit = 0;
397 my $prepay_identifier = '';
398 my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = (0, 0, 0, 0, 0);
400 if ( $self->payby eq 'PREPAY' ) {
402 $self->payby('BILL');
403 $prepay_identifier = $self->payinfo;
406 warn " looking up prepaid card $prepay_identifier\n"
409 my $error = $self->get_prepay( $prepay_identifier,
410 'amount_ref' => \$amount,
411 'seconds_ref' => \$seconds,
412 'upbytes_ref' => \$upbytes,
413 'downbytes_ref' => \$downbytes,
414 'totalbytes_ref' => \$totalbytes,
417 $dbh->rollback if $oldAutoCommit;
418 #return "error applying prepaid card (transaction rolled back): $error";
422 $payby = 'PREP' if $amount;
424 } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
427 $self->payby('BILL');
428 $amount = $self->paid;
432 warn " inserting $self\n"
435 $self->signupdate(time) unless $self->signupdate;
437 $self->auto_agent_custid()
438 if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
440 my $error = $self->SUPER::insert;
442 $dbh->rollback if $oldAutoCommit;
443 #return "inserting cust_main record (transaction rolled back): $error";
447 warn " setting invoicing list\n"
450 if ( $invoicing_list ) {
451 $error = $self->check_invoicing_list( $invoicing_list );
453 $dbh->rollback if $oldAutoCommit;
454 #return "checking invoicing_list (transaction rolled back): $error";
457 $self->invoicing_list( $invoicing_list );
460 if ( $conf->config('cust_main-skeleton_tables')
461 && $conf->config('cust_main-skeleton_custnum') ) {
463 warn " inserting skeleton records\n"
466 my $error = $self->start_copy_skel;
468 $dbh->rollback if $oldAutoCommit;
474 warn " ordering packages\n"
477 $error = $self->order_pkgs( $cust_pkgs,
479 'seconds_ref' => \$seconds,
480 'upbytes_ref' => \$upbytes,
481 'downbytes_ref' => \$downbytes,
482 'totalbytes_ref' => \$totalbytes,
485 $dbh->rollback if $oldAutoCommit;
490 $dbh->rollback if $oldAutoCommit;
491 return "No svc_acct record to apply pre-paid time";
493 if ( $upbytes || $downbytes || $totalbytes ) {
494 $dbh->rollback if $oldAutoCommit;
495 return "No svc_acct record to apply pre-paid data";
499 warn " inserting initial $payby payment of $amount\n"
501 $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
503 $dbh->rollback if $oldAutoCommit;
504 return "inserting payment (transaction rolled back): $error";
508 unless ( $import || $skip_fuzzyfiles ) {
509 warn " queueing fuzzyfiles update\n"
511 $error = $self->queue_fuzzyfiles_update;
513 $dbh->rollback if $oldAutoCommit;
514 return "updating fuzzy search cache: $error";
518 warn " insert complete; committing transaction\n"
521 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
526 use File::CounterFile;
527 sub auto_agent_custid {
530 my $format = $conf->config('cust_main-auto_agent_custid');
532 if ( $format eq '1YMMXXXXXXXX' ) {
534 my $counter = new File::CounterFile 'cust_main.agent_custid';
537 my $ym = 100000000000 + time2str('%y%m00000000', time);
538 if ( $ym > $counter->value ) {
539 $counter->{'value'} = $agent_custid = $ym;
540 $counter->{'updated'} = 1;
542 $agent_custid = $counter->inc;
548 die "Unknown cust_main-auto_agent_custid format: $format";
551 $self->agent_custid($agent_custid);
555 sub start_copy_skel {
558 #'mg_user_preference' => {},
559 #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, },
560 #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' },
561 #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' },
562 #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } },
563 my @tables = eval(join('\n',$conf->config('cust_main-skeleton_tables')));
566 _copy_skel( 'cust_main', #tablename
567 $conf->config('cust_main-skeleton_custnum'), #sourceid
568 $self->custnum, #destid
569 @tables, #child tables
573 #recursive subroutine, not a method
575 my( $table, $sourceid, $destid, %child_tables ) = @_;
578 if ( $table =~ /^(\w+)\.(\w+)$/ ) {
579 ( $table, $primary_key ) = ( $1, $2 );
581 my $dbdef_table = dbdef->table($table);
582 $primary_key = $dbdef_table->primary_key
583 or return "$table has no primary key".
584 " (or do you need to run dbdef-create?)";
587 warn " _copy_skel: $table.$primary_key $sourceid to $destid for ".
588 join (', ', keys %child_tables). "\n"
591 foreach my $child_table_def ( keys %child_tables ) {
595 if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) {
596 ( $child_table, $child_pkey ) = ( $1, $2 );
598 $child_table = $child_table_def;
600 $child_pkey = dbdef->table($child_table)->primary_key;
601 # or return "$table has no primary key".
602 # " (or do you need to run dbdef-create?)\n";
606 if ( keys %{ $child_tables{$child_table_def} } ) {
608 return "$child_table has no primary key".
609 " (run dbdef-create or try specifying it?)\n"
612 #false laziness w/Record::insert and only works on Pg
613 #refactor the proper last-inserted-id stuff out of Record::insert if this
614 # ever gets use for anything besides a quick kludge for one customer
615 my $default = dbdef->table($child_table)->column($child_pkey)->default;
616 $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
617 or return "can't parse $child_table.$child_pkey default value ".
618 " for sequence name: $default";
623 my @sel_columns = grep { $_ ne $primary_key }
624 dbdef->table($child_table)->columns;
625 my $sel_columns = join(', ', @sel_columns );
627 my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
628 my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) ';
629 my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
631 my $sel_st = "SELECT $sel_columns FROM $child_table".
632 " WHERE $primary_key = $sourceid";
635 my $sel_sth = dbh->prepare( $sel_st )
636 or return dbh->errstr;
638 $sel_sth->execute or return $sel_sth->errstr;
640 while ( my $row = $sel_sth->fetchrow_hashref ) {
642 warn " selected row: ".
643 join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n"
647 "INSERT INTO $child_table $ins_columns VALUES $placeholders";
648 my $ins_sth =dbh->prepare($statement)
649 or return dbh->errstr;
650 my @param = ( $destid, map $row->{$_}, @ins_columns );
651 warn " $statement: [ ". join(', ', @param). " ]\n"
653 $ins_sth->execute( @param )
654 or return $ins_sth->errstr;
656 #next unless keys %{ $child_tables{$child_table} };
657 next unless $sequence;
659 #another section of that laziness
660 my $seq_sql = "SELECT currval('$sequence')";
661 my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
662 $seq_sth->execute or return $seq_sth->errstr;
663 my $insertid = $seq_sth->fetchrow_arrayref->[0];
665 # don't drink soap! recurse! recurse! okay!
667 _copy_skel( $child_table_def,
668 $row->{$child_pkey}, #sourceid
670 %{ $child_tables{$child_table_def} },
672 return $error if $error;
682 =item order_pkg HASHREF | OPTION => VALUE ...
684 Orders a single package.
686 Options may be passed as a list of key/value pairs or as a hash reference.
697 Optional FS::cust_location object
701 Optional arryaref of FS::svc_* service objects.
705 If this option is set to a job queue jobnum (see L<FS::queue>), all provisioning
706 jobs will have a dependancy on the supplied job (they will not run until the
707 specific job completes). This can be used to defer provisioning until some
708 action completes (such as running the customer's credit card successfully).
716 my $opt = ref($_[0]) ? shift : { @_ };
718 warn "$me order_pkg called with options ".
719 join(', ', map { "$_: $opt->{$_}" } keys %$opt ). "\n"
722 my $cust_pkg = $opt->{'cust_pkg'};
723 my $svcs = $opt->{'svcs'} || [];
725 my %svc_options = ();
726 $svc_options{'depend_jobnum'} = $opt->{'depend_jobnum'}
727 if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
729 local $SIG{HUP} = 'IGNORE';
730 local $SIG{INT} = 'IGNORE';
731 local $SIG{QUIT} = 'IGNORE';
732 local $SIG{TERM} = 'IGNORE';
733 local $SIG{TSTP} = 'IGNORE';
734 local $SIG{PIPE} = 'IGNORE';
736 my $oldAutoCommit = $FS::UID::AutoCommit;
737 local $FS::UID::AutoCommit = 0;
740 if ( $opt->{'cust_location'} &&
741 ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
742 my $error = $opt->{'cust_location'}->insert;
744 $dbh->rollback if $oldAutoCommit;
745 return "inserting cust_location (transaction rolled back): $error";
747 $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
750 $cust_pkg->custnum( $self->custnum );
752 my $error = $cust_pkg->insert;
754 $dbh->rollback if $oldAutoCommit;
755 return "inserting cust_pkg (transaction rolled back): $error";
758 foreach my $svc_something ( @{ $opt->{'svcs'} } ) {
759 if ( $svc_something->svcnum ) {
760 my $old_cust_svc = $svc_something->cust_svc;
761 my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
762 $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
763 $error = $new_cust_svc->replace($old_cust_svc);
765 $svc_something->pkgnum( $cust_pkg->pkgnum );
766 if ( $svc_something->isa('FS::svc_acct') ) {
767 foreach ( grep { $opt->{$_.'_ref'} && ${ $opt->{$_.'_ref'} } }
768 qw( seconds upbytes downbytes totalbytes ) ) {
769 $svc_something->$_( $svc_something->$_() + ${ $opt->{$_.'_ref'} } );
770 ${ $opt->{$_.'_ref'} } = 0;
773 $error = $svc_something->insert(%svc_options);
776 $dbh->rollback if $oldAutoCommit;
777 return "inserting svc_ (transaction rolled back): $error";
781 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
786 #deprecated #=item order_pkgs HASHREF [ , SECONDSREF ] [ , OPTION => VALUE ... ]
787 =item order_pkgs HASHREF [ , OPTION => VALUE ... ]
789 Like the insert method on an existing record, this method orders multiple
790 packages and included services atomicaly. Pass a Tie::RefHash data structure
791 to this method containing FS::cust_pkg and FS::svc_I<tablename> objects.
792 There should be a better explanation of this, but until then, here's an
796 tie %hash, 'Tie::RefHash'; #this part is important
798 $cust_pkg => [ $svc_acct ],
801 $cust_main->order_pkgs( \%hash, 'noexport'=>1 );
803 Services can be new, in which case they are inserted, or existing unaudited
804 services, in which case they are linked to the newly-created package.
806 Currently available options are: I<depend_jobnum>, I<noexport>, I<seconds_ref>,
807 I<upbytes_ref>, I<downbytes_ref>, and I<totalbytes_ref>.
809 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
810 on the supplied jobnum (they will not run until the specific job completes).
811 This can be used to defer provisioning until some action completes (such
812 as running the customer's credit card successfully).
814 The I<noexport> option is deprecated. If I<noexport> is set true, no
815 provisioning jobs (exports) are scheduled. (You can schedule them later with
816 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
817 on the cust_main object is not recommended, as existing services will also be
820 If I<seconds_ref>, I<upbytes_ref>, I<downbytes_ref>, or I<totalbytes_ref> is
821 provided, the scalars (provided by references) will be incremented by the
822 values of the prepaid card.`
828 my $cust_pkgs = shift;
829 my $seconds_ref = ref($_[0]) ? shift : ''; #deprecated
831 $seconds_ref ||= $options{'seconds_ref'};
833 warn "$me order_pkgs called with options ".
834 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
837 local $SIG{HUP} = 'IGNORE';
838 local $SIG{INT} = 'IGNORE';
839 local $SIG{QUIT} = 'IGNORE';
840 local $SIG{TERM} = 'IGNORE';
841 local $SIG{TSTP} = 'IGNORE';
842 local $SIG{PIPE} = 'IGNORE';
844 my $oldAutoCommit = $FS::UID::AutoCommit;
845 local $FS::UID::AutoCommit = 0;
848 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
850 foreach my $cust_pkg ( keys %$cust_pkgs ) {
852 my $error = $self->order_pkg(
853 'cust_pkg' => $cust_pkg,
854 'svcs' => $cust_pkgs->{$cust_pkg},
855 'seconds_ref' => $seconds_ref,
856 map { $_ => $options{$_} } qw( upbytes_ref downbytes_ref totalbytes_ref
861 $dbh->rollback if $oldAutoCommit;
867 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
871 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
873 Recharges this (existing) customer with the specified prepaid card (see
874 L<FS::prepay_credit>), specified either by I<identifier> or as an
875 FS::prepay_credit object. If there is an error, returns the error, otherwise
878 Optionally, five scalar references can be passed as well. They will have their
879 values filled in with the amount, number of seconds, and number of upload,
880 download, and total bytes applied by this prepaid card.
884 #the ref bullshit here should be refactored like get_prepay. MyAccount.pm is
885 #the only place that uses these args
886 sub recharge_prepay {
887 my( $self, $prepay_credit, $amountref, $secondsref,
888 $upbytesref, $downbytesref, $totalbytesref ) = @_;
890 local $SIG{HUP} = 'IGNORE';
891 local $SIG{INT} = 'IGNORE';
892 local $SIG{QUIT} = 'IGNORE';
893 local $SIG{TERM} = 'IGNORE';
894 local $SIG{TSTP} = 'IGNORE';
895 local $SIG{PIPE} = 'IGNORE';
897 my $oldAutoCommit = $FS::UID::AutoCommit;
898 local $FS::UID::AutoCommit = 0;
901 my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
903 my $error = $self->get_prepay( $prepay_credit,
904 'amount_ref' => \$amount,
905 'seconds_ref' => \$seconds,
906 'upbytes_ref' => \$upbytes,
907 'downbytes_ref' => \$downbytes,
908 'totalbytes_ref' => \$totalbytes,
910 || $self->increment_seconds($seconds)
911 || $self->increment_upbytes($upbytes)
912 || $self->increment_downbytes($downbytes)
913 || $self->increment_totalbytes($totalbytes)
914 || $self->insert_cust_pay_prepay( $amount,
916 ? $prepay_credit->identifier
921 $dbh->rollback if $oldAutoCommit;
925 if ( defined($amountref) ) { $$amountref = $amount; }
926 if ( defined($secondsref) ) { $$secondsref = $seconds; }
927 if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
928 if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
929 if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
931 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
936 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , OPTION => VALUE ... ]
938 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
939 specified either by I<identifier> or as an FS::prepay_credit object.
941 Available options are: I<amount_ref>, I<seconds_ref>, I<upbytes_ref>, I<downbytes_ref>, and I<totalbytes_ref>. The scalars (provided by references) will be
942 incremented by the values of the prepaid card.
944 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
945 check or set this customer's I<agentnum>.
947 If there is an error, returns the error, otherwise returns false.
953 my( $self, $prepay_credit, %opt ) = @_;
955 local $SIG{HUP} = 'IGNORE';
956 local $SIG{INT} = 'IGNORE';
957 local $SIG{QUIT} = 'IGNORE';
958 local $SIG{TERM} = 'IGNORE';
959 local $SIG{TSTP} = 'IGNORE';
960 local $SIG{PIPE} = 'IGNORE';
962 my $oldAutoCommit = $FS::UID::AutoCommit;
963 local $FS::UID::AutoCommit = 0;
966 unless ( ref($prepay_credit) ) {
968 my $identifier = $prepay_credit;
970 $prepay_credit = qsearchs(
972 { 'identifier' => $prepay_credit },
977 unless ( $prepay_credit ) {
978 $dbh->rollback if $oldAutoCommit;
979 return "Invalid prepaid card: ". $identifier;
984 if ( $prepay_credit->agentnum ) {
985 if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
986 $dbh->rollback if $oldAutoCommit;
987 return "prepaid card not valid for agent ". $self->agentnum;
989 $self->agentnum($prepay_credit->agentnum);
992 my $error = $prepay_credit->delete;
994 $dbh->rollback if $oldAutoCommit;
995 return "removing prepay_credit (transaction rolled back): $error";
998 ${ $opt{$_.'_ref'} } += $prepay_credit->$_()
999 for grep $opt{$_.'_ref'}, qw( amount seconds upbytes downbytes totalbytes );
1001 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1006 =item increment_upbytes SECONDS
1008 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1009 the specified number of upbytes. If there is an error, returns the error,
1010 otherwise returns false.
1014 sub increment_upbytes {
1015 _increment_column( shift, 'upbytes', @_);
1018 =item increment_downbytes SECONDS
1020 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1021 the specified number of downbytes. If there is an error, returns the error,
1022 otherwise returns false.
1026 sub increment_downbytes {
1027 _increment_column( shift, 'downbytes', @_);
1030 =item increment_totalbytes SECONDS
1032 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1033 the specified number of totalbytes. If there is an error, returns the error,
1034 otherwise returns false.
1038 sub increment_totalbytes {
1039 _increment_column( shift, 'totalbytes', @_);
1042 =item increment_seconds SECONDS
1044 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1045 the specified number of seconds. If there is an error, returns the error,
1046 otherwise returns false.
1050 sub increment_seconds {
1051 _increment_column( shift, 'seconds', @_);
1054 =item _increment_column AMOUNT
1056 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1057 the specified number of seconds or bytes. If there is an error, returns
1058 the error, otherwise returns false.
1062 sub _increment_column {
1063 my( $self, $column, $amount ) = @_;
1064 warn "$me increment_column called: $column, $amount\n"
1067 return '' unless $amount;
1069 my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
1070 $self->ncancelled_pkgs;
1072 if ( ! @cust_pkg ) {
1073 return 'No packages with primary or single services found'.
1074 ' to apply pre-paid time';
1075 } elsif ( scalar(@cust_pkg) > 1 ) {
1076 #maybe have a way to specify the package/account?
1077 return 'Multiple packages found to apply pre-paid time';
1080 my $cust_pkg = $cust_pkg[0];
1081 warn " found package pkgnum ". $cust_pkg->pkgnum. "\n"
1085 $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
1087 if ( ! @cust_svc ) {
1088 return 'No account found to apply pre-paid time';
1089 } elsif ( scalar(@cust_svc) > 1 ) {
1090 return 'Multiple accounts found to apply pre-paid time';
1093 my $svc_acct = $cust_svc[0]->svc_x;
1094 warn " found service svcnum ". $svc_acct->pkgnum.
1095 ' ('. $svc_acct->email. ")\n"
1098 $column = "increment_$column";
1099 $svc_acct->$column($amount);
1103 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
1105 Inserts a prepayment in the specified amount for this customer. An optional
1106 second argument can specify the prepayment identifier for tracking purposes.
1107 If there is an error, returns the error, otherwise returns false.
1111 sub insert_cust_pay_prepay {
1112 shift->insert_cust_pay('PREP', @_);
1115 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
1117 Inserts a cash payment in the specified amount for this customer. An optional
1118 second argument can specify the payment identifier for tracking purposes.
1119 If there is an error, returns the error, otherwise returns false.
1123 sub insert_cust_pay_cash {
1124 shift->insert_cust_pay('CASH', @_);
1127 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
1129 Inserts a Western Union payment in the specified amount for this customer. An
1130 optional second argument can specify the prepayment identifier for tracking
1131 purposes. If there is an error, returns the error, otherwise returns false.
1135 sub insert_cust_pay_west {
1136 shift->insert_cust_pay('WEST', @_);
1139 sub insert_cust_pay {
1140 my( $self, $payby, $amount ) = splice(@_, 0, 3);
1141 my $payinfo = scalar(@_) ? shift : '';
1143 my $cust_pay = new FS::cust_pay {
1144 'custnum' => $self->custnum,
1145 'paid' => sprintf('%.2f', $amount),
1146 #'_date' => #date the prepaid card was purchased???
1148 'payinfo' => $payinfo,
1156 This method is deprecated. See the I<depend_jobnum> option to the insert and
1157 order_pkgs methods for a better way to defer provisioning.
1159 Re-schedules all exports by calling the B<reexport> method of all associated
1160 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
1161 otherwise returns false.
1168 carp "WARNING: FS::cust_main::reexport is deprectated; ".
1169 "use the depend_jobnum option to insert or order_pkgs to delay export";
1171 local $SIG{HUP} = 'IGNORE';
1172 local $SIG{INT} = 'IGNORE';
1173 local $SIG{QUIT} = 'IGNORE';
1174 local $SIG{TERM} = 'IGNORE';
1175 local $SIG{TSTP} = 'IGNORE';
1176 local $SIG{PIPE} = 'IGNORE';
1178 my $oldAutoCommit = $FS::UID::AutoCommit;
1179 local $FS::UID::AutoCommit = 0;
1182 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
1183 my $error = $cust_pkg->reexport;
1185 $dbh->rollback if $oldAutoCommit;
1190 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1195 =item delete NEW_CUSTNUM
1197 This deletes the customer. If there is an error, returns the error, otherwise
1200 This will completely remove all traces of the customer record. This is not
1201 what you want when a customer cancels service; for that, cancel all of the
1202 customer's packages (see L</cancel>).
1204 If the customer has any uncancelled packages, you need to pass a new (valid)
1205 customer number for those packages to be transferred to. Cancelled packages
1206 will be deleted. Did I mention that this is NOT what you want when a customer
1207 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
1209 You can't delete a customer with invoices (see L<FS::cust_bill>),
1210 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
1211 refunds (see L<FS::cust_refund>).
1218 local $SIG{HUP} = 'IGNORE';
1219 local $SIG{INT} = 'IGNORE';
1220 local $SIG{QUIT} = 'IGNORE';
1221 local $SIG{TERM} = 'IGNORE';
1222 local $SIG{TSTP} = 'IGNORE';
1223 local $SIG{PIPE} = 'IGNORE';
1225 my $oldAutoCommit = $FS::UID::AutoCommit;
1226 local $FS::UID::AutoCommit = 0;
1229 if ( $self->cust_bill ) {
1230 $dbh->rollback if $oldAutoCommit;
1231 return "Can't delete a customer with invoices";
1233 if ( $self->cust_credit ) {
1234 $dbh->rollback if $oldAutoCommit;
1235 return "Can't delete a customer with credits";
1237 if ( $self->cust_pay ) {
1238 $dbh->rollback if $oldAutoCommit;
1239 return "Can't delete a customer with payments";
1241 if ( $self->cust_refund ) {
1242 $dbh->rollback if $oldAutoCommit;
1243 return "Can't delete a customer with refunds";
1246 my @cust_pkg = $self->ncancelled_pkgs;
1248 my $new_custnum = shift;
1249 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1250 $dbh->rollback if $oldAutoCommit;
1251 return "Invalid new customer number: $new_custnum";
1253 foreach my $cust_pkg ( @cust_pkg ) {
1254 my %hash = $cust_pkg->hash;
1255 $hash{'custnum'} = $new_custnum;
1256 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1257 my $error = $new_cust_pkg->replace($cust_pkg,
1258 options => { $cust_pkg->options },
1261 $dbh->rollback if $oldAutoCommit;
1266 my @cancelled_cust_pkg = $self->all_pkgs;
1267 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1268 my $error = $cust_pkg->delete;
1270 $dbh->rollback if $oldAutoCommit;
1275 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
1276 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
1278 my $error = $cust_main_invoice->delete;
1280 $dbh->rollback if $oldAutoCommit;
1285 my $error = $self->SUPER::delete;
1287 $dbh->rollback if $oldAutoCommit;
1291 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1296 =item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ]
1298 Replaces the OLD_RECORD with this one in the database. If there is an error,
1299 returns the error, otherwise returns false.
1301 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1302 be set as the invoicing list (see L<"invoicing_list">). Errors return as
1303 expected and rollback the entire transaction; it is not necessary to call
1304 check_invoicing_list first. Here's an example:
1306 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1313 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
1315 : $self->replace_old;
1319 warn "$me replace called\n"
1322 my $curuser = $FS::CurrentUser::CurrentUser;
1323 if ( $self->payby eq 'COMP'
1324 && $self->payby ne $old->payby
1325 && ! $curuser->access_right('Complimentary customer')
1328 return "You are not permitted to create complimentary accounts.";
1331 local($ignore_expired_card) = 1
1332 if $old->payby =~ /^(CARD|DCRD)$/
1333 && $self->payby =~ /^(CARD|DCRD)$/
1334 && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1336 local $SIG{HUP} = 'IGNORE';
1337 local $SIG{INT} = 'IGNORE';
1338 local $SIG{QUIT} = 'IGNORE';
1339 local $SIG{TERM} = 'IGNORE';
1340 local $SIG{TSTP} = 'IGNORE';
1341 local $SIG{PIPE} = 'IGNORE';
1343 my $oldAutoCommit = $FS::UID::AutoCommit;
1344 local $FS::UID::AutoCommit = 0;
1347 my $error = $self->SUPER::replace($old);
1350 $dbh->rollback if $oldAutoCommit;
1354 if ( @param ) { # INVOICING_LIST_ARYREF
1355 my $invoicing_list = shift @param;
1356 $error = $self->check_invoicing_list( $invoicing_list );
1358 $dbh->rollback if $oldAutoCommit;
1361 $self->invoicing_list( $invoicing_list );
1364 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
1365 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
1366 # card/check/lec info has changed, want to retry realtime_ invoice events
1367 my $error = $self->retry_realtime;
1369 $dbh->rollback if $oldAutoCommit;
1374 unless ( $import || $skip_fuzzyfiles ) {
1375 $error = $self->queue_fuzzyfiles_update;
1377 $dbh->rollback if $oldAutoCommit;
1378 return "updating fuzzy search cache: $error";
1382 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1387 =item queue_fuzzyfiles_update
1389 Used by insert & replace to update the fuzzy search cache
1393 sub queue_fuzzyfiles_update {
1396 local $SIG{HUP} = 'IGNORE';
1397 local $SIG{INT} = 'IGNORE';
1398 local $SIG{QUIT} = 'IGNORE';
1399 local $SIG{TERM} = 'IGNORE';
1400 local $SIG{TSTP} = 'IGNORE';
1401 local $SIG{PIPE} = 'IGNORE';
1403 my $oldAutoCommit = $FS::UID::AutoCommit;
1404 local $FS::UID::AutoCommit = 0;
1407 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1408 my $error = $queue->insert( map $self->getfield($_),
1409 qw(first last company)
1412 $dbh->rollback if $oldAutoCommit;
1413 return "queueing job (transaction rolled back): $error";
1416 if ( $self->ship_last ) {
1417 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1418 $error = $queue->insert( map $self->getfield("ship_$_"),
1419 qw(first last company)
1422 $dbh->rollback if $oldAutoCommit;
1423 return "queueing job (transaction rolled back): $error";
1427 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1434 Checks all fields to make sure this is a valid customer record. If there is
1435 an error, returns the error, otherwise returns false. Called by the insert
1436 and replace methods.
1443 warn "$me check BEFORE: \n". $self->_dump
1447 $self->ut_numbern('custnum')
1448 || $self->ut_number('agentnum')
1449 || $self->ut_textn('agent_custid')
1450 || $self->ut_number('refnum')
1451 || $self->ut_textn('custbatch')
1452 || $self->ut_name('last')
1453 || $self->ut_name('first')
1454 || $self->ut_snumbern('birthdate')
1455 || $self->ut_snumbern('signupdate')
1456 || $self->ut_textn('company')
1457 || $self->ut_text('address1')
1458 || $self->ut_textn('address2')
1459 || $self->ut_text('city')
1460 || $self->ut_textn('county')
1461 || $self->ut_textn('state')
1462 || $self->ut_country('country')
1463 || $self->ut_anything('comments')
1464 || $self->ut_numbern('referral_custnum')
1465 || $self->ut_textn('stateid')
1466 || $self->ut_textn('stateid_state')
1467 || $self->ut_textn('invoice_terms')
1468 || $self->ut_alphan('geocode')
1471 #barf. need message catalogs. i18n. etc.
1472 $error .= "Please select an advertising source."
1473 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1474 return $error if $error;
1476 return "Unknown agent"
1477 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1479 return "Unknown refnum"
1480 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1482 return "Unknown referring custnum: ". $self->referral_custnum
1483 unless ! $self->referral_custnum
1484 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1486 if ( $self->ss eq '' ) {
1491 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1492 or return "Illegal social security number: ". $self->ss;
1493 $self->ss("$1-$2-$3");
1497 # bad idea to disable, causes billing to fail because of no tax rates later
1498 # unless ( $import ) {
1499 unless ( qsearch('cust_main_county', {
1500 'country' => $self->country,
1503 return "Unknown state/county/country: ".
1504 $self->state. "/". $self->county. "/". $self->country
1505 unless qsearch('cust_main_county',{
1506 'state' => $self->state,
1507 'county' => $self->county,
1508 'country' => $self->country,
1514 $self->ut_phonen('daytime', $self->country)
1515 || $self->ut_phonen('night', $self->country)
1516 || $self->ut_phonen('fax', $self->country)
1517 || $self->ut_zip('zip', $self->country)
1519 return $error if $error;
1521 if ( $conf->exists('cust_main-require_phone')
1522 && ! length($self->daytime) && ! length($self->night)
1525 my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1527 : FS::Msgcat::_gettext('daytime');
1528 my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1530 : FS::Msgcat::_gettext('night');
1532 return "$daytime_label or $night_label is required"
1536 if ( $self->has_ship_address
1537 && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1538 $self->addr_fields )
1542 $self->ut_name('ship_last')
1543 || $self->ut_name('ship_first')
1544 || $self->ut_textn('ship_company')
1545 || $self->ut_text('ship_address1')
1546 || $self->ut_textn('ship_address2')
1547 || $self->ut_text('ship_city')
1548 || $self->ut_textn('ship_county')
1549 || $self->ut_textn('ship_state')
1550 || $self->ut_country('ship_country')
1552 return $error if $error;
1554 #false laziness with above
1555 unless ( qsearchs('cust_main_county', {
1556 'country' => $self->ship_country,
1559 return "Unknown ship_state/ship_county/ship_country: ".
1560 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1561 unless qsearch('cust_main_county',{
1562 'state' => $self->ship_state,
1563 'county' => $self->ship_county,
1564 'country' => $self->ship_country,
1570 $self->ut_phonen('ship_daytime', $self->ship_country)
1571 || $self->ut_phonen('ship_night', $self->ship_country)
1572 || $self->ut_phonen('ship_fax', $self->ship_country)
1573 || $self->ut_zip('ship_zip', $self->ship_country)
1575 return $error if $error;
1577 return "Unit # is required."
1578 if $self->ship_address2 =~ /^\s*$/
1579 && $conf->exists('cust_main-require_address2');
1581 } else { # ship_ info eq billing info, so don't store dup info in database
1583 $self->setfield("ship_$_", '')
1584 foreach $self->addr_fields;
1586 return "Unit # is required."
1587 if $self->address2 =~ /^\s*$/
1588 && $conf->exists('cust_main-require_address2');
1592 #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1593 # or return "Illegal payby: ". $self->payby;
1595 FS::payby->can_payby($self->table, $self->payby)
1596 or return "Illegal payby: ". $self->payby;
1598 $error = $self->ut_numbern('paystart_month')
1599 || $self->ut_numbern('paystart_year')
1600 || $self->ut_numbern('payissue')
1601 || $self->ut_textn('paytype')
1603 return $error if $error;
1605 if ( $self->payip eq '' ) {
1608 $error = $self->ut_ip('payip');
1609 return $error if $error;
1612 # If it is encrypted and the private key is not availaible then we can't
1613 # check the credit card.
1615 my $check_payinfo = 1;
1617 if ($self->is_encrypted($self->payinfo)) {
1621 if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1623 my $payinfo = $self->payinfo;
1624 $payinfo =~ s/\D//g;
1625 $payinfo =~ /^(\d{13,16})$/
1626 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1628 $self->payinfo($payinfo);
1630 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1632 return gettext('unknown_card_type')
1633 if cardtype($self->payinfo) eq "Unknown";
1635 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1637 return 'Banned credit card: banned on '.
1638 time2str('%a %h %o at %r', $ban->_date).
1639 ' by '. $ban->otaker.
1640 ' (ban# '. $ban->bannum. ')';
1643 if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1644 if ( cardtype($self->payinfo) eq 'American Express card' ) {
1645 $self->paycvv =~ /^(\d{4})$/
1646 or return "CVV2 (CID) for American Express cards is four digits.";
1649 $self->paycvv =~ /^(\d{3})$/
1650 or return "CVV2 (CVC2/CID) is three digits.";
1657 my $cardtype = cardtype($payinfo);
1658 if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1660 return "Start date or issue number is required for $cardtype cards"
1661 unless $self->paystart_month && $self->paystart_year or $self->payissue;
1663 return "Start month must be between 1 and 12"
1664 if $self->paystart_month
1665 and $self->paystart_month < 1 || $self->paystart_month > 12;
1667 return "Start year must be 1990 or later"
1668 if $self->paystart_year
1669 and $self->paystart_year < 1990;
1671 return "Issue number must be beween 1 and 99"
1673 and $self->payissue < 1 || $self->payissue > 99;
1676 $self->paystart_month('');
1677 $self->paystart_year('');
1678 $self->payissue('');
1681 } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1683 my $payinfo = $self->payinfo;
1684 $payinfo =~ s/[^\d\@]//g;
1685 if ( $conf->exists('echeck-nonus') ) {
1686 $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1687 $payinfo = "$1\@$2";
1689 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1690 $payinfo = "$1\@$2";
1692 $self->payinfo($payinfo);
1695 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1697 return 'Banned ACH account: banned on '.
1698 time2str('%a %h %o at %r', $ban->_date).
1699 ' by '. $ban->otaker.
1700 ' (ban# '. $ban->bannum. ')';
1703 } elsif ( $self->payby eq 'LECB' ) {
1705 my $payinfo = $self->payinfo;
1706 $payinfo =~ s/\D//g;
1707 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1709 $self->payinfo($payinfo);
1712 } elsif ( $self->payby eq 'BILL' ) {
1714 $error = $self->ut_textn('payinfo');
1715 return "Illegal P.O. number: ". $self->payinfo if $error;
1718 } elsif ( $self->payby eq 'COMP' ) {
1720 my $curuser = $FS::CurrentUser::CurrentUser;
1721 if ( ! $self->custnum
1722 && ! $curuser->access_right('Complimentary customer')
1725 return "You are not permitted to create complimentary accounts."
1728 $error = $self->ut_textn('payinfo');
1729 return "Illegal comp account issuer: ". $self->payinfo if $error;
1732 } elsif ( $self->payby eq 'PREPAY' ) {
1734 my $payinfo = $self->payinfo;
1735 $payinfo =~ s/\W//g; #anything else would just confuse things
1736 $self->payinfo($payinfo);
1737 $error = $self->ut_alpha('payinfo');
1738 return "Illegal prepayment identifier: ". $self->payinfo if $error;
1739 return "Unknown prepayment identifier"
1740 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1745 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1746 return "Expiration date required"
1747 unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1751 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1752 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1753 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1754 ( $m, $y ) = ( $3, "20$2" );
1756 return "Illegal expiration date: ". $self->paydate;
1758 $self->paydate("$y-$m-01");
1759 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1760 return gettext('expired_card')
1762 && !$ignore_expired_card
1763 && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1766 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1767 ( ! $conf->exists('require_cardname')
1768 || $self->payby !~ /^(CARD|DCRD)$/ )
1770 $self->payname( $self->first. " ". $self->getfield('last') );
1772 $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1773 or return gettext('illegal_name'). " payname: ". $self->payname;
1777 foreach my $flag (qw( tax spool_cdr squelch_cdr archived )) {
1778 $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1782 $self->otaker(getotaker) unless $self->otaker;
1784 warn "$me check AFTER: \n". $self->_dump
1787 $self->SUPER::check;
1792 Returns a list of fields which have ship_ duplicates.
1797 qw( last first company
1798 address1 address2 city county state zip country
1803 =item has_ship_address
1805 Returns true if this customer record has a separate shipping address.
1809 sub has_ship_address {
1811 scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
1814 =item all_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
1816 Returns all packages (see L<FS::cust_pkg>) for this customer.
1822 my $extra_qsearch = ref($_[0]) ? shift : {};
1824 return $self->num_pkgs unless wantarray || keys(%$extra_qsearch);
1827 if ( $self->{'_pkgnum'} ) {
1828 @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
1830 @cust_pkg = $self->_cust_pkg($extra_qsearch);
1833 sort sort_packages @cust_pkg;
1838 Synonym for B<all_pkgs>.
1843 shift->all_pkgs(@_);
1848 Returns all locations (see L<FS::cust_location>) for this customer.
1854 qsearch('cust_location', { 'custnum' => $self->custnum } );
1857 =item ncancelled_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
1859 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
1863 sub ncancelled_pkgs {
1865 my $extra_qsearch = ref($_[0]) ? shift : {};
1867 return $self->num_ncancelled_pkgs unless wantarray;
1870 if ( $self->{'_pkgnum'} ) {
1872 warn "$me ncancelled_pkgs: returning cached objects"
1875 @cust_pkg = grep { ! $_->getfield('cancel') }
1876 values %{ $self->{'_pkgnum'}->cache };
1880 warn "$me ncancelled_pkgs: searching for packages with custnum ".
1881 $self->custnum. "\n"
1884 $extra_qsearch->{'extra_sql'} .= ' AND ( cancel IS NULL OR cancel = 0 ) ';
1886 @cust_pkg = $self->_cust_pkg($extra_qsearch);
1890 sort sort_packages @cust_pkg;
1896 my $extra_qsearch = ref($_[0]) ? shift : {};
1898 $extra_qsearch->{'select'} ||= '*';
1899 $extra_qsearch->{'select'} .=
1900 ',( SELECT COUNT(*) FROM cust_svc WHERE cust_pkg.pkgnum = cust_svc.pkgnum )
1904 $_->{'_num_cust_svc'} = $_->get('_num_cust_svc');
1909 'table' => 'cust_pkg',
1910 'hashref' => { 'custnum' => $self->custnum },
1915 # This should be generalized to use config options to determine order.
1918 if ( $a->get('cancel') xor $b->get('cancel') ) {
1919 return -1 if $b->get('cancel');
1920 return 1 if $a->get('cancel');
1921 #shouldn't get here...
1924 my $a_num_cust_svc = $a->num_cust_svc;
1925 my $b_num_cust_svc = $b->num_cust_svc;
1926 return 0 if !$a_num_cust_svc && !$b_num_cust_svc;
1927 return -1 if $a_num_cust_svc && !$b_num_cust_svc;
1928 return 1 if !$a_num_cust_svc && $b_num_cust_svc;
1929 my @a_cust_svc = $a->cust_svc;
1930 my @b_cust_svc = $b->cust_svc;
1931 $a_cust_svc[0]->svc_x->label cmp $b_cust_svc[0]->svc_x->label;
1936 =item suspended_pkgs
1938 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
1942 sub suspended_pkgs {
1944 grep { $_->susp } $self->ncancelled_pkgs;
1947 =item unflagged_suspended_pkgs
1949 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
1950 customer (thouse packages without the `manual_flag' set).
1954 sub unflagged_suspended_pkgs {
1956 return $self->suspended_pkgs
1957 unless dbdef->table('cust_pkg')->column('manual_flag');
1958 grep { ! $_->manual_flag } $self->suspended_pkgs;
1961 =item unsuspended_pkgs
1963 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1968 sub unsuspended_pkgs {
1970 grep { ! $_->susp } $self->ncancelled_pkgs;
1973 =item num_cancelled_pkgs
1975 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
1980 sub num_cancelled_pkgs {
1981 shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
1984 sub num_ncancelled_pkgs {
1985 shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
1989 my( $self ) = shift;
1990 my $sql = scalar(@_) ? shift : '';
1991 $sql = "AND $sql" if $sql && $sql !~ /^\s*$/ && $sql !~ /^\s*AND/i;
1992 my $sth = dbh->prepare(
1993 "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? $sql"
1994 ) or die dbh->errstr;
1995 $sth->execute($self->custnum) or die $sth->errstr;
1996 $sth->fetchrow_arrayref->[0];
2001 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
2002 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
2003 on success or a list of errors.
2009 grep { $_->unsuspend } $self->suspended_pkgs;
2014 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
2016 Returns a list: an empty list on success or a list of errors.
2022 grep { $_->suspend(@_) } $self->unsuspended_pkgs;
2025 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2027 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
2028 PKGPARTs (see L<FS::part_pkg>). Preferred usage is to pass a hashref instead
2029 of a list of pkgparts; the hashref has the following keys:
2033 =item pkgparts - listref of pkgparts
2035 =item (other options are passed to the suspend method)
2040 Returns a list: an empty list on success or a list of errors.
2044 sub suspend_if_pkgpart {
2046 my (@pkgparts, %opt);
2047 if (ref($_[0]) eq 'HASH'){
2048 @pkgparts = @{$_[0]{pkgparts}};
2053 grep { $_->suspend(%opt) }
2054 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
2055 $self->unsuspended_pkgs;
2058 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2060 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
2061 given PKGPARTs (see L<FS::part_pkg>). Preferred usage is to pass a hashref
2062 instead of a list of pkgparts; the hashref has the following keys:
2066 =item pkgparts - listref of pkgparts
2068 =item (other options are passed to the suspend method)
2072 Returns a list: an empty list on success or a list of errors.
2076 sub suspend_unless_pkgpart {
2078 my (@pkgparts, %opt);
2079 if (ref($_[0]) eq 'HASH'){
2080 @pkgparts = @{$_[0]{pkgparts}};
2085 grep { $_->suspend(%opt) }
2086 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
2087 $self->unsuspended_pkgs;
2090 =item cancel [ OPTION => VALUE ... ]
2092 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
2094 Available options are:
2098 =item quiet - can be set true to supress email cancellation notices.
2100 =item reason - can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason. The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
2102 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
2106 Always returns a list: an empty list on success or a list of errors.
2111 my( $self, %opt ) = @_;
2113 warn "$me cancel called on customer ". $self->custnum. " with options ".
2114 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
2117 return ( 'access denied' )
2118 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
2120 if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
2122 #should try decryption (we might have the private key)
2123 # and if not maybe queue a job for the server that does?
2124 return ( "Can't (yet) ban encrypted credit cards" )
2125 if $self->is_encrypted($self->payinfo);
2127 my $ban = new FS::banned_pay $self->_banned_pay_hashref;
2128 my $error = $ban->insert;
2129 return ( $error ) if $error;
2133 my @pkgs = $self->ncancelled_pkgs;
2135 warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
2136 scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
2139 grep { $_ } map { $_->cancel(%opt) } $self->ncancelled_pkgs;
2142 sub _banned_pay_hashref {
2153 'payby' => $payby2ban{$self->payby},
2154 'payinfo' => md5_base64($self->payinfo),
2155 #don't ever *search* on reason! #'reason' =>
2161 Returns all notes (see L<FS::cust_main_note>) for this customer.
2168 qsearch( 'cust_main_note',
2169 { 'custnum' => $self->custnum },
2171 'ORDER BY _DATE DESC'
2177 Returns the agent (see L<FS::agent>) for this customer.
2183 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
2186 =item bill_and_collect
2188 Cancels and suspends any packages due, generates bills, applies payments and
2191 Warns on errors (Does not currently: If there is an error, returns the error, otherwise returns false.)
2193 Options are passed as name-value pairs. Currently available options are:
2199 Bills the customer as if it were that time. Specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion functions. For example:
2203 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
2207 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices. Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
2211 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
2215 If set true, re-charges setup fees.
2219 Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
2225 sub bill_and_collect {
2226 my( $self, %options ) = @_;
2228 #$options{actual_time} not $options{time} because freeside-daily -d is for
2229 #pre-printing invoices
2230 $self->cancel_expired_pkgs( $options{actual_time} );
2231 $self->suspend_adjourned_pkgs( $options{actual_time} );
2233 my $error = $self->bill( %options );
2234 warn "Error billing, custnum ". $self->custnum. ": $error" if $error;
2236 $self->apply_payments_and_credits;
2238 unless ( $conf->exists('cancelled_cust-noevents')
2239 && ! $self->num_ncancelled_pkgs
2242 $error = $self->collect( %options );
2243 warn "Error collecting, custnum". $self->custnum. ": $error" if $error;
2249 sub cancel_expired_pkgs {
2250 my ( $self, $time ) = @_;
2252 my @cancel_pkgs = grep { $_->expire && $_->expire <= $time }
2253 $self->ncancelled_pkgs;
2255 foreach my $cust_pkg ( @cancel_pkgs ) {
2256 my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
2257 my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
2258 'reason_otaker' => $cpr->otaker
2262 warn "Error cancelling expired pkg ". $cust_pkg->pkgnum.
2263 " for custnum ". $self->custnum. ": $error"
2269 sub suspend_adjourned_pkgs {
2270 my ( $self, $time ) = @_;
2274 && ( ( $_->part_pkg->is_prepaid
2279 && $_->adjourn <= $time
2283 $self->ncancelled_pkgs;
2285 foreach my $cust_pkg ( @susp_pkgs ) {
2286 my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
2287 if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
2288 my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
2289 'reason_otaker' => $cpr->otaker
2294 warn "Error suspending package ". $cust_pkg->pkgnum.
2295 " for custnum ". $self->custnum. ": $error"
2303 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
2304 conjunction with the collect method by calling B<bill_and_collect>.
2306 If there is an error, returns the error, otherwise returns false.
2308 Options are passed as name-value pairs. Currently available options are:
2314 If set true, re-charges setup fees.
2318 Bills the customer as if it were that time. Specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion functions. For example:
2322 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
2326 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
2328 $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
2332 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices. Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
2339 my( $self, %options ) = @_;
2340 return '' if $self->payby eq 'COMP';
2341 warn "$me bill customer ". $self->custnum. "\n"
2344 my $time = $options{'time'} || time;
2345 my $invoice_time = $options{'invoice_time'} || $time;
2348 local $SIG{HUP} = 'IGNORE';
2349 local $SIG{INT} = 'IGNORE';
2350 local $SIG{QUIT} = 'IGNORE';
2351 local $SIG{TERM} = 'IGNORE';
2352 local $SIG{TSTP} = 'IGNORE';
2353 local $SIG{PIPE} = 'IGNORE';
2355 my $oldAutoCommit = $FS::UID::AutoCommit;
2356 local $FS::UID::AutoCommit = 0;
2359 $self->select_for_update; #mutex
2361 my @cust_bill_pkg = ();
2364 # find the packages which are due for billing, find out how much they are
2365 # & generate invoice database.
2368 my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 );
2370 my @precommit_hooks = ();
2372 my @cust_pkgs = qsearch('cust_pkg', { 'custnum' => $self->custnum } );
2373 foreach my $cust_pkg (@cust_pkgs) {
2375 #NO!! next if $cust_pkg->cancel;
2376 next if $cust_pkg->getfield('cancel');
2378 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
2380 #? to avoid use of uninitialized value errors... ?
2381 $cust_pkg->setfield('bill', '')
2382 unless defined($cust_pkg->bill);
2384 #my $part_pkg = $cust_pkg->part_pkg;
2386 my $real_pkgpart = $cust_pkg->pkgpart;
2387 my %hash = $cust_pkg->hash;
2389 foreach my $part_pkg ( $cust_pkg->part_pkg->self_and_bill_linked ) {
2391 $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
2394 $self->_make_lines( 'part_pkg' => $part_pkg,
2395 'cust_pkg' => $cust_pkg,
2396 'precommit_hooks' => \@precommit_hooks,
2397 'line_items' => \@cust_bill_pkg,
2398 'setup' => \$total_setup,
2399 'recur' => \$total_recur,
2400 'tax_matrix' => \%taxlisthash,
2402 'options' => \%options,
2405 $dbh->rollback if $oldAutoCommit;
2409 } #foreach my $part_pkg
2411 } #foreach my $cust_pkg
2413 unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items
2414 #but do commit any package date cycling that happened
2415 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2419 my $postal_pkg = $self->charge_postal_fee();
2420 if ( $postal_pkg && !ref( $postal_pkg ) ) {
2421 $dbh->rollback if $oldAutoCommit;
2422 return "can't charge postal invoice fee for customer ".
2423 $self->custnum. ": $postal_pkg";
2426 ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
2427 !$conf->exists('postal_invoice-recurring_only')
2431 foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
2433 $self->_make_lines( 'part_pkg' => $part_pkg,
2434 'cust_pkg' => $postal_pkg,
2435 'precommit_hooks' => \@precommit_hooks,
2436 'line_items' => \@cust_bill_pkg,
2437 'setup' => \$total_setup,
2438 'recur' => \$total_recur,
2439 'tax_matrix' => \%taxlisthash,
2441 'options' => \%options,
2444 $dbh->rollback if $oldAutoCommit;
2450 warn "having a look at the taxes we found...\n" if $DEBUG > 2;
2452 # keys are tax names (as printed on invoices / itemdesc )
2453 # values are listrefs of taxlisthash keys (internal identifiers)
2456 # keys are taxlisthash keys (internal identifiers)
2457 # values are (cumulative) amounts
2460 # keys are taxlisthash keys (internal identifiers)
2461 # values are listrefs of cust_bill_pkg_tax_location hashrefs
2462 my %tax_location = ();
2464 foreach my $tax ( keys %taxlisthash ) {
2465 my $tax_object = shift @{ $taxlisthash{$tax} };
2466 warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
2467 warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2;
2468 my $hashref_or_error =
2469 $tax_object->taxline( $taxlisthash{$tax},
2470 'custnum' => $self->custnum,
2471 'invoice_time' => $invoice_time
2473 unless ( ref($hashref_or_error) ) {
2474 $dbh->rollback if $oldAutoCommit;
2475 return $hashref_or_error;
2477 unshift @{ $taxlisthash{$tax} }, $tax_object;
2479 my $name = $hashref_or_error->{'name'};
2480 my $amount = $hashref_or_error->{'amount'};
2482 #warn "adding $amount as $name\n";
2483 $taxname{ $name } ||= [];
2484 push @{ $taxname{ $name } }, $tax;
2486 $tax{ $tax } += $amount;
2488 $tax_location{ $tax } ||= [];
2489 if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
2490 push @{ $tax_location{ $tax } },
2492 'taxnum' => $tax_object->taxnum,
2493 'taxtype' => ref($tax_object),
2494 'pkgnum' => $tax_object->get('pkgnum'),
2495 'locationnum' => $tax_object->get('locationnum'),
2496 'amount' => sprintf('%.2f', $amount ),
2502 #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
2503 my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
2504 foreach my $tax ( keys %taxlisthash ) {
2505 foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
2506 next unless ref($_) eq 'FS::cust_bill_pkg';
2508 push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
2509 splice( @{ $_->_cust_tax_exempt_pkg } );
2513 #consolidate and create tax line items
2514 warn "consolidating and generating...\n" if $DEBUG > 2;
2515 foreach my $taxname ( keys %taxname ) {
2518 my @cust_bill_pkg_tax_location = ();
2519 warn "adding $taxname\n" if $DEBUG > 1;
2520 foreach my $taxitem ( @{ $taxname{$taxname} } ) {
2521 next if $seen{$taxitem}++;
2522 warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
2523 $tax += $tax{$taxitem};
2524 push @cust_bill_pkg_tax_location,
2525 map { new FS::cust_bill_pkg_tax_location $_ }
2526 @{ $tax_location{ $taxitem } };
2530 $tax = sprintf('%.2f', $tax );
2531 $total_setup = sprintf('%.2f', $total_setup+$tax );
2533 push @cust_bill_pkg, new FS::cust_bill_pkg {
2539 'itemdesc' => $taxname,
2540 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
2545 my $charged = sprintf('%.2f', $total_setup + $total_recur );
2547 #create the new invoice
2548 my $cust_bill = new FS::cust_bill ( {
2549 'custnum' => $self->custnum,
2550 '_date' => ( $invoice_time ),
2551 'charged' => $charged,
2553 my $error = $cust_bill->insert;
2555 $dbh->rollback if $oldAutoCommit;
2556 return "can't create invoice for customer #". $self->custnum. ": $error";
2559 foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
2560 $cust_bill_pkg->invnum($cust_bill->invnum);
2561 my $error = $cust_bill_pkg->insert;
2563 $dbh->rollback if $oldAutoCommit;
2564 return "can't create invoice line item: $error";
2569 foreach my $hook ( @precommit_hooks ) {
2571 &{$hook}; #($self) ?
2574 $dbh->rollback if $oldAutoCommit;
2575 return "$@ running precommit hook $hook\n";
2579 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2585 my ($self, %params) = @_;
2587 my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
2588 my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
2589 my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
2590 my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
2591 my $total_setup = $params{setup} or die "no setup accumulator specified";
2592 my $total_recur = $params{recur} or die "no recur accumulator specified";
2593 my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
2594 my $time = $params{'time'} or die "no time specified";
2595 my (%options) = %{$params{options}};
2598 my $real_pkgpart = $cust_pkg->pkgpart;
2599 my %hash = $cust_pkg->hash;
2600 my $old_cust_pkg = new FS::cust_pkg \%hash;
2606 $cust_pkg->pkgpart($part_pkg->pkgpart);
2614 if ( ! $cust_pkg->setup &&
2616 ( $conf->exists('disable_setup_suspended_pkgs') &&
2617 ! $cust_pkg->getfield('susp')
2618 ) || ! $conf->exists('disable_setup_suspended_pkgs')
2620 || $options{'resetup'}
2623 warn " bill setup\n" if $DEBUG > 1;
2626 $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
2627 return "$@ running calc_setup for $cust_pkg\n"
2630 $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
2632 $cust_pkg->setfield('setup', $time)
2633 unless $cust_pkg->setup;
2634 #do need it, but it won't get written to the db
2635 #|| $cust_pkg->pkgpart != $real_pkgpart;
2640 # bill recurring fee
2643 #XXX unit stuff here too
2647 if ( ! $cust_pkg->getfield('susp') and
2648 ( $part_pkg->getfield('freq') ne '0' &&
2649 ( $cust_pkg->getfield('bill') || 0 ) <= $time
2651 || ( $part_pkg->plan eq 'voip_cdr'
2652 && $part_pkg->option('bill_every_call')
2656 # XXX should this be a package event? probably. events are called
2657 # at collection time at the moment, though...
2658 $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
2659 if $part_pkg->can('reset_usage');
2660 #don't want to reset usage just cause we want a line item??
2661 #&& $part_pkg->pkgpart == $real_pkgpart;
2663 warn " bill recur\n" if $DEBUG > 1;
2666 # XXX shared with $recur_prog
2667 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
2669 #over two params! lets at least switch to a hashref for the rest...
2670 my $increment_next_bill = ( $part_pkg->freq ne '0'
2671 && ( $cust_pkg->getfield('bill') || 0 ) <= $time
2673 my %param = ( 'precommit_hooks' => $precommit_hooks,
2674 'increment_next_bill' => $increment_next_bill,
2677 $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
2678 return "$@ running calc_recur for $cust_pkg\n"
2681 if ( $increment_next_bill ) {
2683 my $next_bill = $part_pkg->add_freq($sdate);
2684 return "unparsable frequency: ". $part_pkg->freq
2685 if $next_bill == -1;
2687 #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
2688 # only for figuring next bill date, nothing else, so, reset $sdate again
2690 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
2691 #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
2692 $cust_pkg->last_bill($sdate);
2694 $cust_pkg->setfield('bill', $next_bill );
2700 warn "\$setup is undefined" unless defined($setup);
2701 warn "\$recur is undefined" unless defined($recur);
2702 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
2705 # If there's line items, create em cust_bill_pkg records
2706 # If $cust_pkg has been modified, update it (if we're a real pkgpart)
2711 if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
2712 # hmm.. and if just the options are modified in some weird price plan?
2714 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
2717 my $error = $cust_pkg->replace( $old_cust_pkg,
2718 'options' => { $cust_pkg->options },
2720 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
2721 if $error; #just in case
2724 $setup = sprintf( "%.2f", $setup );
2725 $recur = sprintf( "%.2f", $recur );
2726 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
2727 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
2729 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
2730 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
2733 if ( $setup != 0 || $recur != 0 ) {
2735 warn " charges (setup=$setup, recur=$recur); adding line items\n"
2738 my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
2740 warn " adding customer package invoice detail: $_\n"
2741 foreach @cust_pkg_detail;
2743 push @details, @cust_pkg_detail;
2745 my $cust_bill_pkg = new FS::cust_bill_pkg {
2746 'pkgnum' => $cust_pkg->pkgnum,
2748 'unitsetup' => $unitsetup,
2750 'unitrecur' => $unitrecur,
2751 'quantity' => $cust_pkg->quantity,
2752 'details' => \@details,
2755 if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
2756 $cust_bill_pkg->sdate( $hash{last_bill} );
2757 $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1
2758 } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) {
2759 $cust_bill_pkg->sdate( $sdate );
2760 $cust_bill_pkg->edate( $cust_pkg->bill );
2763 $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
2764 unless $part_pkg->pkgpart == $real_pkgpart;
2766 $$total_setup += $setup;
2767 $$total_recur += $recur;
2774 $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time});
2775 return $error if $error;
2777 push @$cust_bill_pkgs, $cust_bill_pkg;
2779 } #if $setup != 0 || $recur != 0
2789 my $part_pkg = shift;
2790 my $taxlisthash = shift;
2791 my $cust_bill_pkg = shift;
2792 my $cust_pkg = shift;
2793 my $invoice_time = shift;
2795 my %cust_bill_pkg = ();
2799 #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
2800 push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
2801 push @classes, 'setup' if $cust_bill_pkg->setup;
2802 push @classes, 'recur' if $cust_bill_pkg->recur;
2804 if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
2806 if ( $conf->exists('enable_taxproducts')
2807 && ( scalar($part_pkg->part_pkg_taxoverride)
2808 || $part_pkg->has_taxproduct
2813 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
2814 return "fatal: Can't (yet) use tax-pkg_address with taxproducts";
2817 foreach my $class (@classes) {
2818 my $err_or_ref = $self->_gather_taxes( $part_pkg, $class );
2819 return $err_or_ref unless ref($err_or_ref);
2820 $taxes{$class} = $err_or_ref;
2823 unless (exists $taxes{''}) {
2824 my $err_or_ref = $self->_gather_taxes( $part_pkg, '' );
2825 return $err_or_ref unless ref($err_or_ref);
2826 $taxes{''} = $err_or_ref;
2831 my @loc_keys = qw( state county country );
2833 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
2834 my $cust_location = $cust_pkg->cust_location;
2835 %taxhash = map { $_ => $cust_location->$_() } @loc_keys;
2838 ( $conf->exists('tax-ship_address') && length($self->ship_last) )
2841 %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
2844 $taxhash{'taxclass'} = $part_pkg->taxclass;
2846 my @taxes = qsearch( 'cust_main_county', \%taxhash );
2848 my %taxhash_elim = %taxhash;
2850 my @elim = qw( taxclass county state );
2851 while ( !scalar(@taxes) && scalar(@elim) ) {
2852 $taxhash_elim{ shift(@elim) } = '';
2853 @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
2856 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
2858 $_->set('pkgnum', $cust_pkg->pkgnum );
2859 $_->set('locationnum', $cust_pkg->locationnum );
2863 $taxes{''} = [ @taxes ];
2864 $taxes{'setup'} = [ @taxes ];
2865 $taxes{'recur'} = [ @taxes ];
2866 $taxes{$_} = [ @taxes ] foreach (@classes);
2868 # maybe eliminate this entirely, along with all the 0% records
2871 "fatal: can't find tax rate for state/county/country/taxclass ".
2872 join('/', map $taxhash{$_}, qw(state county country taxclass) );
2875 } #if $conf->exists('enable_taxproducts') ...
2880 if ( $conf->exists('separate_usage') ) {
2881 my $section = $cust_pkg->part_pkg->option('usage_section', 'Hush!');
2882 my $summary = $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
2883 push @display, new FS::cust_bill_pkg_display { type => 'S' };
2884 push @display, new FS::cust_bill_pkg_display { type => 'R' };
2885 push @display, new FS::cust_bill_pkg_display { type => 'U',
2888 if ($section && $summary) {
2889 $display[2]->post_total('Y');
2890 push @display, new FS::cust_bill_pkg_display { type => 'U',
2895 $cust_bill_pkg->set('display', \@display);
2897 my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
2898 foreach my $key (keys %tax_cust_bill_pkg) {
2899 my @taxes = @{ $taxes{$key} || [] };
2900 my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
2902 my %localtaxlisthash = ();
2903 foreach my $tax ( @taxes ) {
2905 my $taxname = ref( $tax ). ' '. $tax->taxnum;
2906 # $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
2907 # ' locationnum'. $cust_pkg->locationnum
2908 # if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
2910 $taxlisthash->{ $taxname } ||= [ $tax ];
2911 push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
2913 $localtaxlisthash{ $taxname } ||= [ $tax ];
2914 push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
2918 warn "finding taxed taxes...\n" if $DEBUG > 2;
2919 foreach my $tax ( keys %localtaxlisthash ) {
2920 my $tax_object = shift @{ $localtaxlisthash{$tax} };
2921 warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
2923 next unless $tax_object->can('tax_on_tax');
2925 foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
2926 my $totname = ref( $tot ). ' '. $tot->taxnum;
2928 warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
2930 next unless exists( $localtaxlisthash{ $totname } ); # only increase
2932 warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
2933 my $hashref_or_error =
2934 $tax_object->taxline( $localtaxlisthash{$tax},
2935 'custnum' => $self->custnum,
2936 'invoice_time' => $invoice_time,
2938 return $hashref_or_error
2939 unless ref($hashref_or_error);
2941 $taxlisthash->{ $totname } ||= [ $tot ];
2942 push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
2954 my $part_pkg = shift;
2958 my $geocode = $self->geocode('cch');
2960 my @taxclassnums = map { $_->taxclassnum }
2961 $part_pkg->part_pkg_taxoverride($class);
2963 unless (@taxclassnums) {
2964 @taxclassnums = map { $_->taxclassnum }
2965 $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
2967 warn "Found taxclassnum values of ". join(',', @taxclassnums)
2972 join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
2974 @taxes = qsearch({ 'table' => 'tax_rate',
2975 'hashref' => { 'geocode' => $geocode, },
2976 'extra_sql' => $extra_sql,
2978 if scalar(@taxclassnums);
2980 warn "Found taxes ".
2981 join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
2988 =item collect OPTIONS
2990 (Attempt to) collect money for this customer's outstanding invoices (see
2991 L<FS::cust_bill>). Usually used after the bill method.
2993 Actions are now triggered by billing events; see L<FS::part_event> and the
2994 billing events web interface. Old-style invoice events (see
2995 L<FS::part_bill_event>) have been deprecated.
2997 If there is an error, returns the error, otherwise returns false.
2999 Options are passed as name-value pairs.
3001 Currently available options are:
3007 Use this time when deciding when to print invoices and 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> for conversion functions.
3011 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
3015 set true to surpress email card/ACH decline notices.
3019 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
3023 allows for one time override of normal customer billing method
3027 Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
3035 my( $self, %options ) = @_;
3036 my $invoice_time = $options{'invoice_time'} || time;
3039 local $SIG{HUP} = 'IGNORE';
3040 local $SIG{INT} = 'IGNORE';
3041 local $SIG{QUIT} = 'IGNORE';
3042 local $SIG{TERM} = 'IGNORE';
3043 local $SIG{TSTP} = 'IGNORE';
3044 local $SIG{PIPE} = 'IGNORE';
3046 my $oldAutoCommit = $FS::UID::AutoCommit;
3047 local $FS::UID::AutoCommit = 0;
3050 $self->select_for_update; #mutex
3053 my $balance = $self->balance;
3054 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
3057 if ( exists($options{'retry_card'}) ) {
3058 carp 'retry_card option passed to collect is deprecated; use retry';
3059 $options{'retry'} ||= $options{'retry_card'};
3061 if ( exists($options{'retry'}) && $options{'retry'} ) {
3062 my $error = $self->retry_realtime;
3064 $dbh->rollback if $oldAutoCommit;
3069 # false laziness w/pay_batch::import_results
3071 my $due_cust_event = $self->due_cust_event(
3072 'debug' => ( $options{'debug'} || 0 ),
3073 'time' => $invoice_time,
3074 'check_freq' => $options{'check_freq'},
3076 unless( ref($due_cust_event) ) {
3077 $dbh->rollback if $oldAutoCommit;
3078 return $due_cust_event;
3081 foreach my $cust_event ( @$due_cust_event ) {
3085 #re-eval event conditions (a previous event could have changed things)
3086 unless ( $cust_event->test_conditions( 'time' => $invoice_time ) ) {
3087 #don't leave stray "new/locked" records around
3088 my $error = $cust_event->delete;
3090 #gah, even with transactions
3091 $dbh->commit if $oldAutoCommit; #well.
3098 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
3099 warn " running cust_event ". $cust_event->eventnum. "\n"
3103 #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
3104 if ( my $error = $cust_event->do_event() ) {
3105 #XXX wtf is this? figure out a proper dealio with return value
3107 # gah, even with transactions.
3108 $dbh->commit if $oldAutoCommit; #well.
3115 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3120 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
3122 Inserts database records for and returns an ordered listref of new events due
3123 for this customer, as FS::cust_event objects (see L<FS::cust_event>). If no
3124 events are due, an empty listref is returned. If there is an error, returns a
3125 scalar error message.
3127 To actually run the events, call each event's test_condition method, and if
3128 still true, call the event's do_event method.
3130 Options are passed as a hashref or as a list of name-value pairs. Available
3137 Search only for events of this check frequency (how often events of this type are checked); currently "1d" (daily, the default) and "1m" (monthly) are recognized.
3141 "Current time" for the events.
3145 Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
3149 Only return events for the specified eventtable (by default, events of all eventtables are returned)
3153 Explicitly pass the objects to be tested (typically used with eventtable).
3157 Set to true to return the objects, but not actually insert them into the
3164 sub due_cust_event {
3166 my %opt = ref($_[0]) ? %{ $_[0] } : @_;
3169 #my $DEBUG = $opt{'debug'}
3170 local($DEBUG) = $opt{'debug'}
3171 if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
3173 warn "$me due_cust_event called with options ".
3174 join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
3177 $opt{'time'} ||= time;
3179 local $SIG{HUP} = 'IGNORE';
3180 local $SIG{INT} = 'IGNORE';
3181 local $SIG{QUIT} = 'IGNORE';
3182 local $SIG{TERM} = 'IGNORE';
3183 local $SIG{TSTP} = 'IGNORE';
3184 local $SIG{PIPE} = 'IGNORE';
3186 my $oldAutoCommit = $FS::UID::AutoCommit;
3187 local $FS::UID::AutoCommit = 0;
3190 $self->select_for_update #mutex
3191 unless $opt{testonly};
3194 # 1: find possible events (initial search)
3197 my @cust_event = ();
3199 my @eventtable = $opt{'eventtable'}
3200 ? ( $opt{'eventtable'} )
3201 : FS::part_event->eventtables_runorder;
3203 foreach my $eventtable ( @eventtable ) {
3206 if ( $opt{'objects'} ) {
3208 @objects = @{ $opt{'objects'} };
3212 #my @objects = $self->eventtable(); # sub cust_main { @{ [ $self ] }; }
3213 @objects = ( $eventtable eq 'cust_main' )
3215 : ( $self->$eventtable() );
3219 my @e_cust_event = ();
3221 my $cross = "CROSS JOIN $eventtable";
3222 $cross .= ' LEFT JOIN cust_main USING ( custnum )'
3223 unless $eventtable eq 'cust_main';
3225 foreach my $object ( @objects ) {
3227 #this first search uses the condition_sql magic for optimization.
3228 #the more possible events we can eliminate in this step the better
3230 my $cross_where = '';
3231 my $pkey = $object->primary_key;
3232 $cross_where = "$eventtable.$pkey = ". $object->$pkey();
3234 my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
3236 FS::part_event_condition->where_conditions_sql( $eventtable,
3237 'time'=>$opt{'time'}
3239 my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
3241 $extra_sql = "AND $extra_sql" if $extra_sql;
3243 #here is the agent virtualization
3244 $extra_sql .= " AND ( part_event.agentnum IS NULL
3245 OR part_event.agentnum = ". $self->agentnum. ' )';
3247 $extra_sql .= " $order";
3249 warn "searching for events for $eventtable ". $object->$pkey. "\n"
3250 if $opt{'debug'} > 2;
3251 my @part_event = qsearch( {
3252 'debug' => ( $opt{'debug'} > 3 ? 1 : 0 ),
3253 'select' => 'part_event.*',
3254 'table' => 'part_event',
3255 'addl_from' => "$cross $join",
3256 'hashref' => { 'check_freq' => ( $opt{'check_freq'} || '1d' ),
3257 'eventtable' => $eventtable,
3260 'extra_sql' => "AND $cross_where $extra_sql",
3264 my $pkey = $object->primary_key;
3265 warn " ". scalar(@part_event).
3266 " possible events found for $eventtable ". $object->$pkey(). "\n";
3269 push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
3273 warn " ". scalar(@e_cust_event).
3274 " subtotal possible cust events found for $eventtable\n"
3277 push @cust_event, @e_cust_event;
3281 warn " ". scalar(@cust_event).
3282 " total possible cust events found in initial search\n"
3286 # 2: test conditions
3291 @cust_event = grep $_->test_conditions( 'time' => $opt{'time'},
3292 'stats_hashref' => \%unsat ),
3295 warn " ". scalar(@cust_event). " cust events left satisfying conditions\n"
3298 warn " invalid conditions not eliminated with condition_sql:\n".
3299 join('', map " $_: ".$unsat{$_}."\n", keys %unsat )
3306 unless( $opt{testonly} ) {
3307 foreach my $cust_event ( @cust_event ) {
3309 my $error = $cust_event->insert();
3311 $dbh->rollback if $oldAutoCommit;
3318 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3324 warn " returning events: ". Dumper(@cust_event). "\n"
3331 =item retry_realtime
3333 Schedules realtime / batch credit card / electronic check / LEC billing
3334 events for for retry. Useful if card information has changed or manual
3335 retry is desired. The 'collect' method must be called to actually retry
3338 Implementation details: For either this customer, or for each of this
3339 customer's open invoices, changes the status of the first "done" (with
3340 statustext error) realtime processing event to "failed".
3344 sub retry_realtime {
3347 local $SIG{HUP} = 'IGNORE';
3348 local $SIG{INT} = 'IGNORE';
3349 local $SIG{QUIT} = 'IGNORE';
3350 local $SIG{TERM} = 'IGNORE';
3351 local $SIG{TSTP} = 'IGNORE';
3352 local $SIG{PIPE} = 'IGNORE';
3354 my $oldAutoCommit = $FS::UID::AutoCommit;
3355 local $FS::UID::AutoCommit = 0;
3358 #a little false laziness w/due_cust_event (not too bad, really)
3360 my $join = FS::part_event_condition->join_conditions_sql;
3361 my $order = FS::part_event_condition->order_conditions_sql;
3364 . join ( ' OR ' , map {
3365 "( part_event.eventtable = " . dbh->quote($_)
3366 . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ;
3367 } FS::part_event->eventtables)
3370 #here is the agent virtualization
3371 my $agent_virt = " ( part_event.agentnum IS NULL
3372 OR part_event.agentnum = ". $self->agentnum. ' )';
3374 #XXX this shouldn't be hardcoded, actions should declare it...
3375 my @realtime_events = qw(
3376 cust_bill_realtime_card
3377 cust_bill_realtime_check
3378 cust_bill_realtime_lec
3382 my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'",
3387 my @cust_event = qsearchs({
3388 'table' => 'cust_event',
3389 'select' => 'cust_event.*',
3390 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
3391 'hashref' => { 'status' => 'done' },
3392 'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ".
3393 " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
3396 my %seen_invnum = ();
3397 foreach my $cust_event (@cust_event) {
3399 #max one for the customer, one for each open invoice
3400 my $cust_X = $cust_event->cust_X;
3401 next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
3405 or $cust_event->part_event->eventtable eq 'cust_bill'
3408 my $error = $cust_event->retry;
3410 $dbh->rollback if $oldAutoCommit;
3411 return "error scheduling event for retry: $error";
3416 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3421 # some horrid false laziness here to avoid refactor fallout
3422 # eventually realtime realtime_bop and realtime_refund_bop should go
3423 # away and be replaced by _new_realtime_bop and _new_realtime_refund_bop
3425 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
3427 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
3428 via a Business::OnlinePayment realtime gateway. See
3429 L<http://420.am/business-onlinepayment> for supported gateways.
3431 Available methods are: I<CC>, I<ECHECK> and I<LEC>
3433 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
3435 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
3436 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
3437 if set, will override the value from the customer record.
3439 I<description> is a free-text field passed to the gateway. It defaults to
3440 "Internet services".
3442 If an I<invnum> is specified, this payment (if successful) is applied to the
3443 specified invoice. If you don't specify an I<invnum> you might want to
3444 call the B<apply_payments> method.
3446 I<quiet> can be set true to surpress email decline notices.
3448 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
3449 resulting paynum, if any.
3451 I<payunique> is a unique identifier for this payment.
3453 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
3460 return $self->_new_realtime_bop(@_)
3461 if $self->_new_bop_required();
3463 my( $method, $amount, %options ) = @_;
3465 warn "$me realtime_bop: $method $amount\n";
3466 warn " $_ => $options{$_}\n" foreach keys %options;
3469 $options{'description'} ||= 'Internet services';
3471 return $self->fake_bop($method, $amount, %options) if $options{'fake'};
3473 eval "use Business::OnlinePayment";
3476 my $payinfo = exists($options{'payinfo'})
3477 ? $options{'payinfo'}
3480 my %method2payby = (
3487 # check for banned credit card/ACH
3490 my $ban = qsearchs('banned_pay', {
3491 'payby' => $method2payby{$method},
3492 'payinfo' => md5_base64($payinfo),
3494 return "Banned credit card" if $ban;
3497 # set taxclass and trans_is_recur based on invnum if there is one
3501 my $trans_is_recur = 0;
3502 if ( $options{'invnum'} ) {
3504 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
3505 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
3508 map { $_->part_pkg }
3510 map { $_->cust_pkg }
3511 $cust_bill->cust_bill_pkg;
3513 my @taxclasses = map $_->taxclass, @part_pkg;
3514 $taxclass = $taxclasses[0]
3515 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
3516 #different taxclasses
3518 if grep { $_->freq ne '0' } @part_pkg;
3526 #look for an agent gateway override first
3528 if ( $method eq 'CC' ) {
3529 $cardtype = cardtype($payinfo);
3530 } elsif ( $method eq 'ECHECK' ) {
3533 $cardtype = $method;
3537 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
3538 cardtype => $cardtype,
3539 taxclass => $taxclass, } )
3540 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
3542 taxclass => $taxclass, } )
3543 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
3544 cardtype => $cardtype,
3546 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
3548 taxclass => '', } );
3550 my $payment_gateway = '';
3551 my( $processor, $login, $password, $action, @bop_options );
3552 if ( $override ) { #use a payment gateway override
3554 $payment_gateway = $override->payment_gateway;
3556 $processor = $payment_gateway->gateway_module;
3557 $login = $payment_gateway->gateway_username;
3558 $password = $payment_gateway->gateway_password;
3559 $action = $payment_gateway->gateway_action;
3560 @bop_options = $payment_gateway->options;
3562 } else { #use the standard settings from the config
3564 ( $processor, $login, $password, $action, @bop_options ) =
3565 $self->default_payment_gateway($method);
3573 my $address = exists($options{'address1'})
3574 ? $options{'address1'}
3576 my $address2 = exists($options{'address2'})
3577 ? $options{'address2'}
3579 $address .= ", ". $address2 if length($address2);
3581 my $o_payname = exists($options{'payname'})
3582 ? $options{'payname'}
3584 my($payname, $payfirst, $paylast);
3585 if ( $o_payname && $method ne 'ECHECK' ) {
3586 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
3587 or return "Illegal payname $payname";
3588 ($payfirst, $paylast) = ($1, $2);
3590 $payfirst = $self->getfield('first');
3591 $paylast = $self->getfield('last');
3592 $payname = "$payfirst $paylast";
3595 my @invoicing_list = $self->invoicing_list_emailonly;
3596 if ( $conf->exists('emailinvoiceautoalways')
3597 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
3598 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
3599 push @invoicing_list, $self->all_emails;
3602 my $email = ($conf->exists('business-onlinepayment-email-override'))
3603 ? $conf->config('business-onlinepayment-email-override')
3604 : $invoicing_list[0];
3608 my $payip = exists($options{'payip'})
3611 $content{customer_ip} = $payip
3614 $content{invoice_number} = $options{'invnum'}
3615 if exists($options{'invnum'}) && length($options{'invnum'});
3617 $content{email_customer} =
3618 ( $conf->exists('business-onlinepayment-email_customer')
3619 || $conf->exists('business-onlinepayment-email-override') );
3622 if ( $method eq 'CC' ) {
3624 $content{card_number} = $payinfo;
3625 $paydate = exists($options{'paydate'})
3626 ? $options{'paydate'}
3628 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
3629 $content{expiration} = "$2/$1";
3631 my $paycvv = exists($options{'paycvv'})
3632 ? $options{'paycvv'}
3634 $content{cvv2} = $paycvv
3637 my $paystart_month = exists($options{'paystart_month'})
3638 ? $options{'paystart_month'}
3639 : $self->paystart_month;
3641 my $paystart_year = exists($options{'paystart_year'})
3642 ? $options{'paystart_year'}
3643 : $self->paystart_year;
3645 $content{card_start} = "$paystart_month/$paystart_year"
3646 if $paystart_month && $paystart_year;
3648 my $payissue = exists($options{'payissue'})
3649 ? $options{'payissue'}
3651 $content{issue_number} = $payissue if $payissue;
3653 if ( $self->_bop_recurring_billing( 'payinfo' => $payinfo,
3654 'trans_is_recur' => $trans_is_recur,
3658 $content{recurring_billing} = 'YES';
3659 $content{acct_code} = 'rebill'
3660 if $conf->exists('credit_card-recurring_billing_acct_code');
3663 } elsif ( $method eq 'ECHECK' ) {
3664 ( $content{account_number}, $content{routing_code} ) =
3665 split('@', $payinfo);
3666 $content{bank_name} = $o_payname;
3667 $content{bank_state} = exists($options{'paystate'})
3668 ? $options{'paystate'}
3669 : $self->getfield('paystate');
3670 $content{account_type} = exists($options{'paytype'})
3671 ? uc($options{'paytype'}) || 'CHECKING'
3672 : uc($self->getfield('paytype')) || 'CHECKING';
3673 $content{account_name} = $payname;
3674 $content{customer_org} = $self->company ? 'B' : 'I';
3675 $content{state_id} = exists($options{'stateid'})
3676 ? $options{'stateid'}
3677 : $self->getfield('stateid');
3678 $content{state_id_state} = exists($options{'stateid_state'})
3679 ? $options{'stateid_state'}
3680 : $self->getfield('stateid_state');
3681 $content{customer_ssn} = exists($options{'ss'})
3684 } elsif ( $method eq 'LEC' ) {
3685 $content{phone} = $payinfo;
3689 # run transaction(s)
3692 my $balance = exists( $options{'balance'} )
3693 ? $options{'balance'}
3696 $self->select_for_update; #mutex ... just until we get our pending record in
3698 #the checks here are intended to catch concurrent payments
3699 #double-form-submission prevention is taken care of in cust_pay_pending::check
3702 return "The customer's balance has changed; $method transaction aborted."
3703 if $self->balance < $balance;
3704 #&& $self->balance < $amount; #might as well anyway?
3706 #also check and make sure there aren't *other* pending payments for this cust
3708 my @pending = qsearch('cust_pay_pending', {
3709 'custnum' => $self->custnum,
3710 'status' => { op=>'!=', value=>'done' }
3712 return "A payment is already being processed for this customer (".
3713 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
3714 "); $method transaction aborted."
3715 if scalar(@pending);
3717 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
3719 my $cust_pay_pending = new FS::cust_pay_pending {
3720 'custnum' => $self->custnum,
3721 #'invnum' => $options{'invnum'},
3724 'payby' => $method2payby{$method},
3725 'payinfo' => $payinfo,
3726 'paydate' => $paydate,
3727 'recurring_billing' => $content{recurring_billing},
3729 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
3731 $cust_pay_pending->payunique( $options{payunique} )
3732 if defined($options{payunique}) && length($options{payunique});
3733 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
3734 return $cpp_new_err if $cpp_new_err;
3736 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
3738 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
3739 $transaction->content(
3742 'password' => $password,
3743 'action' => $action1,
3744 'description' => $options{'description'},
3745 'amount' => $amount,
3746 #'invoice_number' => $options{'invnum'},
3747 'customer_id' => $self->custnum,
3748 'last_name' => $paylast,
3749 'first_name' => $payfirst,
3751 'address' => $address,
3752 'city' => ( exists($options{'city'})
3755 'state' => ( exists($options{'state'})
3758 'zip' => ( exists($options{'zip'})
3761 'country' => ( exists($options{'country'})
3762 ? $options{'country'}
3764 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
3766 'phone' => $self->daytime || $self->night,
3770 $cust_pay_pending->status('pending');
3771 my $cpp_pending_err = $cust_pay_pending->replace;
3772 return $cpp_pending_err if $cpp_pending_err;
3775 my $BOP_TESTING = 0;
3776 my $BOP_TESTING_SUCCESS = 1;
3778 unless ( $BOP_TESTING ) {
3779 $transaction->submit();
3781 if ( $BOP_TESTING_SUCCESS ) {
3782 $transaction->is_success(1);
3783 $transaction->authorization('fake auth');
3785 $transaction->is_success(0);
3786 $transaction->error_message('fake failure');
3790 if ( $transaction->is_success() && $action2 ) {
3792 $cust_pay_pending->status('authorized');
3793 my $cpp_authorized_err = $cust_pay_pending->replace;
3794 return $cpp_authorized_err if $cpp_authorized_err;
3796 my $auth = $transaction->authorization;
3797 my $ordernum = $transaction->can('order_number')
3798 ? $transaction->order_number
3802 new Business::OnlinePayment( $processor, @bop_options );
3809 password => $password,
3810 order_number => $ordernum,
3812 authorization => $auth,
3813 description => $options{'description'},
3816 foreach my $field (qw( authorization_source_code returned_ACI
3817 transaction_identifier validation_code
3818 transaction_sequence_num local_transaction_date
3819 local_transaction_time AVS_result_code )) {
3820 $capture{$field} = $transaction->$field() if $transaction->can($field);
3823 $capture->content( %capture );
3827 unless ( $capture->is_success ) {
3828 my $e = "Authorization successful but capture failed, custnum #".
3829 $self->custnum. ': '. $capture->result_code.
3830 ": ". $capture->error_message;
3837 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
3838 my $cpp_captured_err = $cust_pay_pending->replace;
3839 return $cpp_captured_err if $cpp_captured_err;
3842 # remove paycvv after initial transaction
3845 #false laziness w/misc/process/payment.cgi - check both to make sure working
3847 if ( defined $self->dbdef_table->column('paycvv')
3848 && length($self->paycvv)
3849 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
3851 my $error = $self->remove_cvv;
3853 warn "WARNING: error removing cvv: $error\n";
3861 if ( $transaction->is_success() ) {
3864 if ( $payment_gateway ) { # agent override
3865 $paybatch = $payment_gateway->gatewaynum. '-';
3868 $paybatch .= "$processor:". $transaction->authorization;
3870 $paybatch .= ':'. $transaction->order_number
3871 if $transaction->can('order_number')
3872 && length($transaction->order_number);
3874 my $cust_pay = new FS::cust_pay ( {
3875 'custnum' => $self->custnum,
3876 'invnum' => $options{'invnum'},
3879 'payby' => $method2payby{$method},
3880 'payinfo' => $payinfo,
3881 'paybatch' => $paybatch,
3882 'paydate' => $paydate,
3884 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
3885 $cust_pay->payunique( $options{payunique} )
3886 if defined($options{payunique}) && length($options{payunique});
3888 my $oldAutoCommit = $FS::UID::AutoCommit;
3889 local $FS::UID::AutoCommit = 0;
3892 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
3894 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
3897 $cust_pay->invnum(''); #try again with no specific invnum
3898 my $error2 = $cust_pay->insert( $options{'manual'} ?
3899 ( 'manual' => 1 ) : ()
3902 # gah. but at least we have a record of the state we had to abort in
3903 # from cust_pay_pending now.
3904 my $e = "WARNING: $method captured but payment not recorded - ".
3905 "error inserting payment ($processor): $error2".
3906 " (previously tried insert with invnum #$options{'invnum'}" .
3907 ": $error ) - pending payment saved as paypendingnum ".
3908 $cust_pay_pending->paypendingnum. "\n";
3914 if ( $options{'paynum_ref'} ) {
3915 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
3918 $cust_pay_pending->status('done');
3919 $cust_pay_pending->statustext('captured');
3920 $cust_pay_pending->paynum($cust_pay->paynum);
3921 my $cpp_done_err = $cust_pay_pending->replace;
3923 if ( $cpp_done_err ) {
3925 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
3926 my $e = "WARNING: $method captured but payment not recorded - ".
3927 "error updating status for paypendingnum ".
3928 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
3934 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3935 return ''; #no error
3941 my $perror = "$processor error: ". $transaction->error_message;
3943 unless ( $transaction->error_message ) {
3946 if ( $transaction->can('response_page') ) {
3948 'page' => ( $transaction->can('response_page')
3949 ? $transaction->response_page
3952 'code' => ( $transaction->can('response_code')
3953 ? $transaction->response_code
3956 'headers' => ( $transaction->can('response_headers')
3957 ? $transaction->response_headers
3963 "No additional debugging information available for $processor";
3966 $perror .= "No error_message returned from $processor -- ".
3967 ( ref($t_response) ? Dumper($t_response) : $t_response );
3971 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
3972 && $conf->exists('emaildecline')
3973 && grep { $_ ne 'POST' } $self->invoicing_list
3974 && ! grep { $transaction->error_message =~ /$_/ }
3975 $conf->config('emaildecline-exclude')
3977 my @templ = $conf->config('declinetemplate');
3978 my $template = new Text::Template (
3980 SOURCE => [ map "$_\n", @templ ],
3981 ) or return "($perror) can't create template: $Text::Template::ERROR";
3982 $template->compile()
3983 or return "($perror) can't compile template: $Text::Template::ERROR";
3985 my $templ_hash = { error => $transaction->error_message };
3987 my $error = send_email(
3988 'from' => $conf->config('invoice_from', $self->agentnum ),
3989 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
3990 'subject' => 'Your payment could not be processed',
3991 'body' => [ $template->fill_in(HASH => $templ_hash) ],
3994 $perror .= " (also received error sending decline notification: $error)"
3999 $cust_pay_pending->status('done');
4000 $cust_pay_pending->statustext("declined: $perror");
4001 my $cpp_done_err = $cust_pay_pending->replace;
4002 if ( $cpp_done_err ) {
4003 my $e = "WARNING: $method declined but pending payment not resolved - ".
4004 "error updating status for paypendingnum ".
4005 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
4007 $perror = "$e ($perror)";
4015 sub _bop_recurring_billing {
4016 my( $self, %opt ) = @_;
4018 my $method = $conf->config('credit_card-recurring_billing_flag');
4020 if ( $method eq 'transaction_is_recur' ) {
4022 return 1 if $opt{'trans_is_recur'};
4026 my %hash = ( 'custnum' => $self->custnum,
4031 if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
4032 || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
4043 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
4045 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
4046 via a Business::OnlinePayment realtime gateway. See
4047 L<http://420.am/business-onlinepayment> for supported gateways.
4049 Available methods are: I<CC>, I<ECHECK> and I<LEC>
4051 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
4053 Most gateways require a reference to an original payment transaction to refund,
4054 so you probably need to specify a I<paynum>.
4056 I<amount> defaults to the original amount of the payment if not specified.
4058 I<reason> specifies a reason for the refund.
4060 I<paydate> specifies the expiration date for a credit card overriding the
4061 value from the customer record or the payment record. Specified as yyyy-mm-dd
4063 Implementation note: If I<amount> is unspecified or equal to the amount of the
4064 orignal payment, first an attempt is made to "void" the transaction via
4065 the gateway (to cancel a not-yet settled transaction) and then if that fails,
4066 the normal attempt is made to "refund" ("credit") the transaction via the
4067 gateway is attempted.
4069 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
4070 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
4071 #if set, will override the value from the customer record.
4073 #If an I<invnum> is specified, this payment (if successful) is applied to the
4074 #specified invoice. If you don't specify an I<invnum> you might want to
4075 #call the B<apply_payments> method.
4079 #some false laziness w/realtime_bop, not enough to make it worth merging
4080 #but some useful small subs should be pulled out
4081 sub realtime_refund_bop {
4084 return $self->_new_realtime_refund_bop(@_)
4085 if $self->_new_bop_required();
4087 my( $method, %options ) = @_;
4089 warn "$me realtime_refund_bop: $method refund\n";
4090 warn " $_ => $options{$_}\n" foreach keys %options;
4093 eval "use Business::OnlinePayment";
4097 # look up the original payment and optionally a gateway for that payment
4101 my $amount = $options{'amount'};
4103 my( $processor, $login, $password, @bop_options ) ;
4104 my( $auth, $order_number ) = ( '', '', '' );
4106 if ( $options{'paynum'} ) {
4108 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
4109 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
4110 or return "Unknown paynum $options{'paynum'}";
4111 $amount ||= $cust_pay->paid;
4113 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
4114 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
4115 $cust_pay->paybatch;
4116 my $gatewaynum = '';
4117 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
4119 if ( $gatewaynum ) { #gateway for the payment to be refunded
4121 my $payment_gateway =
4122 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
4123 die "payment gateway $gatewaynum not found"
4124 unless $payment_gateway;
4126 $processor = $payment_gateway->gateway_module;
4127 $login = $payment_gateway->gateway_username;
4128 $password = $payment_gateway->gateway_password;
4129 @bop_options = $payment_gateway->options;
4131 } else { #try the default gateway
4133 my( $conf_processor, $unused_action );
4134 ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
4135 $self->default_payment_gateway($method);
4137 return "processor of payment $options{'paynum'} $processor does not".
4138 " match default processor $conf_processor"
4139 unless $processor eq $conf_processor;
4144 } else { # didn't specify a paynum, so look for agent gateway overrides
4145 # like a normal transaction
4148 if ( $method eq 'CC' ) {
4149 $cardtype = cardtype($self->payinfo);
4150 } elsif ( $method eq 'ECHECK' ) {
4153 $cardtype = $method;
4156 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4157 cardtype => $cardtype,
4159 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4161 taxclass => '', } );
4163 if ( $override ) { #use a payment gateway override
4165 my $payment_gateway = $override->payment_gateway;
4167 $processor = $payment_gateway->gateway_module;
4168 $login = $payment_gateway->gateway_username;
4169 $password = $payment_gateway->gateway_password;
4170 #$action = $payment_gateway->gateway_action;
4171 @bop_options = $payment_gateway->options;
4173 } else { #use the standard settings from the config
4176 ( $processor, $login, $password, $unused_action, @bop_options ) =
4177 $self->default_payment_gateway($method);
4182 return "neither amount nor paynum specified" unless $amount;
4187 'password' => $password,
4188 'order_number' => $order_number,
4189 'amount' => $amount,
4190 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
4192 $content{authorization} = $auth
4193 if length($auth); #echeck/ACH transactions have an order # but no auth
4194 #(at least with authorize.net)
4196 my $disable_void_after;
4197 if ($conf->exists('disable_void_after')
4198 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
4199 $disable_void_after = $1;
4202 #first try void if applicable
4203 if ( $cust_pay && $cust_pay->paid == $amount
4205 ( not defined($disable_void_after) )
4206 || ( time < ($cust_pay->_date + $disable_void_after ) )
4209 warn " attempting void\n" if $DEBUG > 1;
4210 my $void = new Business::OnlinePayment( $processor, @bop_options );
4211 $void->content( 'action' => 'void', %content );
4213 if ( $void->is_success ) {
4214 my $error = $cust_pay->void($options{'reason'});
4216 # gah, even with transactions.
4217 my $e = 'WARNING: Card/ACH voided but database not updated - '.
4218 "error voiding payment: $error";
4222 warn " void successful\n" if $DEBUG > 1;
4227 warn " void unsuccessful, trying refund\n"
4231 my $address = $self->address1;
4232 $address .= ", ". $self->address2 if $self->address2;
4234 my($payname, $payfirst, $paylast);
4235 if ( $self->payname && $method ne 'ECHECK' ) {
4236 $payname = $self->payname;
4237 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
4238 or return "Illegal payname $payname";
4239 ($payfirst, $paylast) = ($1, $2);
4241 $payfirst = $self->getfield('first');
4242 $paylast = $self->getfield('last');
4243 $payname = "$payfirst $paylast";
4246 my @invoicing_list = $self->invoicing_list_emailonly;
4247 if ( $conf->exists('emailinvoiceautoalways')
4248 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
4249 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
4250 push @invoicing_list, $self->all_emails;
4253 my $email = ($conf->exists('business-onlinepayment-email-override'))
4254 ? $conf->config('business-onlinepayment-email-override')
4255 : $invoicing_list[0];
4257 my $payip = exists($options{'payip'})
4260 $content{customer_ip} = $payip
4264 if ( $method eq 'CC' ) {
4267 $content{card_number} = $payinfo = $cust_pay->payinfo;
4268 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
4269 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
4270 ($content{expiration} = "$2/$1"); # where available
4272 $content{card_number} = $payinfo = $self->payinfo;
4273 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
4274 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
4275 $content{expiration} = "$2/$1";
4278 } elsif ( $method eq 'ECHECK' ) {
4281 $payinfo = $cust_pay->payinfo;
4283 $payinfo = $self->payinfo;
4285 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
4286 $content{bank_name} = $self->payname;
4287 $content{account_type} = 'CHECKING';
4288 $content{account_name} = $payname;
4289 $content{customer_org} = $self->company ? 'B' : 'I';
4290 $content{customer_ssn} = $self->ss;
4291 } elsif ( $method eq 'LEC' ) {
4292 $content{phone} = $payinfo = $self->payinfo;
4296 my $refund = new Business::OnlinePayment( $processor, @bop_options );
4297 my %sub_content = $refund->content(
4298 'action' => 'credit',
4299 'customer_id' => $self->custnum,
4300 'last_name' => $paylast,
4301 'first_name' => $payfirst,
4303 'address' => $address,
4304 'city' => $self->city,
4305 'state' => $self->state,
4306 'zip' => $self->zip,
4307 'country' => $self->country,
4309 'phone' => $self->daytime || $self->night,
4312 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
4316 return "$processor error: ". $refund->error_message
4317 unless $refund->is_success();
4319 my %method2payby = (
4325 my $paybatch = "$processor:". $refund->authorization;
4326 $paybatch .= ':'. $refund->order_number
4327 if $refund->can('order_number') && $refund->order_number;
4329 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
4330 my @cust_bill_pay = $cust_pay->cust_bill_pay;
4331 last unless @cust_bill_pay;
4332 my $cust_bill_pay = pop @cust_bill_pay;
4333 my $error = $cust_bill_pay->delete;
4337 my $cust_refund = new FS::cust_refund ( {
4338 'custnum' => $self->custnum,
4339 'paynum' => $options{'paynum'},
4340 'refund' => $amount,
4342 'payby' => $method2payby{$method},
4343 'payinfo' => $payinfo,
4344 'paybatch' => $paybatch,
4345 'reason' => $options{'reason'} || 'card or ACH refund',
4347 my $error = $cust_refund->insert;
4349 $cust_refund->paynum(''); #try again with no specific paynum
4350 my $error2 = $cust_refund->insert;
4352 # gah, even with transactions.
4353 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
4354 "error inserting refund ($processor): $error2".
4355 " (previously tried insert with paynum #$options{'paynum'}" .
4366 # does the configuration indicate the new bop routines are required?
4368 sub _new_bop_required {
4371 my $botpp = 'Business::OnlineThirdPartyPayment';
4374 if ( $conf->config('business-onlinepayment-namespace') eq $botpp ||
4375 scalar( grep { $_->gateway_namespace eq $botpp }
4376 qsearch( 'payment_gateway', { 'disabled' => '' } )
4385 =item realtime_collect [ OPTION => VALUE ... ]
4387 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
4388 via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime
4389 gateway. See L<http://420.am/business-onlinepayment> and
4390 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
4392 On failure returns an error message.
4394 Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url.
4396 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
4398 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
4399 then it is deduced from the customer record.
4401 If no I<amount> is specified, then the customer balance is used.
4403 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
4404 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
4405 if set, will override the value from the customer record.
4407 I<description> is a free-text field passed to the gateway. It defaults to
4408 "Internet services".
4410 If an I<invnum> is specified, this payment (if successful) is applied to the
4411 specified invoice. If you don't specify an I<invnum> you might want to
4412 call the B<apply_payments> method.
4414 I<quiet> can be set true to surpress email decline notices.
4416 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
4417 resulting paynum, if any.
4419 I<payunique> is a unique identifier for this payment.
4421 I<session_id> is a session identifier associated with this payment.
4423 I<depend_jobnum> allows payment capture to unlock export jobs
4427 sub realtime_collect {
4428 my( $self, %options ) = @_;
4431 warn "$me realtime_collect:\n";
4432 warn " $_ => $options{$_}\n" foreach keys %options;
4435 $options{amount} = $self->balance unless exists( $options{amount} );
4436 $options{method} = FS::payby->payby2bop($self->payby)
4437 unless exists( $options{method} );
4439 return $self->realtime_bop({%options});
4443 =item _realtime_bop { [ ARG => VALUE ... ] }
4445 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
4446 via a Business::OnlinePayment realtime gateway. See
4447 L<http://420.am/business-onlinepayment> for supported gateways.
4449 Required arguments in the hashref are I<method>, and I<amount>
4451 Available methods are: I<CC>, I<ECHECK> and I<LEC>
4453 Available optional arguments are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
4455 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
4456 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
4457 if set, will override the value from the customer record.
4459 I<description> is a free-text field passed to the gateway. It defaults to
4460 "Internet services".
4462 If an I<invnum> is specified, this payment (if successful) is applied to the
4463 specified invoice. If you don't specify an I<invnum> you might want to
4464 call the B<apply_payments> method.
4466 I<quiet> can be set true to surpress email decline notices.
4468 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
4469 resulting paynum, if any.
4471 I<payunique> is a unique identifier for this payment.
4473 I<session_id> is a session identifier associated with this payment.
4475 I<depend_jobnum> allows payment capture to unlock export jobs
4477 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
4481 # some helper routines
4482 sub _payment_gateway {
4483 my ($self, $options) = @_;
4485 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
4486 unless exists($options->{payment_gateway});
4488 $options->{payment_gateway};
4492 my ($self, $options) = @_;
4495 'login' => $options->{payment_gateway}->gateway_username,
4496 'password' => $options->{payment_gateway}->gateway_password,
4501 my ($self, $options) = @_;
4503 $options->{payment_gateway}->gatewaynum
4504 ? $options->{payment_gateway}->options
4505 : @{ $options->{payment_gateway}->get('options') };
4509 my ($self, $options) = @_;
4511 $options->{description} ||= 'Internet services';
4512 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
4513 $options->{invnum} ||= '';
4514 $options->{payname} = $self->payname unless exists( $options->{payname} );
4518 my ($self, $options) = @_;
4521 $content{address} = exists($options->{'address1'})
4522 ? $options->{'address1'}
4524 my $address2 = exists($options->{'address2'})
4525 ? $options->{'address2'}
4527 $content{address} .= ", ". $address2 if length($address2);
4529 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
4530 $content{customer_ip} = $payip if length($payip);
4532 $content{invoice_number} = $options->{'invnum'}
4533 if exists($options->{'invnum'}) && length($options->{'invnum'});
4535 $content{email_customer} =
4536 ( $conf->exists('business-onlinepayment-email_customer')
4537 || $conf->exists('business-onlinepayment-email-override') );
4539 $content{payfirst} = $self->getfield('first');
4540 $content{paylast} = $self->getfield('last');
4542 $content{account_name} = "$content{payfirst} $content{paylast}"
4543 if $options->{method} eq 'ECHECK';
4545 $content{name} = $options->{payname};
4546 $content{name} = $content{account_name} if exists($content{account_name});
4548 $content{city} = exists($options->{city})
4551 $content{state} = exists($options->{state})
4554 $content{zip} = exists($options->{zip})
4557 $content{country} = exists($options->{country})
4558 ? $options->{country}
4560 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
4561 $content{phone} = $self->daytime || $self->night;
4566 my %bop_method2payby = (
4572 sub _new_realtime_bop {
4576 if (ref($_[0]) eq 'HASH') {
4577 %options = %{$_[0]};
4579 my ( $method, $amount ) = ( shift, shift );
4581 $options{method} = $method;
4582 $options{amount} = $amount;
4586 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
4587 warn " $_ => $options{$_}\n" foreach keys %options;
4590 return $self->fake_bop(%options) if $options{'fake'};
4592 $self->_bop_defaults(\%options);
4595 # set trans_is_recur based on invnum if there is one
4598 my $trans_is_recur = 0;
4599 if ( $options{'invnum'} ) {
4601 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
4602 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
4605 map { $_->part_pkg }
4607 map { $_->cust_pkg }
4608 $cust_bill->cust_bill_pkg;
4611 if grep { $_->freq ne '0' } @part_pkg;
4619 my $payment_gateway = $self->_payment_gateway( \%options );
4620 my $namespace = $payment_gateway->gateway_namespace;
4622 eval "use $namespace";
4626 # check for banned credit card/ACH
4629 my $ban = qsearchs('banned_pay', {
4630 'payby' => $bop_method2payby{$options{method}},
4631 'payinfo' => md5_base64($options{payinfo}),
4633 return "Banned credit card" if $ban;
4639 my (%bop_content) = $self->_bop_content(\%options);
4641 if ( $options{method} ne 'ECHECK' ) {
4642 $options{payname} =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
4643 or return "Illegal payname $options{payname}";
4644 ($bop_content{payfirst}, $bop_content{paylast}) = ($1, $2);
4647 my @invoicing_list = $self->invoicing_list_emailonly;
4648 if ( $conf->exists('emailinvoiceautoalways')
4649 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
4650 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
4651 push @invoicing_list, $self->all_emails;
4654 my $email = ($conf->exists('business-onlinepayment-email-override'))
4655 ? $conf->config('business-onlinepayment-email-override')
4656 : $invoicing_list[0];
4660 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
4662 $content{card_number} = $options{payinfo};
4663 $paydate = exists($options{'paydate'})
4664 ? $options{'paydate'}
4666 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
4667 $content{expiration} = "$2/$1";
4669 my $paycvv = exists($options{'paycvv'})
4670 ? $options{'paycvv'}
4672 $content{cvv2} = $paycvv
4675 my $paystart_month = exists($options{'paystart_month'})
4676 ? $options{'paystart_month'}
4677 : $self->paystart_month;
4679 my $paystart_year = exists($options{'paystart_year'})
4680 ? $options{'paystart_year'}
4681 : $self->paystart_year;
4683 $content{card_start} = "$paystart_month/$paystart_year"
4684 if $paystart_month && $paystart_year;
4686 my $payissue = exists($options{'payissue'})
4687 ? $options{'payissue'}
4689 $content{issue_number} = $payissue if $payissue;
4691 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
4692 'trans_is_recur' => $trans_is_recur,
4696 $content{recurring_billing} = 'YES';
4697 $content{acct_code} = 'rebill'
4698 if $conf->exists('credit_card-recurring_billing_acct_code');
4701 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
4702 ( $content{account_number}, $content{routing_code} ) =
4703 split('@', $options{payinfo});
4704 $content{bank_name} = $options{payname};
4705 $content{bank_state} = exists($options{'paystate'})
4706 ? $options{'paystate'}
4707 : $self->getfield('paystate');
4708 $content{account_type} = exists($options{'paytype'})
4709 ? uc($options{'paytype'}) || 'CHECKING'
4710 : uc($self->getfield('paytype')) || 'CHECKING';
4711 $content{customer_org} = $self->company ? 'B' : 'I';
4712 $content{state_id} = exists($options{'stateid'})
4713 ? $options{'stateid'}
4714 : $self->getfield('stateid');
4715 $content{state_id_state} = exists($options{'stateid_state'})
4716 ? $options{'stateid_state'}
4717 : $self->getfield('stateid_state');
4718 $content{customer_ssn} = exists($options{'ss'})
4721 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
4722 $content{phone} = $options{payinfo};
4723 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
4730 # run transaction(s)
4733 my $balance = exists( $options{'balance'} )
4734 ? $options{'balance'}
4737 $self->select_for_update; #mutex ... just until we get our pending record in
4739 #the checks here are intended to catch concurrent payments
4740 #double-form-submission prevention is taken care of in cust_pay_pending::check
4743 return "The customer's balance has changed; $options{method} transaction aborted."
4744 if $self->balance < $balance;
4745 #&& $self->balance < $options{amount}; #might as well anyway?
4747 #also check and make sure there aren't *other* pending payments for this cust
4749 my @pending = qsearch('cust_pay_pending', {
4750 'custnum' => $self->custnum,
4751 'status' => { op=>'!=', value=>'done' }
4753 return "A payment is already being processed for this customer (".
4754 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
4755 "); $options{method} transaction aborted."
4756 if scalar(@pending);
4758 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
4760 my $cust_pay_pending = new FS::cust_pay_pending {
4761 'custnum' => $self->custnum,
4762 #'invnum' => $options{'invnum'},
4763 'paid' => $options{amount},
4765 'payby' => $bop_method2payby{$options{method}},
4766 'payinfo' => $options{payinfo},
4767 'paydate' => $paydate,
4768 'recurring_billing' => $content{recurring_billing},
4770 'gatewaynum' => $payment_gateway->gatewaynum || '',
4771 'session_id' => $options{session_id} || '',
4772 'jobnum' => $options{depend_jobnum} || '',
4774 $cust_pay_pending->payunique( $options{payunique} )
4775 if defined($options{payunique}) && length($options{payunique});
4776 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
4777 return $cpp_new_err if $cpp_new_err;
4779 my( $action1, $action2 ) =
4780 split( /\s*\,\s*/, $payment_gateway->gateway_action );
4782 my $transaction = new $namespace( $payment_gateway->gateway_module,
4783 $self->_bop_options(\%options),
4786 $transaction->content(
4787 'type' => $options{method},
4788 $self->_bop_auth(\%options),
4789 'action' => $action1,
4790 'description' => $options{'description'},
4791 'amount' => $options{amount},
4792 #'invoice_number' => $options{'invnum'},
4793 'customer_id' => $self->custnum,
4795 'reference' => $cust_pay_pending->paypendingnum, #for now
4800 $cust_pay_pending->status('pending');
4801 my $cpp_pending_err = $cust_pay_pending->replace;
4802 return $cpp_pending_err if $cpp_pending_err;
4805 my $BOP_TESTING = 0;
4806 my $BOP_TESTING_SUCCESS = 1;
4808 unless ( $BOP_TESTING ) {
4809 $transaction->submit();
4811 if ( $BOP_TESTING_SUCCESS ) {
4812 $transaction->is_success(1);
4813 $transaction->authorization('fake auth');
4815 $transaction->is_success(0);
4816 $transaction->error_message('fake failure');
4820 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
4822 return { reference => $cust_pay_pending->paypendingnum,
4823 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
4825 } elsif ( $transaction->is_success() && $action2 ) {
4827 $cust_pay_pending->status('authorized');
4828 my $cpp_authorized_err = $cust_pay_pending->replace;
4829 return $cpp_authorized_err if $cpp_authorized_err;
4831 my $auth = $transaction->authorization;
4832 my $ordernum = $transaction->can('order_number')
4833 ? $transaction->order_number
4837 new Business::OnlinePayment( $payment_gateway->gateway_module,
4838 $self->_bop_options(\%options),
4843 type => $options{method},
4845 $self->_bop_auth(\%options),
4846 order_number => $ordernum,
4847 amount => $options{amount},
4848 authorization => $auth,
4849 description => $options{'description'},
4852 foreach my $field (qw( authorization_source_code returned_ACI
4853 transaction_identifier validation_code
4854 transaction_sequence_num local_transaction_date
4855 local_transaction_time AVS_result_code )) {
4856 $capture{$field} = $transaction->$field() if $transaction->can($field);
4859 $capture->content( %capture );
4863 unless ( $capture->is_success ) {
4864 my $e = "Authorization successful but capture failed, custnum #".
4865 $self->custnum. ': '. $capture->result_code.
4866 ": ". $capture->error_message;
4874 # remove paycvv after initial transaction
4877 #false laziness w/misc/process/payment.cgi - check both to make sure working
4879 if ( defined $self->dbdef_table->column('paycvv')
4880 && length($self->paycvv)
4881 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
4883 my $error = $self->remove_cvv;
4885 warn "WARNING: error removing cvv: $error\n";
4893 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
4905 if (ref($_[0]) eq 'HASH') {
4906 %options = %{$_[0]};
4908 my ( $method, $amount ) = ( shift, shift );
4910 $options{method} = $method;
4911 $options{amount} = $amount;
4914 if ( $options{'fake_failure'} ) {
4915 return "Error: No error; test failure requested with fake_failure";
4919 #if ( $payment_gateway->gatewaynum ) { # agent override
4920 # $paybatch = $payment_gateway->gatewaynum. '-';
4923 #$paybatch .= "$processor:". $transaction->authorization;
4925 #$paybatch .= ':'. $transaction->order_number
4926 # if $transaction->can('order_number')
4927 # && length($transaction->order_number);
4929 my $paybatch = 'FakeProcessor:54:32';
4931 my $cust_pay = new FS::cust_pay ( {
4932 'custnum' => $self->custnum,
4933 'invnum' => $options{'invnum'},
4934 'paid' => $options{amount},
4936 'payby' => $bop_method2payby{$options{method}},
4937 #'payinfo' => $payinfo,
4938 'payinfo' => '4111111111111111',
4939 'paybatch' => $paybatch,
4940 #'paydate' => $paydate,
4941 'paydate' => '2012-05-01',
4943 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
4945 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
4948 $cust_pay->invnum(''); #try again with no specific invnum
4949 my $error2 = $cust_pay->insert( $options{'manual'} ?
4950 ( 'manual' => 1 ) : ()
4953 # gah, even with transactions.
4954 my $e = 'WARNING: Card/ACH debited but database not updated - '.
4955 "error inserting (fake!) payment: $error2".
4956 " (previously tried insert with invnum #$options{'invnum'}" .
4963 if ( $options{'paynum_ref'} ) {
4964 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
4967 return ''; #no error
4972 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
4974 # Wraps up processing of a realtime credit card, ACH (electronic check) or
4975 # phone bill transaction.
4977 sub _realtime_bop_result {
4978 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
4980 warn "$me _realtime_bop_result: pending transaction ".
4981 $cust_pay_pending->paypendingnum. "\n";
4982 warn " $_ => $options{$_}\n" foreach keys %options;
4985 my $payment_gateway = $options{payment_gateway}
4986 or return "no payment gateway in arguments to _realtime_bop_result";
4988 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
4989 my $cpp_captured_err = $cust_pay_pending->replace;
4990 return $cpp_captured_err if $cpp_captured_err;
4992 if ( $transaction->is_success() ) {
4995 if ( $payment_gateway->gatewaynum ) { # agent override
4996 $paybatch = $payment_gateway->gatewaynum. '-';
4999 $paybatch .= $payment_gateway->gateway_module. ":".
5000 $transaction->authorization;
5002 $paybatch .= ':'. $transaction->order_number
5003 if $transaction->can('order_number')
5004 && length($transaction->order_number);
5006 my $cust_pay = new FS::cust_pay ( {
5007 'custnum' => $self->custnum,
5008 'invnum' => $options{'invnum'},
5009 'paid' => $cust_pay_pending->paid,
5011 'payby' => $cust_pay_pending->payby,
5012 #'payinfo' => $payinfo,
5013 'paybatch' => $paybatch,
5014 'paydate' => $cust_pay_pending->paydate,
5016 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
5017 $cust_pay->payunique( $options{payunique} )
5018 if defined($options{payunique}) && length($options{payunique});
5020 my $oldAutoCommit = $FS::UID::AutoCommit;
5021 local $FS::UID::AutoCommit = 0;
5024 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
5026 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
5029 $cust_pay->invnum(''); #try again with no specific invnum
5030 my $error2 = $cust_pay->insert( $options{'manual'} ?
5031 ( 'manual' => 1 ) : ()
5034 # gah. but at least we have a record of the state we had to abort in
5035 # from cust_pay_pending now.
5036 my $e = "WARNING: $options{method} captured but payment not recorded -".
5037 " error inserting payment (". $payment_gateway->gateway_module.
5039 " (previously tried insert with invnum #$options{'invnum'}" .
5040 ": $error ) - pending payment saved as paypendingnum ".
5041 $cust_pay_pending->paypendingnum. "\n";
5047 my $jobnum = $cust_pay_pending->jobnum;
5049 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
5051 unless ( $placeholder ) {
5052 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5053 my $e = "WARNING: $options{method} captured but job $jobnum not ".
5054 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
5059 $error = $placeholder->delete;
5062 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5063 my $e = "WARNING: $options{method} captured but could not delete ".
5064 "job $jobnum for paypendingnum ".
5065 $cust_pay_pending->paypendingnum. ": $error\n";
5072 if ( $options{'paynum_ref'} ) {
5073 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
5076 $cust_pay_pending->status('done');
5077 $cust_pay_pending->statustext('captured');
5078 $cust_pay_pending->paynum($cust_pay->paynum);
5079 my $cpp_done_err = $cust_pay_pending->replace;
5081 if ( $cpp_done_err ) {
5083 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5084 my $e = "WARNING: $options{method} captured but payment not recorded - ".
5085 "error updating status for paypendingnum ".
5086 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
5092 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5093 return ''; #no error
5099 my $perror = $payment_gateway->gateway_module. " error: ".
5100 $transaction->error_message;
5102 my $jobnum = $cust_pay_pending->jobnum;
5104 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
5106 if ( $placeholder ) {
5107 my $error = $placeholder->depended_delete;
5108 $error ||= $placeholder->delete;
5109 warn "error removing provisioning jobs after declined paypendingnum ".
5110 $cust_pay_pending->paypendingnum. "\n";
5112 my $e = "error finding job $jobnum for declined paypendingnum ".
5113 $cust_pay_pending->paypendingnum. "\n";
5119 unless ( $transaction->error_message ) {
5122 if ( $transaction->can('response_page') ) {
5124 'page' => ( $transaction->can('response_page')
5125 ? $transaction->response_page
5128 'code' => ( $transaction->can('response_code')
5129 ? $transaction->response_code
5132 'headers' => ( $transaction->can('response_headers')
5133 ? $transaction->response_headers
5139 "No additional debugging information available for ".
5140 $payment_gateway->gateway_module;
5143 $perror .= "No error_message returned from ".
5144 $payment_gateway->gateway_module. " -- ".
5145 ( ref($t_response) ? Dumper($t_response) : $t_response );
5149 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
5150 && $conf->exists('emaildecline')
5151 && grep { $_ ne 'POST' } $self->invoicing_list
5152 && ! grep { $transaction->error_message =~ /$_/ }
5153 $conf->config('emaildecline-exclude')
5155 my @templ = $conf->config('declinetemplate');
5156 my $template = new Text::Template (
5158 SOURCE => [ map "$_\n", @templ ],
5159 ) or return "($perror) can't create template: $Text::Template::ERROR";
5160 $template->compile()
5161 or return "($perror) can't compile template: $Text::Template::ERROR";
5163 my $templ_hash = { error => $transaction->error_message };
5165 my $error = send_email(
5166 'from' => $conf->config('invoice_from', $self->agentnum ),
5167 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
5168 'subject' => 'Your payment could not be processed',
5169 'body' => [ $template->fill_in(HASH => $templ_hash) ],
5172 $perror .= " (also received error sending decline notification: $error)"
5177 $cust_pay_pending->status('done');
5178 $cust_pay_pending->statustext("declined: $perror");
5179 my $cpp_done_err = $cust_pay_pending->replace;
5180 if ( $cpp_done_err ) {
5181 my $e = "WARNING: $options{method} declined but pending payment not ".
5182 "resolved - error updating status for paypendingnum ".
5183 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
5185 $perror = "$e ($perror)";
5193 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
5195 Verifies successful third party processing of a realtime credit card,
5196 ACH (electronic check) or phone bill transaction via a
5197 Business::OnlineThirdPartyPayment realtime gateway. See
5198 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
5200 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
5202 The additional options I<payname>, I<city>, I<state>,
5203 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
5204 if set, will override the value from the customer record.
5206 I<description> is a free-text field passed to the gateway. It defaults to
5207 "Internet services".
5209 If an I<invnum> is specified, this payment (if successful) is applied to the
5210 specified invoice. If you don't specify an I<invnum> you might want to
5211 call the B<apply_payments> method.
5213 I<quiet> can be set true to surpress email decline notices.
5215 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
5216 resulting paynum, if any.
5218 I<payunique> is a unique identifier for this payment.
5220 Returns a hashref containing elements bill_error (which will be undefined
5221 upon success) and session_id of any associated session.
5225 sub realtime_botpp_capture {
5226 my( $self, $cust_pay_pending, %options ) = @_;
5228 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
5229 warn " $_ => $options{$_}\n" foreach keys %options;
5232 eval "use Business::OnlineThirdPartyPayment";
5236 # select the gateway
5239 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
5241 my $payment_gateway = $cust_pay_pending->gatewaynum
5242 ? qsearchs( 'payment_gateway',
5243 { gatewaynum => $cust_pay_pending->gatewaynum }
5245 : $self->agent->payment_gateway( 'method' => $method,
5246 # 'invnum' => $cust_pay_pending->invnum,
5247 # 'payinfo' => $cust_pay_pending->payinfo,
5250 $options{payment_gateway} = $payment_gateway; # for the helper subs
5256 my @invoicing_list = $self->invoicing_list_emailonly;
5257 if ( $conf->exists('emailinvoiceautoalways')
5258 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
5259 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
5260 push @invoicing_list, $self->all_emails;
5263 my $email = ($conf->exists('business-onlinepayment-email-override'))
5264 ? $conf->config('business-onlinepayment-email-override')
5265 : $invoicing_list[0];
5269 $content{email_customer} =
5270 ( $conf->exists('business-onlinepayment-email_customer')
5271 || $conf->exists('business-onlinepayment-email-override') );
5274 # run transaction(s)
5278 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
5279 $self->_bop_options(\%options),
5282 $transaction->reference({ %options });
5284 $transaction->content(
5286 $self->_bop_auth(\%options),
5287 'action' => 'Post Authorization',
5288 'description' => $options{'description'},
5289 'amount' => $cust_pay_pending->paid,
5290 #'invoice_number' => $options{'invnum'},
5291 'customer_id' => $self->custnum,
5292 'referer' => 'http://cleanwhisker.420.am/',
5293 'reference' => $cust_pay_pending->paypendingnum,
5295 'phone' => $self->daytime || $self->night,
5297 # plus whatever is required for bogus capture avoidance
5300 $transaction->submit();
5303 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
5306 bill_error => $error,
5307 session_id => $cust_pay_pending->session_id,
5312 =item default_payment_gateway DEPRECATED -- use agent->payment_gateway
5316 sub default_payment_gateway {
5317 my( $self, $method ) = @_;
5319 die "Real-time processing not enabled\n"
5320 unless $conf->exists('business-onlinepayment');
5322 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
5325 my $bop_config = 'business-onlinepayment';
5326 $bop_config .= '-ach'
5327 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
5328 my ( $processor, $login, $password, $action, @bop_options ) =
5329 $conf->config($bop_config);
5330 $action ||= 'normal authorization';
5331 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
5332 die "No real-time processor is enabled - ".
5333 "did you set the business-onlinepayment configuration value?\n"
5336 ( $processor, $login, $password, $action, @bop_options )
5341 Removes the I<paycvv> field from the database directly.
5343 If there is an error, returns the error, otherwise returns false.
5349 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
5350 or return dbh->errstr;
5351 $sth->execute($self->custnum)
5352 or return $sth->errstr;
5357 =item _new_realtime_refund_bop METHOD [ OPTION => VALUE ... ]
5359 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
5360 via a Business::OnlinePayment realtime gateway. See
5361 L<http://420.am/business-onlinepayment> for supported gateways.
5363 Available methods are: I<CC>, I<ECHECK> and I<LEC>
5365 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
5367 Most gateways require a reference to an original payment transaction to refund,
5368 so you probably need to specify a I<paynum>.
5370 I<amount> defaults to the original amount of the payment if not specified.
5372 I<reason> specifies a reason for the refund.
5374 I<paydate> specifies the expiration date for a credit card overriding the
5375 value from the customer record or the payment record. Specified as yyyy-mm-dd
5377 Implementation note: If I<amount> is unspecified or equal to the amount of the
5378 orignal payment, first an attempt is made to "void" the transaction via
5379 the gateway (to cancel a not-yet settled transaction) and then if that fails,
5380 the normal attempt is made to "refund" ("credit") the transaction via the
5381 gateway is attempted.
5383 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
5384 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
5385 #if set, will override the value from the customer record.
5387 #If an I<invnum> is specified, this payment (if successful) is applied to the
5388 #specified invoice. If you don't specify an I<invnum> you might want to
5389 #call the B<apply_payments> method.
5393 #some false laziness w/realtime_bop, not enough to make it worth merging
5394 #but some useful small subs should be pulled out
5395 sub _new_realtime_refund_bop {
5399 if (ref($_[0]) ne 'HASH') {
5400 %options = %{$_[0]};
5404 $options{method} = $method;
5408 warn "$me realtime_refund_bop (new): $options{method} refund\n";
5409 warn " $_ => $options{$_}\n" foreach keys %options;
5413 # look up the original payment and optionally a gateway for that payment
5417 my $amount = $options{'amount'};
5419 my( $processor, $login, $password, @bop_options, $namespace ) ;
5420 my( $auth, $order_number ) = ( '', '', '' );
5422 if ( $options{'paynum'} ) {
5424 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
5425 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
5426 or return "Unknown paynum $options{'paynum'}";
5427 $amount ||= $cust_pay->paid;
5429 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
5430 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
5431 $cust_pay->paybatch;
5432 my $gatewaynum = '';
5433 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
5435 if ( $gatewaynum ) { #gateway for the payment to be refunded
5437 my $payment_gateway =
5438 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
5439 die "payment gateway $gatewaynum not found"
5440 unless $payment_gateway;
5442 $processor = $payment_gateway->gateway_module;
5443 $login = $payment_gateway->gateway_username;
5444 $password = $payment_gateway->gateway_password;
5445 $namespace = $payment_gateway->gateway_namespace;
5446 @bop_options = $payment_gateway->options;
5448 } else { #try the default gateway
5451 my $payment_gateway =
5452 $self->agent->payment_gateway('method' => $options{method});
5454 ( $conf_processor, $login, $password, $namespace ) =
5455 map { my $method = "gateway_$_"; $payment_gateway->$method }
5456 qw( module username password namespace );
5458 @bop_options = $payment_gateway->gatewaynum
5459 ? $payment_gateway->options
5460 : @{ $payment_gateway->get('options') };
5462 return "processor of payment $options{'paynum'} $processor does not".
5463 " match default processor $conf_processor"
5464 unless $processor eq $conf_processor;
5469 } else { # didn't specify a paynum, so look for agent gateway overrides
5470 # like a normal transaction
5472 my $payment_gateway =
5473 $self->agent->payment_gateway( 'method' => $options{method},
5474 #'payinfo' => $payinfo,
5476 my( $processor, $login, $password, $namespace ) =
5477 map { my $method = "gateway_$_"; $payment_gateway->$method }
5478 qw( module username password namespace );
5480 my @bop_options = $payment_gateway->gatewaynum
5481 ? $payment_gateway->options
5482 : @{ $payment_gateway->get('options') };
5485 return "neither amount nor paynum specified" unless $amount;
5487 eval "use $namespace";
5491 'type' => $options{method},
5493 'password' => $password,
5494 'order_number' => $order_number,
5495 'amount' => $amount,
5496 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
5498 $content{authorization} = $auth
5499 if length($auth); #echeck/ACH transactions have an order # but no auth
5500 #(at least with authorize.net)
5502 my $disable_void_after;
5503 if ($conf->exists('disable_void_after')
5504 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
5505 $disable_void_after = $1;
5508 #first try void if applicable
5509 if ( $cust_pay && $cust_pay->paid == $amount
5511 ( not defined($disable_void_after) )
5512 || ( time < ($cust_pay->_date + $disable_void_after ) )
5515 warn " attempting void\n" if $DEBUG > 1;
5516 my $void = new Business::OnlinePayment( $processor, @bop_options );
5517 $void->content( 'action' => 'void', %content );
5519 if ( $void->is_success ) {
5520 my $error = $cust_pay->void($options{'reason'});
5522 # gah, even with transactions.
5523 my $e = 'WARNING: Card/ACH voided but database not updated - '.
5524 "error voiding payment: $error";
5528 warn " void successful\n" if $DEBUG > 1;
5533 warn " void unsuccessful, trying refund\n"
5537 my $address = $self->address1;
5538 $address .= ", ". $self->address2 if $self->address2;
5540 my($payname, $payfirst, $paylast);
5541 if ( $self->payname && $options{method} ne 'ECHECK' ) {
5542 $payname = $self->payname;
5543 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
5544 or return "Illegal payname $payname";
5545 ($payfirst, $paylast) = ($1, $2);
5547 $payfirst = $self->getfield('first');
5548 $paylast = $self->getfield('last');
5549 $payname = "$payfirst $paylast";
5552 my @invoicing_list = $self->invoicing_list_emailonly;
5553 if ( $conf->exists('emailinvoiceautoalways')
5554 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
5555 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
5556 push @invoicing_list, $self->all_emails;
5559 my $email = ($conf->exists('business-onlinepayment-email-override'))
5560 ? $conf->config('business-onlinepayment-email-override')
5561 : $invoicing_list[0];
5563 my $payip = exists($options{'payip'})
5566 $content{customer_ip} = $payip
5570 if ( $options{method} eq 'CC' ) {
5573 $content{card_number} = $payinfo = $cust_pay->payinfo;
5574 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
5575 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
5576 ($content{expiration} = "$2/$1"); # where available
5578 $content{card_number} = $payinfo = $self->payinfo;
5579 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
5580 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
5581 $content{expiration} = "$2/$1";
5584 } elsif ( $options{method} eq 'ECHECK' ) {
5587 $payinfo = $cust_pay->payinfo;
5589 $payinfo = $self->payinfo;
5591 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
5592 $content{bank_name} = $self->payname;
5593 $content{account_type} = 'CHECKING';
5594 $content{account_name} = $payname;
5595 $content{customer_org} = $self->company ? 'B' : 'I';
5596 $content{customer_ssn} = $self->ss;
5597 } elsif ( $options{method} eq 'LEC' ) {
5598 $content{phone} = $payinfo = $self->payinfo;
5602 my $refund = new Business::OnlinePayment( $processor, @bop_options );
5603 my %sub_content = $refund->content(
5604 'action' => 'credit',
5605 'customer_id' => $self->custnum,
5606 'last_name' => $paylast,
5607 'first_name' => $payfirst,
5609 'address' => $address,
5610 'city' => $self->city,
5611 'state' => $self->state,
5612 'zip' => $self->zip,
5613 'country' => $self->country,
5615 'phone' => $self->daytime || $self->night,
5618 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
5622 return "$processor error: ". $refund->error_message
5623 unless $refund->is_success();
5625 my $paybatch = "$processor:". $refund->authorization;
5626 $paybatch .= ':'. $refund->order_number
5627 if $refund->can('order_number') && $refund->order_number;
5629 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
5630 my @cust_bill_pay = $cust_pay->cust_bill_pay;
5631 last unless @cust_bill_pay;
5632 my $cust_bill_pay = pop @cust_bill_pay;
5633 my $error = $cust_bill_pay->delete;
5637 my $cust_refund = new FS::cust_refund ( {
5638 'custnum' => $self->custnum,
5639 'paynum' => $options{'paynum'},
5640 'refund' => $amount,
5642 'payby' => $bop_method2payby{$options{method}},
5643 'payinfo' => $payinfo,
5644 'paybatch' => $paybatch,
5645 'reason' => $options{'reason'} || 'card or ACH refund',
5647 my $error = $cust_refund->insert;
5649 $cust_refund->paynum(''); #try again with no specific paynum
5650 my $error2 = $cust_refund->insert;
5652 # gah, even with transactions.
5653 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
5654 "error inserting refund ($processor): $error2".
5655 " (previously tried insert with paynum #$options{'paynum'}" .
5666 =item batch_card OPTION => VALUE...
5668 Adds a payment for this invoice to the pending credit card batch (see
5669 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
5670 runs the payment using a realtime gateway.
5675 my ($self, %options) = @_;
5678 if (exists($options{amount})) {
5679 $amount = $options{amount};
5681 $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
5683 return '' unless $amount > 0;
5685 my $invnum = delete $options{invnum};
5686 my $payby = $options{invnum} || $self->payby; #dubious
5688 if ($options{'realtime'}) {
5689 return $self->realtime_bop( FS::payby->payby2bop($self->payby),
5695 my $oldAutoCommit = $FS::UID::AutoCommit;
5696 local $FS::UID::AutoCommit = 0;
5699 #this needs to handle mysql as well as Pg, like svc_acct.pm
5700 #(make it into a common function if folks need to do batching with mysql)
5701 $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
5702 or return "Cannot lock pay_batch: " . $dbh->errstr;
5706 'payby' => FS::payby->payby2payment($payby),
5709 my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
5711 unless ( $pay_batch ) {
5712 $pay_batch = new FS::pay_batch \%pay_batch;
5713 my $error = $pay_batch->insert;
5715 $dbh->rollback if $oldAutoCommit;
5716 die "error creating new batch: $error\n";
5720 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
5721 'batchnum' => $pay_batch->batchnum,
5722 'custnum' => $self->custnum,
5725 foreach (qw( address1 address2 city state zip country payby payinfo paydate
5727 $options{$_} = '' unless exists($options{$_});
5730 my $cust_pay_batch = new FS::cust_pay_batch ( {
5731 'batchnum' => $pay_batch->batchnum,
5732 'invnum' => $invnum || 0, # is there a better value?
5733 # this field should be
5735 # cust_bill_pay_batch now
5736 'custnum' => $self->custnum,
5737 'last' => $self->getfield('last'),
5738 'first' => $self->getfield('first'),
5739 'address1' => $options{address1} || $self->address1,
5740 'address2' => $options{address2} || $self->address2,
5741 'city' => $options{city} || $self->city,
5742 'state' => $options{state} || $self->state,
5743 'zip' => $options{zip} || $self->zip,
5744 'country' => $options{country} || $self->country,
5745 'payby' => $options{payby} || $self->payby,
5746 'payinfo' => $options{payinfo} || $self->payinfo,
5747 'exp' => $options{paydate} || $self->paydate,
5748 'payname' => $options{payname} || $self->payname,
5749 'amount' => $amount, # consolidating
5752 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
5753 if $old_cust_pay_batch;
5756 if ($old_cust_pay_batch) {
5757 $error = $cust_pay_batch->replace($old_cust_pay_batch)
5759 $error = $cust_pay_batch->insert;
5763 $dbh->rollback if $oldAutoCommit;
5767 my $unapplied = $self->total_unapplied_credits
5768 + $self->total_unapplied_payments
5769 + $self->in_transit_payments;
5770 foreach my $cust_bill ($self->open_cust_bill) {
5771 #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
5772 my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
5773 'invnum' => $cust_bill->invnum,
5774 'paybatchnum' => $cust_pay_batch->paybatchnum,
5775 'amount' => $cust_bill->owed,
5778 if ($unapplied >= $cust_bill_pay_batch->amount){
5779 $unapplied -= $cust_bill_pay_batch->amount;
5782 $cust_bill_pay_batch->amount(sprintf ( "%.2f",
5783 $cust_bill_pay_batch->amount - $unapplied )); $unapplied = 0;
5785 $error = $cust_bill_pay_batch->insert;
5787 $dbh->rollback if $oldAutoCommit;
5792 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5796 =item apply_payments_and_credits
5798 Applies unapplied payments and credits.
5800 In most cases, this new method should be used in place of sequential
5801 apply_payments and apply_credits methods.
5803 If there is an error, returns the error, otherwise returns false.
5807 sub apply_payments_and_credits {
5810 local $SIG{HUP} = 'IGNORE';
5811 local $SIG{INT} = 'IGNORE';
5812 local $SIG{QUIT} = 'IGNORE';
5813 local $SIG{TERM} = 'IGNORE';
5814 local $SIG{TSTP} = 'IGNORE';
5815 local $SIG{PIPE} = 'IGNORE';
5817 my $oldAutoCommit = $FS::UID::AutoCommit;
5818 local $FS::UID::AutoCommit = 0;
5821 $self->select_for_update; #mutex
5823 foreach my $cust_bill ( $self->open_cust_bill ) {
5824 my $error = $cust_bill->apply_payments_and_credits;
5826 $dbh->rollback if $oldAutoCommit;
5827 return "Error applying: $error";
5831 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5836 =item apply_credits OPTION => VALUE ...
5838 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
5839 to outstanding invoice balances in chronological order (or reverse
5840 chronological order if the I<order> option is set to B<newest>) and returns the
5841 value of any remaining unapplied credits available for refund (see
5842 L<FS::cust_refund>).
5844 Dies if there is an error.
5852 local $SIG{HUP} = 'IGNORE';
5853 local $SIG{INT} = 'IGNORE';
5854 local $SIG{QUIT} = 'IGNORE';
5855 local $SIG{TERM} = 'IGNORE';
5856 local $SIG{TSTP} = 'IGNORE';
5857 local $SIG{PIPE} = 'IGNORE';
5859 my $oldAutoCommit = $FS::UID::AutoCommit;
5860 local $FS::UID::AutoCommit = 0;
5863 $self->select_for_update; #mutex
5865 unless ( $self->total_unapplied_credits ) {
5866 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5870 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
5871 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
5873 my @invoices = $self->open_cust_bill;
5874 @invoices = sort { $b->_date <=> $a->_date } @invoices
5875 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
5878 foreach my $cust_bill ( @invoices ) {
5881 if ( !defined($credit) || $credit->credited == 0) {
5882 $credit = pop @credits or last;
5885 if ($cust_bill->owed >= $credit->credited) {
5886 $amount=$credit->credited;
5888 $amount=$cust_bill->owed;
5891 my $cust_credit_bill = new FS::cust_credit_bill ( {
5892 'crednum' => $credit->crednum,
5893 'invnum' => $cust_bill->invnum,
5894 'amount' => $amount,
5896 my $error = $cust_credit_bill->insert;
5898 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5902 redo if ($cust_bill->owed > 0);
5906 my $total_unapplied_credits = $self->total_unapplied_credits;
5908 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5910 return $total_unapplied_credits;
5913 =item apply_payments
5915 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
5916 to outstanding invoice balances in chronological order.
5918 #and returns the value of any remaining unapplied payments.
5920 Dies if there is an error.
5924 sub apply_payments {
5927 local $SIG{HUP} = 'IGNORE';
5928 local $SIG{INT} = 'IGNORE';
5929 local $SIG{QUIT} = 'IGNORE';
5930 local $SIG{TERM} = 'IGNORE';
5931 local $SIG{TSTP} = 'IGNORE';
5932 local $SIG{PIPE} = 'IGNORE';
5934 my $oldAutoCommit = $FS::UID::AutoCommit;
5935 local $FS::UID::AutoCommit = 0;
5938 $self->select_for_update; #mutex
5942 my @payments = sort { $b->_date <=> $a->_date }
5943 grep { $_->unapplied > 0 }
5946 my @invoices = sort { $a->_date <=> $b->_date}
5947 grep { $_->owed > 0 }
5952 foreach my $cust_bill ( @invoices ) {
5955 if ( !defined($payment) || $payment->unapplied == 0 ) {
5956 $payment = pop @payments or last;
5959 if ( $cust_bill->owed >= $payment->unapplied ) {
5960 $amount = $payment->unapplied;
5962 $amount = $cust_bill->owed;
5965 my $cust_bill_pay = new FS::cust_bill_pay ( {
5966 'paynum' => $payment->paynum,
5967 'invnum' => $cust_bill->invnum,
5968 'amount' => $amount,
5970 my $error = $cust_bill_pay->insert;
5972 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5976 redo if ( $cust_bill->owed > 0);
5980 my $total_unapplied_payments = $self->total_unapplied_payments;
5982 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5984 return $total_unapplied_payments;
5989 Returns the total owed for this customer on all invoices
5990 (see L<FS::cust_bill/owed>).
5996 $self->total_owed_date(2145859200); #12/31/2037
5999 =item total_owed_date TIME
6001 Returns the total owed for this customer on all invoices with date earlier than
6002 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
6003 see L<Time::Local> and L<Date::Parse> for conversion functions.
6007 sub total_owed_date {
6011 foreach my $cust_bill (
6012 grep { $_->_date <= $time }
6013 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
6015 $total_bill += $cust_bill->owed;
6017 sprintf( "%.2f", $total_bill );
6022 Returns the total amount of all payments.
6029 $total += $_->paid foreach $self->cust_pay;
6030 sprintf( "%.2f", $total );
6033 =item total_unapplied_credits
6035 Returns the total outstanding credit (see L<FS::cust_credit>) for this
6036 customer. See L<FS::cust_credit/credited>.
6038 =item total_credited
6040 Old name for total_unapplied_credits. Don't use.
6044 sub total_credited {
6045 #carp "total_credited deprecated, use total_unapplied_credits";
6046 shift->total_unapplied_credits(@_);
6049 sub total_unapplied_credits {
6051 my $total_credit = 0;
6052 $total_credit += $_->credited foreach $self->cust_credit;
6053 sprintf( "%.2f", $total_credit );
6056 =item total_unapplied_payments
6058 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
6059 See L<FS::cust_pay/unapplied>.
6063 sub total_unapplied_payments {
6065 my $total_unapplied = 0;
6066 $total_unapplied += $_->unapplied foreach $self->cust_pay;
6067 sprintf( "%.2f", $total_unapplied );
6070 =item total_unapplied_refunds
6072 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
6073 customer. See L<FS::cust_refund/unapplied>.
6077 sub total_unapplied_refunds {
6079 my $total_unapplied = 0;
6080 $total_unapplied += $_->unapplied foreach $self->cust_refund;
6081 sprintf( "%.2f", $total_unapplied );
6086 Returns the balance for this customer (total_owed plus total_unrefunded, minus
6087 total_unapplied_credits minus total_unapplied_payments).
6095 + $self->total_unapplied_refunds
6096 - $self->total_unapplied_credits
6097 - $self->total_unapplied_payments
6101 =item balance_date TIME
6103 Returns the balance for this customer, only considering invoices with date
6104 earlier than TIME (total_owed_date minus total_credited minus
6105 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
6106 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
6115 $self->total_owed_date($time)
6116 + $self->total_unapplied_refunds
6117 - $self->total_unapplied_credits
6118 - $self->total_unapplied_payments
6122 =item in_transit_payments
6124 Returns the total of requests for payments for this customer pending in
6125 batches in transit to the bank. See L<FS::pay_batch> and L<FS::cust_pay_batch>
6129 sub in_transit_payments {
6131 my $in_transit_payments = 0;
6132 foreach my $pay_batch ( qsearch('pay_batch', {
6135 foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
6136 'batchnum' => $pay_batch->batchnum,
6137 'custnum' => $self->custnum,
6139 $in_transit_payments += $cust_pay_batch->amount;
6142 sprintf( "%.2f", $in_transit_payments );
6147 Returns a hash of useful information for making a payment.
6157 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
6158 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
6159 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
6163 For credit card transactions:
6175 For electronic check transactions:
6190 $return{balance} = $self->balance;
6192 $return{payname} = $self->payname
6193 || ( $self->first. ' '. $self->get('last') );
6195 $return{$_} = $self->get($_) for qw(address1 address2 city state zip);
6197 $return{payby} = $self->payby;
6198 $return{stateid_state} = $self->stateid_state;
6200 if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
6201 $return{card_type} = cardtype($self->payinfo);
6202 $return{payinfo} = $self->paymask;
6204 @return{'month', 'year'} = $self->paydate_monthyear;
6208 if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
6209 my ($payinfo1, $payinfo2) = split '@', $self->paymask;
6210 $return{payinfo1} = $payinfo1;
6211 $return{payinfo2} = $payinfo2;
6212 $return{paytype} = $self->paytype;
6213 $return{paystate} = $self->paystate;
6217 #doubleclick protection
6219 $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
6225 =item paydate_monthyear
6227 Returns a two-element list consisting of the month and year of this customer's
6228 paydate (credit card expiration date for CARD customers)
6232 sub paydate_monthyear {
6234 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
6236 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
6243 =item invoicing_list [ ARRAYREF ]
6245 If an arguement is given, sets these email addresses as invoice recipients
6246 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
6247 (except as warnings), so use check_invoicing_list first.
6249 Returns a list of email addresses (with svcnum entries expanded).
6251 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
6252 check it without disturbing anything by passing nothing.
6254 This interface may change in the future.
6258 sub invoicing_list {
6259 my( $self, $arrayref ) = @_;
6262 my @cust_main_invoice;
6263 if ( $self->custnum ) {
6264 @cust_main_invoice =
6265 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
6267 @cust_main_invoice = ();
6269 foreach my $cust_main_invoice ( @cust_main_invoice ) {
6270 #warn $cust_main_invoice->destnum;
6271 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
6272 #warn $cust_main_invoice->destnum;
6273 my $error = $cust_main_invoice->delete;
6274 warn $error if $error;
6277 if ( $self->custnum ) {
6278 @cust_main_invoice =
6279 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
6281 @cust_main_invoice = ();
6283 my %seen = map { $_->address => 1 } @cust_main_invoice;
6284 foreach my $address ( @{$arrayref} ) {
6285 next if exists $seen{$address} && $seen{$address};
6286 $seen{$address} = 1;
6287 my $cust_main_invoice = new FS::cust_main_invoice ( {
6288 'custnum' => $self->custnum,
6291 my $error = $cust_main_invoice->insert;
6292 warn $error if $error;
6296 if ( $self->custnum ) {
6298 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
6305 =item check_invoicing_list ARRAYREF
6307 Checks these arguements as valid input for the invoicing_list method. If there
6308 is an error, returns the error, otherwise returns false.
6312 sub check_invoicing_list {
6313 my( $self, $arrayref ) = @_;
6315 foreach my $address ( @$arrayref ) {
6317 if ($address eq 'FAX' and $self->getfield('fax') eq '') {
6318 return 'Can\'t add FAX invoice destination with a blank FAX number.';
6321 my $cust_main_invoice = new FS::cust_main_invoice ( {
6322 'custnum' => $self->custnum,
6325 my $error = $self->custnum
6326 ? $cust_main_invoice->check
6327 : $cust_main_invoice->checkdest
6329 return $error if $error;
6333 return "Email address required"
6334 if $conf->exists('cust_main-require_invoicing_list_email')
6335 && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
6340 =item set_default_invoicing_list
6342 Sets the invoicing list to all accounts associated with this customer,
6343 overwriting any previous invoicing list.
6347 sub set_default_invoicing_list {
6349 $self->invoicing_list($self->all_emails);
6354 Returns the email addresses of all accounts provisioned for this customer.
6361 foreach my $cust_pkg ( $self->all_pkgs ) {
6362 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
6364 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
6365 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
6367 $list{$_}=1 foreach map { $_->email } @svc_acct;
6372 =item invoicing_list_addpost
6374 Adds postal invoicing to this customer. If this customer is already configured
6375 to receive postal invoices, does nothing.
6379 sub invoicing_list_addpost {
6381 return if grep { $_ eq 'POST' } $self->invoicing_list;
6382 my @invoicing_list = $self->invoicing_list;
6383 push @invoicing_list, 'POST';
6384 $self->invoicing_list(\@invoicing_list);
6387 =item invoicing_list_emailonly
6389 Returns the list of email invoice recipients (invoicing_list without non-email
6390 destinations such as POST and FAX).
6394 sub invoicing_list_emailonly {
6396 warn "$me invoicing_list_emailonly called"
6398 grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
6401 =item invoicing_list_emailonly_scalar
6403 Returns the list of email invoice recipients (invoicing_list without non-email
6404 destinations such as POST and FAX) as a comma-separated scalar.
6408 sub invoicing_list_emailonly_scalar {
6410 warn "$me invoicing_list_emailonly_scalar called"
6412 join(', ', $self->invoicing_list_emailonly);
6415 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
6417 Returns an array of customers referred by this customer (referral_custnum set
6418 to this custnum). If DEPTH is given, recurses up to the given depth, returning
6419 customers referred by customers referred by this customer and so on, inclusive.
6420 The default behavior is DEPTH 1 (no recursion).
6424 sub referral_cust_main {
6426 my $depth = @_ ? shift : 1;
6427 my $exclude = @_ ? shift : {};
6430 map { $exclude->{$_->custnum}++; $_; }
6431 grep { ! $exclude->{ $_->custnum } }
6432 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
6436 map { $_->referral_cust_main($depth-1, $exclude) }
6443 =item referral_cust_main_ncancelled
6445 Same as referral_cust_main, except only returns customers with uncancelled
6450 sub referral_cust_main_ncancelled {
6452 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
6455 =item referral_cust_pkg [ DEPTH ]
6457 Like referral_cust_main, except returns a flat list of all unsuspended (and
6458 uncancelled) packages for each customer. The number of items in this list may
6459 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
6463 sub referral_cust_pkg {
6465 my $depth = @_ ? shift : 1;
6467 map { $_->unsuspended_pkgs }
6468 grep { $_->unsuspended_pkgs }
6469 $self->referral_cust_main($depth);
6472 =item referring_cust_main
6474 Returns the single cust_main record for the customer who referred this customer
6475 (referral_custnum), or false.
6479 sub referring_cust_main {
6481 return '' unless $self->referral_custnum;
6482 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
6485 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
6487 Applies a credit to this customer. If there is an error, returns the error,
6488 otherwise returns false.
6490 REASON can be a text string, an FS::reason object, or a scalar reference to
6491 a reasonnum. If a text string, it will be automatically inserted as a new
6492 reason, and a 'reason_type' option must be passed to indicate the
6493 FS::reason_type for the new reason.
6495 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
6497 Any other options are passed to FS::cust_credit::insert.
6502 my( $self, $amount, $reason, %options ) = @_;
6504 my $cust_credit = new FS::cust_credit {
6505 'custnum' => $self->custnum,
6506 'amount' => $amount,
6509 if ( ref($reason) ) {
6511 if ( ref($reason) eq 'SCALAR' ) {
6512 $cust_credit->reasonnum( $$reason );
6514 $cust_credit->reasonnum( $reason->reasonnum );
6518 $cust_credit->set('reason', $reason)
6521 $cust_credit->addlinfo( delete $options{'addlinfo'} )
6522 if exists($options{'addlinfo'});
6524 $cust_credit->insert(%options);
6528 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
6530 Creates a one-time charge for this customer. If there is an error, returns
6531 the error, otherwise returns false.
6537 my ( $amount, $quantity, $pkg, $comment, $classnum, $additional );
6538 my ( $setuptax, $taxclass ); #internal taxes
6539 my ( $taxproduct, $override ); #vendor (CCH) taxes
6540 if ( ref( $_[0] ) ) {
6541 $amount = $_[0]->{amount};
6542 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
6543 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
6544 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
6545 : '$'. sprintf("%.2f",$amount);
6546 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
6547 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
6548 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
6549 $additional = $_[0]->{additional};
6550 $taxproduct = $_[0]->{taxproductnum};
6551 $override = { '' => $_[0]->{tax_override} };
6555 $pkg = @_ ? shift : 'One-time charge';
6556 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
6558 $taxclass = @_ ? shift : '';
6562 local $SIG{HUP} = 'IGNORE';
6563 local $SIG{INT} = 'IGNORE';
6564 local $SIG{QUIT} = 'IGNORE';
6565 local $SIG{TERM} = 'IGNORE';
6566 local $SIG{TSTP} = 'IGNORE';
6567 local $SIG{PIPE} = 'IGNORE';
6569 my $oldAutoCommit = $FS::UID::AutoCommit;
6570 local $FS::UID::AutoCommit = 0;
6573 my $part_pkg = new FS::part_pkg ( {
6575 'comment' => $comment,
6579 'classnum' => $classnum ? $classnum : '',
6580 'setuptax' => $setuptax,
6581 'taxclass' => $taxclass,
6582 'taxproductnum' => $taxproduct,
6585 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
6586 ( 0 .. @$additional - 1 )
6588 'additional_count' => scalar(@$additional),
6589 'setup_fee' => $amount,
6592 my $error = $part_pkg->insert( options => \%options,
6593 tax_overrides => $override,
6596 $dbh->rollback if $oldAutoCommit;
6600 my $pkgpart = $part_pkg->pkgpart;
6601 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
6602 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
6603 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
6604 $error = $type_pkgs->insert;
6606 $dbh->rollback if $oldAutoCommit;
6611 my $cust_pkg = new FS::cust_pkg ( {
6612 'custnum' => $self->custnum,
6613 'pkgpart' => $pkgpart,
6614 'quantity' => $quantity,
6617 $error = $cust_pkg->insert;
6619 $dbh->rollback if $oldAutoCommit;
6623 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6628 #=item charge_postal_fee
6630 #Applies a one time charge this customer. If there is an error,
6631 #returns the error, returns the cust_pkg charge object or false
6632 #if there was no charge.
6636 # This should be a customer event. For that to work requires that bill
6637 # also be a customer event.
6639 sub charge_postal_fee {
6642 my $pkgpart = $conf->config('postal_invoice-fee_pkgpart');
6643 return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
6645 my $cust_pkg = new FS::cust_pkg ( {
6646 'custnum' => $self->custnum,
6647 'pkgpart' => $pkgpart,
6651 my $error = $cust_pkg->insert;
6652 $error ? $error : $cust_pkg;
6657 Returns all the invoices (see L<FS::cust_bill>) for this customer.
6663 sort { $a->_date <=> $b->_date }
6664 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
6667 =item open_cust_bill
6669 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
6674 sub open_cust_bill {
6676 grep { $_->owed > 0 } $self->cust_bill;
6681 Returns all the credits (see L<FS::cust_credit>) for this customer.
6687 sort { $a->_date <=> $b->_date }
6688 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
6693 Returns all the payments (see L<FS::cust_pay>) for this customer.
6699 sort { $a->_date <=> $b->_date }
6700 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
6705 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
6711 sort { $a->_date <=> $b->_date }
6712 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
6715 =item cust_pay_batch
6717 Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
6721 sub cust_pay_batch {
6723 sort { $a->_date <=> $b->_date }
6724 qsearch( 'cust_pay_batch', { 'custnum' => $self->custnum } )
6727 =item cust_pay_pending
6729 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
6730 (without status "done").
6734 sub cust_pay_pending {
6736 return $self->num_cust_pay_pending unless wantarray;
6737 sort { $a->_date <=> $b->_date }
6738 qsearch( 'cust_pay_pending', {
6739 'custnum' => $self->custnum,
6740 'status' => { op=>'!=', value=>'done' },
6745 =item num_cust_pay_pending
6747 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
6748 customer (without status "done"). Also called automatically when the
6749 cust_pay_pending method is used in a scalar context.
6753 sub num_cust_pay_pending {
6755 my $sql = " SELECT COUNT(*) FROM cust_pay_pending ".
6756 " WHERE custnum = ? AND status != 'done' ";
6757 my $sth = dbh->prepare($sql) or die dbh->errstr;
6758 $sth->execute($self->custnum) or die $sth->errstr;
6759 $sth->fetchrow_arrayref->[0];
6764 Returns all the refunds (see L<FS::cust_refund>) for this customer.
6770 sort { $a->_date <=> $b->_date }
6771 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
6774 =item display_custnum
6776 Returns the displayed customer number for this customer: agent_custid if
6777 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
6781 sub display_custnum {
6783 if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
6784 return $self->agent_custid;
6786 return $self->custnum;
6792 Returns a name string for this customer, either "Company (Last, First)" or
6799 my $name = $self->contact;
6800 $name = $self->company. " ($name)" if $self->company;
6806 Returns a name string for this (service/shipping) contact, either
6807 "Company (Last, First)" or "Last, First".
6813 if ( $self->get('ship_last') ) {
6814 my $name = $self->ship_contact;
6815 $name = $self->ship_company. " ($name)" if $self->ship_company;
6824 Returns a name string for this customer, either "Company" or "First Last".
6830 $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
6833 =item ship_name_short
6835 Returns a name string for this (service/shipping) contact, either "Company"
6840 sub ship_name_short {
6842 if ( $self->get('ship_last') ) {
6843 $self->ship_company !~ /^\s*$/
6844 ? $self->ship_company
6845 : $self->ship_contact_firstlast;
6847 $self->name_company_or_firstlast;
6853 Returns this customer's full (billing) contact name only, "Last, First"
6859 $self->get('last'). ', '. $self->first;
6864 Returns this customer's full (shipping) contact name only, "Last, First"
6870 $self->get('ship_last')
6871 ? $self->get('ship_last'). ', '. $self->ship_first
6875 =item contact_firstlast
6877 Returns this customers full (billing) contact name only, "First Last".
6881 sub contact_firstlast {
6883 $self->first. ' '. $self->get('last');
6886 =item ship_contact_firstlast
6888 Returns this customer's full (shipping) contact name only, "First Last".
6892 sub ship_contact_firstlast {
6894 $self->get('ship_last')
6895 ? $self->first. ' '. $self->get('ship_last')
6896 : $self->contact_firstlast;
6901 Returns this customer's full country name
6907 code2country($self->country);
6910 =item geocode DATA_VENDOR
6912 Returns a value for the customer location as encoded by DATA_VENDOR.
6913 Currently this only makes sense for "CCH" as DATA_VENDOR.
6918 my ($self, $data_vendor) = (shift, shift); #always cch for now
6920 my $geocode = $self->get('geocode'); #XXX only one data_vendor for geocode
6921 return $geocode if $geocode;
6923 my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) )
6927 my ($zip,$plus4) = split /-/, $self->get("${prefix}zip")
6928 if $self->country eq 'US';
6930 #CCH specific location stuff
6931 my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'";
6933 my @cust_tax_location =
6935 'table' => 'cust_tax_location',
6936 'hashref' => { 'zip' => $zip, 'data_vendor' => $data_vendor },
6937 'extra_sql' => $extra_sql,
6938 'order_by' => 'ORDER BY plus4hi',#overlapping with distinct ends
6941 $geocode = $cust_tax_location[0]->geocode
6942 if scalar(@cust_tax_location);
6951 Returns a status string for this customer, currently:
6955 =item prospect - No packages have ever been ordered
6957 =item active - One or more recurring packages is active
6959 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
6961 =item suspended - All non-cancelled recurring packages are suspended
6963 =item cancelled - All recurring packages are cancelled
6969 sub status { shift->cust_status(@_); }
6973 for my $status (qw( prospect active inactive suspended cancelled )) {
6974 my $method = $status.'_sql';
6975 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
6976 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
6977 $sth->execute( ($self->custnum) x $numnum )
6978 or die "Error executing 'SELECT $sql': ". $sth->errstr;
6979 return $status if $sth->fetchrow_arrayref->[0];
6983 =item ucfirst_cust_status
6985 =item ucfirst_status
6987 Returns the status with the first character capitalized.
6991 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
6993 sub ucfirst_cust_status {
6995 ucfirst($self->cust_status);
7000 Returns a hex triplet color string for this customer's status.
7004 use vars qw(%statuscolor);
7005 tie %statuscolor, 'Tie::IxHash',
7006 'prospect' => '7e0079', #'000000', #black? naw, purple
7007 'active' => '00CC00', #green
7008 'inactive' => '0000CC', #blue
7009 'suspended' => 'FF9900', #yellow
7010 'cancelled' => 'FF0000', #red
7013 sub statuscolor { shift->cust_statuscolor(@_); }
7015 sub cust_statuscolor {
7017 $statuscolor{$self->cust_status};
7022 Returns an array of hashes representing the customer's RT tickets.
7029 my $num = $conf->config('cust_main-max_tickets') || 10;
7032 if ( $conf->config('ticket_system') ) {
7033 unless ( $conf->config('ticket_system-custom_priority_field') ) {
7035 @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
7039 foreach my $priority (
7040 $conf->config('ticket_system-custom_priority_field-values'), ''
7042 last if scalar(@tickets) >= $num;
7044 @{ FS::TicketSystem->customer_tickets( $self->custnum,
7045 $num - scalar(@tickets),
7055 # Return services representing svc_accts in customer support packages
7056 sub support_services {
7058 my %packages = map { $_ => 1 } $conf->config('support_packages');
7060 grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
7061 grep { $_->part_svc->svcdb eq 'svc_acct' }
7062 map { $_->cust_svc }
7063 grep { exists $packages{ $_->pkgpart } }
7064 $self->ncancelled_pkgs;
7070 =head1 CLASS METHODS
7076 Class method that returns the list of possible status strings for customers
7077 (see L<the status method|/status>). For example:
7079 @statuses = FS::cust_main->statuses();
7084 #my $self = shift; #could be class...
7090 Returns an SQL expression identifying prospective cust_main records (customers
7091 with no packages ever ordered)
7095 use vars qw($select_count_pkgs);
7096 $select_count_pkgs =
7097 "SELECT COUNT(*) FROM cust_pkg
7098 WHERE cust_pkg.custnum = cust_main.custnum";
7100 sub select_count_pkgs_sql {
7104 sub prospect_sql { "
7105 0 = ( $select_count_pkgs )
7110 Returns an SQL expression identifying active cust_main records (customers with
7111 active recurring packages).
7116 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
7122 Returns an SQL expression identifying inactive cust_main records (customers with
7123 no active recurring packages, but otherwise unsuspended/uncancelled).
7127 sub inactive_sql { "
7128 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
7130 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
7136 Returns an SQL expression identifying suspended cust_main records.
7141 sub suspended_sql { susp_sql(@_); }
7143 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
7145 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
7151 Returns an SQL expression identifying cancelled cust_main records.
7155 sub cancelled_sql { cancel_sql(@_); }
7158 my $recurring_sql = FS::cust_pkg->recurring_sql;
7159 my $cancelled_sql = FS::cust_pkg->cancelled_sql;
7162 0 < ( $select_count_pkgs )
7163 AND 0 < ( $select_count_pkgs AND $recurring_sql AND $cancelled_sql )
7164 AND 0 = ( $select_count_pkgs AND $recurring_sql
7165 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
7167 AND 0 = ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
7173 =item uncancelled_sql
7175 Returns an SQL expression identifying un-cancelled cust_main records.
7179 sub uncancelled_sql { uncancel_sql(@_); }
7180 sub uncancel_sql { "
7181 ( 0 < ( $select_count_pkgs
7182 AND ( cust_pkg.cancel IS NULL
7183 OR cust_pkg.cancel = 0
7186 OR 0 = ( $select_count_pkgs )
7192 Returns an SQL fragment to retreive the balance.
7197 ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
7198 WHERE cust_bill.custnum = cust_main.custnum )
7199 - ( SELECT COALESCE( SUM(paid), 0 ) FROM cust_pay
7200 WHERE cust_pay.custnum = cust_main.custnum )
7201 - ( SELECT COALESCE( SUM(amount), 0 ) FROM cust_credit
7202 WHERE cust_credit.custnum = cust_main.custnum )
7203 + ( SELECT COALESCE( SUM(refund), 0 ) FROM cust_refund
7204 WHERE cust_refund.custnum = cust_main.custnum )
7207 =item balance_date_sql START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
7209 Returns an SQL fragment to retreive the balance for this customer, only
7210 considering invoices with date earlier than START_TIME, and optionally not
7211 later than END_TIME (total_owed_date minus total_unapplied_credits minus
7212 total_unapplied_payments).
7214 Times are specified as SQL fragments or numeric
7215 UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
7216 L<Date::Parse> for conversion functions. The empty string can be passed
7217 to disable that time constraint completely.
7219 Available options are:
7223 =item unapplied_date
7225 set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering)
7230 set to true to remove all customer comparison clauses, for totals
7235 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
7240 JOIN clause (typically used with the total option)
7246 sub balance_date_sql {
7247 my( $class, $start, $end, %opt ) = @_;
7249 my $owed = FS::cust_bill->owed_sql;
7250 my $unapp_refund = FS::cust_refund->unapplied_sql;
7251 my $unapp_credit = FS::cust_credit->unapplied_sql;
7252 my $unapp_pay = FS::cust_pay->unapplied_sql;
7254 my $j = $opt{'join'} || '';
7256 my $owed_wh = $class->_money_table_where( 'cust_bill', $start,$end,%opt );
7257 my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
7258 my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
7259 my $pay_wh = $class->_money_table_where( 'cust_pay', $start,$end,%opt );
7261 " ( SELECT COALESCE(SUM($owed), 0) FROM cust_bill $j $owed_wh )
7262 + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
7263 - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
7264 - ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $j $pay_wh )
7269 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
7271 Helper method for balance_date_sql; name (and usage) subject to change
7272 (suggestions welcome).
7274 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
7275 cust_refund, cust_credit or cust_pay).
7277 If TABLE is "cust_bill" or the unapplied_date option is true, only
7278 considers records with date earlier than START_TIME, and optionally not
7279 later than END_TIME .
7283 sub _money_table_where {
7284 my( $class, $table, $start, $end, %opt ) = @_;
7287 push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
7288 if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
7289 push @where, "$table._date <= $start" if defined($start) && length($start);
7290 push @where, "$table._date > $end" if defined($end) && length($end);
7292 push @where, @{$opt{'where'}} if $opt{'where'};
7293 my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
7299 =item search_sql HASHREF
7303 Returns a qsearch hash expression to search for parameters specified in HREF.
7304 Valid parameters are
7312 =item cancelled_pkgs
7318 listref of start date, end date
7324 =item current_balance
7326 listref (list returned by FS::UI::Web::parse_lt_gt($cgi, 'current_balance'))
7330 =item flattened_pkgs
7339 my ($class, $params) = @_;
7350 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
7352 "cust_main.agentnum = $1";
7359 #prospect active inactive suspended cancelled
7360 if ( grep { $params->{'status'} eq $_ } FS::cust_main->statuses() ) {
7361 my $method = $params->{'status'}. '_sql';
7362 #push @where, $class->$method();
7363 push @where, FS::cust_main->$method();
7367 # parse cancelled package checkbox
7372 $pkgwhere .= "AND (cancel = 0 or cancel is null)"
7373 unless $params->{'cancelled_pkgs'};
7379 foreach my $field (qw( signupdate )) {
7381 next unless exists($params->{$field});
7383 my($beginning, $ending) = @{$params->{$field}};
7386 "cust_main.$field IS NOT NULL",
7387 "cust_main.$field >= $beginning",
7388 "cust_main.$field <= $ending";
7390 $orderby ||= "ORDER BY cust_main.$field";
7398 my @payby = grep /^([A-Z]{4})$/, @{ $params->{'payby'} };
7400 push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )';
7407 #my $balance_sql = $class->balance_sql();
7408 my $balance_sql = FS::cust_main->balance_sql();
7410 push @where, map { s/current_balance/$balance_sql/; $_ }
7411 @{ $params->{'current_balance'} };
7417 if ( $params->{'custbatch'} =~ /^([\w\/\-\:\.]+)$/ and $1 ) {
7419 "cust_main.custbatch = '$1'";
7423 # setup queries, subs, etc. for the search
7426 $orderby ||= 'ORDER BY custnum';
7428 # here is the agent virtualization
7429 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
7431 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
7433 my $addl_from = 'LEFT JOIN cust_pkg USING ( custnum ) ';
7435 my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql";
7437 my $select = join(', ',
7438 'cust_main.custnum',
7439 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
7442 my(@extra_headers) = ();
7443 my(@extra_fields) = ();
7445 if ($params->{'flattened_pkgs'}) {
7447 if ($dbh->{Driver}->{Name} eq 'Pg') {
7449 $select .= ", array_to_string(array(select pkg from cust_pkg left join part_pkg using ( pkgpart ) where cust_main.custnum = cust_pkg.custnum $pkgwhere),'|') as magic";
7451 }elsif ($dbh->{Driver}->{Name} =~ /^mysql/i) {
7452 $select .= ", GROUP_CONCAT(pkg SEPARATOR '|') as magic";
7453 $addl_from .= " LEFT JOIN part_pkg using ( pkgpart )";
7455 warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
7456 "omitting packing information from report.";
7459 my $header_query = "SELECT COUNT(cust_pkg.custnum = cust_main.custnum) AS count FROM cust_main $addl_from $extra_sql $pkgwhere group by cust_main.custnum order by count desc limit 1";
7461 my $sth = dbh->prepare($header_query) or die dbh->errstr;
7462 $sth->execute() or die $sth->errstr;
7463 my $headerrow = $sth->fetchrow_arrayref;
7464 my $headercount = $headerrow ? $headerrow->[0] : 0;
7465 while($headercount) {
7466 unshift @extra_headers, "Package ". $headercount;
7467 unshift @extra_fields, eval q!sub {my $c = shift;
7468 my @a = split '\|', $c->magic;
7469 my $p = $a[!.--$headercount. q!];
7477 'table' => 'cust_main',
7478 'select' => $select,
7480 'extra_sql' => $extra_sql,
7481 'order_by' => $orderby,
7482 'count_query' => $count_query,
7483 'extra_headers' => \@extra_headers,
7484 'extra_fields' => \@extra_fields,
7489 =item email_search_sql HASHREF
7493 Emails a notice to the specified customers.
7495 Valid parameters are those of the L<search_sql> method, plus the following:
7517 Optional job queue job for status updates.
7521 Returns an error message, or false for success.
7523 If an error occurs during any email, stops the enture send and returns that
7524 error. Presumably if you're getting SMTP errors aborting is better than
7525 retrying everything.
7529 sub email_search_sql {
7530 my($class, $params) = @_;
7532 my $from = delete $params->{from};
7533 my $subject = delete $params->{subject};
7534 my $html_body = delete $params->{html_body};
7535 my $text_body = delete $params->{text_body};
7537 my $job = delete $params->{'job'};
7539 my $sql_query = $class->search_sql($params);
7541 my $count_query = delete($sql_query->{'count_query'});
7542 my $count_sth = dbh->prepare($count_query)
7543 or die "Error preparing $count_query: ". dbh->errstr;
7545 or die "Error executing $count_query: ". $count_sth->errstr;
7546 my $count_arrayref = $count_sth->fetchrow_arrayref;
7547 my $num_cust = $count_arrayref->[0];
7549 #my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
7550 #my @extra_fields = @{ delete($sql_query->{'extra_fields'}) };
7553 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
7555 #eventually order+limit magic to reduce memory use?
7556 foreach my $cust_main ( qsearch($sql_query) ) {
7558 my $to = $cust_main->invoicing_list_emailonly_scalar;
7561 my $error = send_email(
7565 'subject' => $subject,
7566 'html_body' => $html_body,
7567 'text_body' => $text_body,
7570 return $error if $error;
7572 if ( $job ) { #progressbar foo
7574 if ( time - $min_sec > $last ) {
7575 my $error = $job->update_statustext(
7576 int( 100 * $num / $num_cust )
7578 die $error if $error;
7588 use Storable qw(thaw);
7591 sub process_email_search_sql {
7593 #warn "$me process_re_X $method for job $job\n" if $DEBUG;
7595 my $param = thaw(decode_base64(shift));
7596 warn Dumper($param) if $DEBUG;
7598 $param->{'job'} = $job;
7600 my $error = FS::cust_main->email_search_sql( $param );
7601 die $error if $error;
7605 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
7607 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
7608 records. Currently, I<first>, I<last> and/or I<company> may be specified (the
7609 appropriate ship_ field is also searched).
7611 Additional options are the same as FS::Record::qsearch
7616 my( $self, $fuzzy, $hash, @opt) = @_;
7621 check_and_rebuild_fuzzyfiles();
7622 foreach my $field ( keys %$fuzzy ) {
7624 my $all = $self->all_X($field);
7625 next unless scalar(@$all);
7628 $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
7631 foreach ( keys %match ) {
7632 push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
7633 push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
7636 push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
7639 # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
7641 @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main;
7649 Returns a masked version of the named field
7654 my ($self,$field) = @_;
7658 'x'x(length($self->getfield($field))-4).
7659 substr($self->getfield($field), (length($self->getfield($field))-4));
7669 =item smart_search OPTION => VALUE ...
7671 Accepts the following options: I<search>, the string to search for. The string
7672 will be searched for as a customer number, phone number, name or company name,
7673 as an exact, or, in some cases, a substring or fuzzy match (see the source code
7674 for the exact heuristics used); I<no_fuzzy_on_exact>, causes smart_search to
7675 skip fuzzy matching when an exact match is found.
7677 Any additional options are treated as an additional qualifier on the search
7680 Returns a (possibly empty) array of FS::cust_main objects.
7687 #here is the agent virtualization
7688 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
7692 my $skip_fuzzy = delete $options{'no_fuzzy_on_exact'};
7693 my $search = delete $options{'search'};
7694 ( my $alphanum_search = $search ) =~ s/\W//g;
7696 if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search
7698 #false laziness w/Record::ut_phone
7699 my $phonen = "$1-$2-$3";
7700 $phonen .= " x$4" if $4;
7702 push @cust_main, qsearch( {
7703 'table' => 'cust_main',
7704 'hashref' => { %options },
7705 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
7707 join(' OR ', map "$_ = '$phonen'",
7708 qw( daytime night fax
7709 ship_daytime ship_night ship_fax )
7712 " AND $agentnums_sql", #agent virtualization
7715 unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
7716 #try looking for matches with extensions unless one was specified
7718 push @cust_main, qsearch( {
7719 'table' => 'cust_main',
7720 'hashref' => { %options },
7721 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
7723 join(' OR ', map "$_ LIKE '$phonen\%'",
7725 ship_daytime ship_night )
7728 " AND $agentnums_sql", #agent virtualization
7733 # custnum search (also try agent_custid), with some tweaking options if your
7734 # legacy cust "numbers" have letters
7737 if ( $search =~ /^\s*(\d+)\s*$/
7738 || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
7739 && $search =~ /^\s*(\w\w?\d+)\s*$/
7746 if ( $num <= 2147483647 ) { #need a bigint custnum? wow.
7747 push @cust_main, qsearch( {
7748 'table' => 'cust_main',
7749 'hashref' => { 'custnum' => $num, %options },
7750 'extra_sql' => " AND $agentnums_sql", #agent virtualization
7754 push @cust_main, qsearch( {
7755 'table' => 'cust_main',
7756 'hashref' => { 'agent_custid' => $num, %options },
7757 'extra_sql' => " AND $agentnums_sql", #agent virtualization
7760 } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
7762 my($company, $last, $first) = ( $1, $2, $3 );
7764 # "Company (Last, First)"
7765 #this is probably something a browser remembered,
7766 #so just do an exact search
7768 foreach my $prefix ( '', 'ship_' ) {
7769 push @cust_main, qsearch( {
7770 'table' => 'cust_main',
7771 'hashref' => { $prefix.'first' => $first,
7772 $prefix.'last' => $last,
7773 $prefix.'company' => $company,
7776 'extra_sql' => " AND $agentnums_sql",
7780 } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
7781 # try (ship_){last,company}
7785 # # remove "(Last, First)" in "Company (Last, First)", otherwise the
7786 # # full strings the browser remembers won't work
7787 # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name
7789 use Lingua::EN::NameParse;
7790 my $NameParse = new Lingua::EN::NameParse(
7792 allow_reversed => 1,
7795 my($last, $first) = ( '', '' );
7796 #maybe disable this too and just rely on NameParse?
7797 if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First
7799 ($last, $first) = ( $1, $2 );
7801 #} elsif ( $value =~ /^(.+)\s+(.+)$/ ) {
7802 } elsif ( ! $NameParse->parse($value) ) {
7804 my %name = $NameParse->components;
7805 $first = $name{'given_name_1'};
7806 $last = $name{'surname_1'};
7810 if ( $first && $last ) {
7812 my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) );
7815 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
7817 ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
7818 OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
7821 push @cust_main, qsearch( {
7822 'table' => 'cust_main',
7823 'hashref' => \%options,
7824 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
7827 # or it just be something that was typed in... (try that in a sec)
7831 my $q_value = dbh->quote($value);
7834 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
7835 $sql .= " ( LOWER(last) = $q_value
7836 OR LOWER(company) = $q_value
7837 OR LOWER(ship_last) = $q_value
7838 OR LOWER(ship_company) = $q_value
7841 push @cust_main, qsearch( {
7842 'table' => 'cust_main',
7843 'hashref' => \%options,
7844 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
7847 #no exact match, trying substring/fuzzy
7848 #always do substring & fuzzy (unless they're explicity config'ed off)
7849 #getting complaints searches are not returning enough
7850 unless ( @cust_main && $skip_fuzzy || $conf->exists('disable-fuzzy') ) {
7852 #still some false laziness w/search_sql (was search/cust_main.cgi)
7857 { 'company' => { op=>'ILIKE', value=>"%$value%" }, },
7858 { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
7861 if ( $first && $last ) {
7864 { 'first' => { op=>'ILIKE', value=>"%$first%" },
7865 'last' => { op=>'ILIKE', value=>"%$last%" },
7867 { 'ship_first' => { op=>'ILIKE', value=>"%$first%" },
7868 'ship_last' => { op=>'ILIKE', value=>"%$last%" },
7875 { 'last' => { op=>'ILIKE', value=>"%$value%" }, },
7876 { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, },
7880 foreach my $hashref ( @hashrefs ) {
7882 push @cust_main, qsearch( {
7883 'table' => 'cust_main',
7884 'hashref' => { %$hashref,
7887 'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton
7896 " AND $agentnums_sql", #extra_sql #agent virtualization
7899 if ( $first && $last ) {
7900 push @cust_main, FS::cust_main->fuzzy_search(
7901 { 'last' => $last, #fuzzy hashref
7902 'first' => $first }, #
7906 foreach my $field ( 'last', 'company' ) {
7908 FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
7913 #eliminate duplicates
7915 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
7925 Accepts the following options: I<email>, the email address to search for. The
7926 email address will be searched for as an email invoice destination and as an
7929 #Any additional options are treated as an additional qualifier on the search
7930 #(i.e. I<agentnum>).
7932 Returns a (possibly empty) array of FS::cust_main objects (but usually just
7942 my $email = delete $options{'email'};
7944 #we're only being used by RT at the moment... no agent virtualization yet
7945 #my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
7949 if ( $email =~ /([^@]+)\@([^@]+)/ ) {
7951 my ( $user, $domain ) = ( $1, $2 );
7953 warn "$me smart_search: searching for $user in domain $domain"
7959 'table' => 'cust_main_invoice',
7960 'hashref' => { 'dest' => $email },
7967 map $_->cust_svc->cust_pkg,
7969 'table' => 'svc_acct',
7970 'hashref' => { 'username' => $user, },
7972 'AND ( SELECT domain FROM svc_domain
7973 WHERE svc_acct.domsvc = svc_domain.svcnum
7974 ) = '. dbh->quote($domain),
7980 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
7982 warn "$me smart_search: found ". scalar(@cust_main). " unique customers"
7989 =item check_and_rebuild_fuzzyfiles
7993 use vars qw(@fuzzyfields);
7994 @fuzzyfields = ( 'last', 'first', 'company' );
7996 sub check_and_rebuild_fuzzyfiles {
7997 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
7998 rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
8001 =item rebuild_fuzzyfiles
8005 sub rebuild_fuzzyfiles {
8007 use Fcntl qw(:flock);
8009 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
8010 mkdir $dir, 0700 unless -d $dir;
8012 foreach my $fuzzy ( @fuzzyfields ) {
8014 open(LOCK,">>$dir/cust_main.$fuzzy")
8015 or die "can't open $dir/cust_main.$fuzzy: $!";
8017 or die "can't lock $dir/cust_main.$fuzzy: $!";
8019 open (CACHE,">$dir/cust_main.$fuzzy.tmp")
8020 or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
8022 foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
8023 my $sth = dbh->prepare("SELECT $field FROM cust_main".
8024 " WHERE $field != '' AND $field IS NOT NULL");
8025 $sth->execute or die $sth->errstr;
8027 while ( my $row = $sth->fetchrow_arrayref ) {
8028 print CACHE $row->[0]. "\n";
8033 close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
8035 rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
8046 my( $self, $field ) = @_;
8047 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
8048 open(CACHE,"<$dir/cust_main.$field")
8049 or die "can't open $dir/cust_main.$field: $!";
8050 my @array = map { chomp; $_; } <CACHE>;
8055 =item append_fuzzyfiles LASTNAME COMPANY
8059 sub append_fuzzyfiles {
8060 #my( $first, $last, $company ) = @_;
8062 &check_and_rebuild_fuzzyfiles;
8064 use Fcntl qw(:flock);
8066 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
8068 foreach my $field (qw( first last company )) {
8073 open(CACHE,">>$dir/cust_main.$field")
8074 or die "can't open $dir/cust_main.$field: $!";
8075 flock(CACHE,LOCK_EX)
8076 or die "can't lock $dir/cust_main.$field: $!";
8078 print CACHE "$value\n";
8080 flock(CACHE,LOCK_UN)
8081 or die "can't unlock $dir/cust_main.$field: $!";
8096 #warn join('-',keys %$param);
8097 my $fh = $param->{filehandle};
8098 my @fields = @{$param->{fields}};
8100 eval "use Text::CSV_XS;";
8103 my $csv = new Text::CSV_XS;
8110 local $SIG{HUP} = 'IGNORE';
8111 local $SIG{INT} = 'IGNORE';
8112 local $SIG{QUIT} = 'IGNORE';
8113 local $SIG{TERM} = 'IGNORE';
8114 local $SIG{TSTP} = 'IGNORE';
8115 local $SIG{PIPE} = 'IGNORE';
8117 my $oldAutoCommit = $FS::UID::AutoCommit;
8118 local $FS::UID::AutoCommit = 0;
8121 #while ( $columns = $csv->getline($fh) ) {
8123 while ( defined($line=<$fh>) ) {
8125 $csv->parse($line) or do {
8126 $dbh->rollback if $oldAutoCommit;
8127 return "can't parse: ". $csv->error_input();
8130 my @columns = $csv->fields();
8131 #warn join('-',@columns);
8134 foreach my $field ( @fields ) {
8135 $row{$field} = shift @columns;
8138 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
8139 unless ( $cust_main ) {
8140 $dbh->rollback if $oldAutoCommit;
8141 return "unknown custnum $row{'custnum'}";
8144 if ( $row{'amount'} > 0 ) {
8145 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
8147 $dbh->rollback if $oldAutoCommit;
8151 } elsif ( $row{'amount'} < 0 ) {
8152 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
8155 $dbh->rollback if $oldAutoCommit;
8165 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
8167 return "Empty file!" unless $imported;
8173 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
8175 Sends a templated email notification to the customer (see L<Text::Template>).
8177 OPTIONS is a hash and may include
8179 I<from> - the email sender (default is invoice_from)
8181 I<to> - comma-separated scalar or arrayref of recipients
8182 (default is invoicing_list)
8184 I<subject> - The subject line of the sent email notification
8185 (default is "Notice from company_name")
8187 I<extra_fields> - a hashref of name/value pairs which will be substituted
8190 The following variables are vavailable in the template.
8192 I<$first> - the customer first name
8193 I<$last> - the customer last name
8194 I<$company> - the customer company
8195 I<$payby> - a description of the method of payment for the customer
8196 # would be nice to use FS::payby::shortname
8197 I<$payinfo> - the account information used to collect for this customer
8198 I<$expdate> - the expiration of the customer payment in seconds from epoch
8203 my ($self, $template, %options) = @_;
8205 return unless $conf->exists($template);
8207 my $from = $conf->config('invoice_from', $self->agentnum)
8208 if $conf->exists('invoice_from', $self->agentnum);
8209 $from = $options{from} if exists($options{from});
8211 my $to = join(',', $self->invoicing_list_emailonly);
8212 $to = $options{to} if exists($options{to});
8214 my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
8215 if $conf->exists('company_name', $self->agentnum);
8216 $subject = $options{subject} if exists($options{subject});
8218 my $notify_template = new Text::Template (TYPE => 'ARRAY',
8219 SOURCE => [ map "$_\n",
8220 $conf->config($template)]
8222 or die "can't create new Text::Template object: Text::Template::ERROR";
8223 $notify_template->compile()
8224 or die "can't compile template: Text::Template::ERROR";
8226 $FS::notify_template::_template::company_name =
8227 $conf->config('company_name', $self->agentnum);
8228 $FS::notify_template::_template::company_address =
8229 join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
8231 my $paydate = $self->paydate || '2037-12-31';
8232 $FS::notify_template::_template::first = $self->first;
8233 $FS::notify_template::_template::last = $self->last;
8234 $FS::notify_template::_template::company = $self->company;
8235 $FS::notify_template::_template::payinfo = $self->mask_payinfo;
8236 my $payby = $self->payby;
8237 my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
8238 my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
8240 #credit cards expire at the end of the month/year of their exp date
8241 if ($payby eq 'CARD' || $payby eq 'DCRD') {
8242 $FS::notify_template::_template::payby = 'credit card';
8243 ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
8244 $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
8246 }elsif ($payby eq 'COMP') {
8247 $FS::notify_template::_template::payby = 'complimentary account';
8249 $FS::notify_template::_template::payby = 'current method';
8251 $FS::notify_template::_template::expdate = $expire_time;
8253 for (keys %{$options{extra_fields}}){
8255 ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
8258 send_email(from => $from,
8260 subject => $subject,
8261 body => $notify_template->fill_in( PACKAGE =>
8262 'FS::notify_template::_template' ),
8267 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
8269 Generates a templated notification to the customer (see L<Text::Template>).
8271 OPTIONS is a hash and may include
8273 I<extra_fields> - a hashref of name/value pairs which will be substituted
8274 into the template. These values may override values mentioned below
8275 and those from the customer record.
8277 The following variables are available in the template instead of or in addition
8278 to the fields of the customer record.
8280 I<$payby> - a description of the method of payment for the customer
8281 # would be nice to use FS::payby::shortname
8282 I<$payinfo> - the masked account information used to collect for this customer
8283 I<$expdate> - the expiration of the customer payment method in seconds from epoch
8284 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
8288 sub generate_letter {
8289 my ($self, $template, %options) = @_;
8291 return unless $conf->exists($template);
8293 my $letter_template = new Text::Template
8295 SOURCE => [ map "$_\n", $conf->config($template)],
8296 DELIMITERS => [ '[@--', '--@]' ],
8298 or die "can't create new Text::Template object: Text::Template::ERROR";
8300 $letter_template->compile()
8301 or die "can't compile template: Text::Template::ERROR";
8303 my %letter_data = map { $_ => $self->$_ } $self->fields;
8304 $letter_data{payinfo} = $self->mask_payinfo;
8306 #my $paydate = $self->paydate || '2037-12-31';
8307 my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
8309 my $payby = $self->payby;
8310 my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
8311 my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
8313 #credit cards expire at the end of the month/year of their exp date
8314 if ($payby eq 'CARD' || $payby eq 'DCRD') {
8315 $letter_data{payby} = 'credit card';
8316 ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
8317 $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
8319 }elsif ($payby eq 'COMP') {
8320 $letter_data{payby} = 'complimentary account';
8322 $letter_data{payby} = 'current method';
8324 $letter_data{expdate} = $expire_time;
8326 for (keys %{$options{extra_fields}}){
8327 $letter_data{$_} = $options{extra_fields}->{$_};
8330 unless(exists($letter_data{returnaddress})){
8331 my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
8332 $self->agent_template)
8334 if ( length($retadd) ) {
8335 $letter_data{returnaddress} = $retadd;
8336 } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
8337 $letter_data{returnaddress} =
8338 join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
8339 $conf->config('company_address', $self->agentnum)
8342 $letter_data{returnaddress} = '~';
8346 $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
8348 $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
8350 my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
8351 my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
8355 ) or die "can't open temp file: $!\n";
8357 $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
8359 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
8363 =item print_ps TEMPLATE
8365 Returns an postscript letter filled in from TEMPLATE, as a scalar.
8371 my $file = $self->generate_letter(@_);
8372 FS::Misc::generate_ps($file);
8375 =item print TEMPLATE
8377 Prints the filled in template.
8379 TEMPLATE is the name of a L<Text::Template> to fill in and print.
8383 sub queueable_print {
8386 my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
8387 or die "invalid customer number: " . $opt{custvnum};
8389 my $error = $self->print( $opt{template} );
8390 die $error if $error;
8394 my ($self, $template) = (shift, shift);
8395 do_print [ $self->print_ps($template) ];
8398 #these three subs should just go away once agent stuff is all config overrides
8400 sub agent_template {
8402 $self->_agent_plandata('agent_templatename');
8405 sub agent_invoice_from {
8407 $self->_agent_plandata('agent_invoice_from');
8410 sub _agent_plandata {
8411 my( $self, $option ) = @_;
8413 #yuck. this whole thing needs to be reconciled better with 1.9's idea of
8414 #agent-specific Conf
8416 use FS::part_event::Condition;
8418 my $agentnum = $self->agentnum;
8421 if ( driver_name =~ /^Pg/i ) {
8423 } elsif ( driver_name =~ /^mysql/i ) {
8426 die "don't know how to use regular expressions in ". driver_name. " databases";
8429 my $part_event_option =
8431 'select' => 'part_event_option.*',
8432 'table' => 'part_event_option',
8434 LEFT JOIN part_event USING ( eventpart )
8435 LEFT JOIN part_event_option AS peo_agentnum
8436 ON ( part_event.eventpart = peo_agentnum.eventpart
8437 AND peo_agentnum.optionname = 'agentnum'
8438 AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
8440 LEFT JOIN part_event_condition
8441 ON ( part_event.eventpart = part_event_condition.eventpart
8442 AND part_event_condition.conditionname = 'cust_bill_age'
8444 LEFT JOIN part_event_condition_option
8445 ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
8446 AND part_event_condition_option.optionname = 'age'
8449 #'hashref' => { 'optionname' => $option },
8450 #'hashref' => { 'part_event_option.optionname' => $option },
8452 " WHERE part_event_option.optionname = ". dbh->quote($option).
8453 " AND action = 'cust_bill_send_agent' ".
8454 " AND ( disabled IS NULL OR disabled != 'Y' ) ".
8455 " AND peo_agentnum.optionname = 'agentnum' ".
8456 " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
8458 CASE WHEN part_event_condition_option.optionname IS NULL
8460 ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
8462 , part_event.weight".
8466 unless ( $part_event_option ) {
8467 return $self->agent->invoice_template || ''
8468 if $option eq 'agent_templatename';
8472 $part_event_option->optionvalue;
8477 ## actual sub, not a method, designed to be called from the queue.
8478 ## sets up the customer, and calls the bill_and_collect
8479 my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
8480 my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
8481 $cust_main->bill_and_collect(
8486 sub _upgrade_data { #class method
8487 my ($class, %opts) = @_;
8489 my $sql = 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL';
8490 my $sth = dbh->prepare($sql) or die dbh->errstr;
8491 $sth->execute or die $sth->errstr;
8501 The delete method should possibly take an FS::cust_main object reference
8502 instead of a scalar customer number.
8504 Bill and collect options should probably be passed as references instead of a
8507 There should probably be a configuration file with a list of allowed credit
8510 No multiple currency support (probably a larger project than just this module).
8512 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
8514 Birthdates rely on negative epoch values.
8516 The payby for card/check batches is broken. With mixed batching, bad
8519 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
8523 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
8524 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
8525 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.