5 use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf
7 $import $ignore_expired_card
8 $skip_fuzzyfiles @fuzzyfields
11 use vars qw( $realtime_bop_decline_quiet ); #ugh
15 use Scalar::Util qw( blessed );
16 use List::Util qw( min );
17 use Time::Local qw(timelocal);
20 use Digest::MD5 qw(md5_base64);
23 use File::Temp qw( tempfile );
24 use String::Approx qw(amatch);
25 use Business::CreditCard 0.28;
27 use FS::UID qw( getotaker dbh driver_name );
28 use FS::Record qw( qsearchs qsearch dbdef );
29 use FS::Misc qw( generate_email send_email generate_ps do_print );
30 use FS::Msgcat qw(gettext);
35 use FS::cust_bill_pkg;
36 use FS::cust_bill_pkg_display;
37 use FS::cust_bill_pkg_tax_location;
38 use FS::cust_bill_pkg_tax_rate_location;
40 use FS::cust_pay_pending;
41 use FS::cust_pay_void;
42 use FS::cust_pay_batch;
45 use FS::part_referral;
46 use FS::cust_main_county;
47 use FS::cust_location;
48 use FS::cust_main_exemption;
49 use FS::cust_tax_adjustment;
51 use FS::tax_rate_location;
52 use FS::cust_tax_location;
53 use FS::part_pkg_taxrate;
55 use FS::cust_main_invoice;
57 use FS::cust_credit_bill;
58 use FS::cust_bill_pay;
59 use FS::prepay_credit;
63 use FS::part_event_condition;
66 use FS::payment_gateway;
67 use FS::agent_payment_gateway;
69 use FS::payinfo_Mixin;
72 @ISA = qw( FS::payinfo_Mixin FS::Record );
74 @EXPORT_OK = qw( smart_search );
76 $realtime_bop_decline_quiet = 0;
78 # 1 is mostly method/subroutine entry and options
79 # 2 traces progress of some operations
80 # 3 is even more information including possibly sensitive data
82 $me = '[FS::cust_main]';
85 $ignore_expired_card = 0;
88 @fuzzyfields = ( 'first', 'last', 'company', 'address1' );
90 @encrypted_fields = ('payinfo', 'paycvv');
91 sub nohistory_fields { ('paycvv'); }
93 @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
95 #ask FS::UID to run this stuff for us later
96 #$FS::UID::callback{'FS::cust_main'} = sub {
97 install_callback FS::UID sub {
99 #yes, need it for stuff below (prolly should be cached)
104 my ( $hashref, $cache ) = @_;
105 if ( exists $hashref->{'pkgnum'} ) {
106 #@{ $self->{'_pkgnum'} } = ();
107 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
108 $self->{'_pkgnum'} = $subcache;
109 #push @{ $self->{'_pkgnum'} },
110 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
116 FS::cust_main - Object methods for cust_main records
122 $record = new FS::cust_main \%hash;
123 $record = new FS::cust_main { 'column' => 'value' };
125 $error = $record->insert;
127 $error = $new_record->replace($old_record);
129 $error = $record->delete;
131 $error = $record->check;
133 @cust_pkg = $record->all_pkgs;
135 @cust_pkg = $record->ncancelled_pkgs;
137 @cust_pkg = $record->suspended_pkgs;
139 $error = $record->bill;
140 $error = $record->bill %options;
141 $error = $record->bill 'time' => $time;
143 $error = $record->collect;
144 $error = $record->collect %options;
145 $error = $record->collect 'invoice_time' => $time,
150 An FS::cust_main object represents a customer. FS::cust_main inherits from
151 FS::Record. The following fields are currently supported:
157 Primary key (assigned automatically for new customers)
161 Agent (see L<FS::agent>)
165 Advertising source (see L<FS::part_referral>)
177 Cocial security number (optional)
193 (optional, see L<FS::cust_main_county>)
197 (see L<FS::cust_main_county>)
203 (see L<FS::cust_main_county>)
239 (optional, see L<FS::cust_main_county>)
243 (see L<FS::cust_main_county>)
249 (see L<FS::cust_main_county>)
265 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
269 Payment Information (See L<FS::payinfo_Mixin> for data format)
273 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
277 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
281 Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
285 Start date month (maestro/solo cards only)
289 Start date year (maestro/solo cards only)
293 Issue number (maestro/solo cards only)
297 Name on card or billing name
301 IP address from which payment information was received
305 Tax exempt, empty or `Y'
309 Order taker (assigned automatically, see L<FS::UID>)
315 =item referral_custnum
317 Referring customer number
321 Enable individual CDR spooling, empty or `Y'
325 A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
329 Discourage individual CDR printing, empty or `Y'
339 Creates a new customer. To add the customer to the database, see L<"insert">.
341 Note that this stores the hash reference, not a distinct copy of the hash it
342 points to. You can ask the object for a copy with the I<hash> method.
346 sub table { 'cust_main'; }
348 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
350 Adds this customer to the database. If there is an error, returns the error,
351 otherwise returns false.
353 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
354 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
355 are inserted atomicly, or the transaction is rolled back. Passing an empty
356 hash reference is equivalent to not supplying this parameter. There should be
357 a better explanation of this, but until then, here's an example:
360 tie %hash, 'Tie::RefHash'; #this part is important
362 $cust_pkg => [ $svc_acct ],
365 $cust_main->insert( \%hash );
367 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
368 be set as the invoicing list (see L<"invoicing_list">). Errors return as
369 expected and rollback the entire transaction; it is not necessary to call
370 check_invoicing_list first. The invoicing_list is set after the records in the
371 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
372 invoicing_list destination to the newly-created svc_acct. Here's an example:
374 $cust_main->insert( {}, [ $email, 'POST' ] );
376 Currently available options are: I<depend_jobnum>, I<noexport> and I<tax_exemption>.
378 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
379 on the supplied jobnum (they will not run until the specific job completes).
380 This can be used to defer provisioning until some action completes (such
381 as running the customer's credit card successfully).
383 The I<noexport> option is deprecated. If I<noexport> is set true, no
384 provisioning jobs (exports) are scheduled. (You can schedule them later with
385 the B<reexport> method.)
387 The I<tax_exemption> option can be set to an arrayref of tax names.
388 FS::cust_main_exemption records will be created and inserted.
394 my $cust_pkgs = @_ ? shift : {};
395 my $invoicing_list = @_ ? shift : '';
397 warn "$me insert called with options ".
398 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
401 local $SIG{HUP} = 'IGNORE';
402 local $SIG{INT} = 'IGNORE';
403 local $SIG{QUIT} = 'IGNORE';
404 local $SIG{TERM} = 'IGNORE';
405 local $SIG{TSTP} = 'IGNORE';
406 local $SIG{PIPE} = 'IGNORE';
408 my $oldAutoCommit = $FS::UID::AutoCommit;
409 local $FS::UID::AutoCommit = 0;
412 my $prepay_identifier = '';
413 my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = (0, 0, 0, 0, 0);
415 if ( $self->payby eq 'PREPAY' ) {
417 $self->payby('BILL');
418 $prepay_identifier = $self->payinfo;
421 warn " looking up prepaid card $prepay_identifier\n"
424 my $error = $self->get_prepay( $prepay_identifier,
425 'amount_ref' => \$amount,
426 'seconds_ref' => \$seconds,
427 'upbytes_ref' => \$upbytes,
428 'downbytes_ref' => \$downbytes,
429 'totalbytes_ref' => \$totalbytes,
432 $dbh->rollback if $oldAutoCommit;
433 #return "error applying prepaid card (transaction rolled back): $error";
437 $payby = 'PREP' if $amount;
439 } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
442 $self->payby('BILL');
443 $amount = $self->paid;
447 warn " inserting $self\n"
450 $self->signupdate(time) unless $self->signupdate;
452 $self->auto_agent_custid()
453 if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
455 my $error = $self->SUPER::insert;
457 $dbh->rollback if $oldAutoCommit;
458 #return "inserting cust_main record (transaction rolled back): $error";
462 warn " setting invoicing list\n"
465 if ( $invoicing_list ) {
466 $error = $self->check_invoicing_list( $invoicing_list );
468 $dbh->rollback if $oldAutoCommit;
469 #return "checking invoicing_list (transaction rolled back): $error";
472 $self->invoicing_list( $invoicing_list );
475 warn " setting customer tags\n"
478 foreach my $tagnum ( @{ $self->tagnum || [] } ) {
479 my $cust_tag = new FS::cust_tag { 'tagnum' => $tagnum,
480 'custnum' => $self->custnum };
481 my $error = $cust_tag->insert;
483 $dbh->rollback if $oldAutoCommit;
488 if ( $invoicing_list ) {
489 $error = $self->check_invoicing_list( $invoicing_list );
491 $dbh->rollback if $oldAutoCommit;
492 #return "checking invoicing_list (transaction rolled back): $error";
495 $self->invoicing_list( $invoicing_list );
499 warn " setting cust_main_exemption\n"
502 my $tax_exemption = delete $options{'tax_exemption'};
503 if ( $tax_exemption ) {
504 foreach my $taxname ( @$tax_exemption ) {
505 my $cust_main_exemption = new FS::cust_main_exemption {
506 'custnum' => $self->custnum,
507 'taxname' => $taxname,
509 my $error = $cust_main_exemption->insert;
511 $dbh->rollback if $oldAutoCommit;
512 return "inserting cust_main_exemption (transaction rolled back): $error";
517 if ( $conf->config('cust_main-skeleton_tables')
518 && $conf->config('cust_main-skeleton_custnum') ) {
520 warn " inserting skeleton records\n"
523 my $error = $self->start_copy_skel;
525 $dbh->rollback if $oldAutoCommit;
531 warn " ordering packages\n"
534 $error = $self->order_pkgs( $cust_pkgs,
536 'seconds_ref' => \$seconds,
537 'upbytes_ref' => \$upbytes,
538 'downbytes_ref' => \$downbytes,
539 'totalbytes_ref' => \$totalbytes,
542 $dbh->rollback if $oldAutoCommit;
547 $dbh->rollback if $oldAutoCommit;
548 return "No svc_acct record to apply pre-paid time";
550 if ( $upbytes || $downbytes || $totalbytes ) {
551 $dbh->rollback if $oldAutoCommit;
552 return "No svc_acct record to apply pre-paid data";
556 warn " inserting initial $payby payment of $amount\n"
558 $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
560 $dbh->rollback if $oldAutoCommit;
561 return "inserting payment (transaction rolled back): $error";
565 unless ( $import || $skip_fuzzyfiles ) {
566 warn " queueing fuzzyfiles update\n"
568 $error = $self->queue_fuzzyfiles_update;
570 $dbh->rollback if $oldAutoCommit;
571 return "updating fuzzy search cache: $error";
575 warn " insert complete; committing transaction\n"
578 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
583 use File::CounterFile;
584 sub auto_agent_custid {
587 my $format = $conf->config('cust_main-auto_agent_custid');
589 if ( $format eq '1YMMXXXXXXXX' ) {
591 my $counter = new File::CounterFile 'cust_main.agent_custid';
594 my $ym = 100000000000 + time2str('%y%m00000000', time);
595 if ( $ym > $counter->value ) {
596 $counter->{'value'} = $agent_custid = $ym;
597 $counter->{'updated'} = 1;
599 $agent_custid = $counter->inc;
605 die "Unknown cust_main-auto_agent_custid format: $format";
608 $self->agent_custid($agent_custid);
612 sub start_copy_skel {
615 #'mg_user_preference' => {},
616 #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, },
617 #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' },
618 #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' },
619 #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } },
620 my @tables = eval(join('\n',$conf->config('cust_main-skeleton_tables')));
623 _copy_skel( 'cust_main', #tablename
624 $conf->config('cust_main-skeleton_custnum'), #sourceid
625 $self->custnum, #destid
626 @tables, #child tables
630 #recursive subroutine, not a method
632 my( $table, $sourceid, $destid, %child_tables ) = @_;
635 if ( $table =~ /^(\w+)\.(\w+)$/ ) {
636 ( $table, $primary_key ) = ( $1, $2 );
638 my $dbdef_table = dbdef->table($table);
639 $primary_key = $dbdef_table->primary_key
640 or return "$table has no primary key".
641 " (or do you need to run dbdef-create?)";
644 warn " _copy_skel: $table.$primary_key $sourceid to $destid for ".
645 join (', ', keys %child_tables). "\n"
648 foreach my $child_table_def ( keys %child_tables ) {
652 if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) {
653 ( $child_table, $child_pkey ) = ( $1, $2 );
655 $child_table = $child_table_def;
657 $child_pkey = dbdef->table($child_table)->primary_key;
658 # or return "$table has no primary key".
659 # " (or do you need to run dbdef-create?)\n";
663 if ( keys %{ $child_tables{$child_table_def} } ) {
665 return "$child_table has no primary key".
666 " (run dbdef-create or try specifying it?)\n"
669 #false laziness w/Record::insert and only works on Pg
670 #refactor the proper last-inserted-id stuff out of Record::insert if this
671 # ever gets use for anything besides a quick kludge for one customer
672 my $default = dbdef->table($child_table)->column($child_pkey)->default;
673 $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
674 or return "can't parse $child_table.$child_pkey default value ".
675 " for sequence name: $default";
680 my @sel_columns = grep { $_ ne $primary_key }
681 dbdef->table($child_table)->columns;
682 my $sel_columns = join(', ', @sel_columns );
684 my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
685 my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) ';
686 my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
688 my $sel_st = "SELECT $sel_columns FROM $child_table".
689 " WHERE $primary_key = $sourceid";
692 my $sel_sth = dbh->prepare( $sel_st )
693 or return dbh->errstr;
695 $sel_sth->execute or return $sel_sth->errstr;
697 while ( my $row = $sel_sth->fetchrow_hashref ) {
699 warn " selected row: ".
700 join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n"
704 "INSERT INTO $child_table $ins_columns VALUES $placeholders";
705 my $ins_sth =dbh->prepare($statement)
706 or return dbh->errstr;
707 my @param = ( $destid, map $row->{$_}, @ins_columns );
708 warn " $statement: [ ". join(', ', @param). " ]\n"
710 $ins_sth->execute( @param )
711 or return $ins_sth->errstr;
713 #next unless keys %{ $child_tables{$child_table} };
714 next unless $sequence;
716 #another section of that laziness
717 my $seq_sql = "SELECT currval('$sequence')";
718 my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
719 $seq_sth->execute or return $seq_sth->errstr;
720 my $insertid = $seq_sth->fetchrow_arrayref->[0];
722 # don't drink soap! recurse! recurse! okay!
724 _copy_skel( $child_table_def,
725 $row->{$child_pkey}, #sourceid
727 %{ $child_tables{$child_table_def} },
729 return $error if $error;
739 =item order_pkg HASHREF | OPTION => VALUE ...
741 Orders a single package.
743 Options may be passed as a list of key/value pairs or as a hash reference.
754 Optional FS::cust_location object
758 Optional arryaref of FS::svc_* service objects.
762 If this option is set to a job queue jobnum (see L<FS::queue>), all provisioning
763 jobs will have a dependancy on the supplied job (they will not run until the
764 specific job completes). This can be used to defer provisioning until some
765 action completes (such as running the customer's credit card successfully).
769 Optional subject for a ticket created and attached to this customer
773 Optional queue name for ticket additions
781 my $opt = ref($_[0]) ? shift : { @_ };
783 warn "$me order_pkg called with options ".
784 join(', ', map { "$_: $opt->{$_}" } keys %$opt ). "\n"
787 my $cust_pkg = $opt->{'cust_pkg'};
788 my $svcs = $opt->{'svcs'} || [];
790 my %svc_options = ();
791 $svc_options{'depend_jobnum'} = $opt->{'depend_jobnum'}
792 if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
794 my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () }
795 qw( ticket_subject ticket_queue );
797 local $SIG{HUP} = 'IGNORE';
798 local $SIG{INT} = 'IGNORE';
799 local $SIG{QUIT} = 'IGNORE';
800 local $SIG{TERM} = 'IGNORE';
801 local $SIG{TSTP} = 'IGNORE';
802 local $SIG{PIPE} = 'IGNORE';
804 my $oldAutoCommit = $FS::UID::AutoCommit;
805 local $FS::UID::AutoCommit = 0;
808 if ( $opt->{'cust_location'} &&
809 ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
810 my $error = $opt->{'cust_location'}->insert;
812 $dbh->rollback if $oldAutoCommit;
813 return "inserting cust_location (transaction rolled back): $error";
815 $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
818 $cust_pkg->custnum( $self->custnum );
820 my $error = $cust_pkg->insert( %insert_params );
822 $dbh->rollback if $oldAutoCommit;
823 return "inserting cust_pkg (transaction rolled back): $error";
826 foreach my $svc_something ( @{ $opt->{'svcs'} } ) {
827 if ( $svc_something->svcnum ) {
828 my $old_cust_svc = $svc_something->cust_svc;
829 my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
830 $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
831 $error = $new_cust_svc->replace($old_cust_svc);
833 $svc_something->pkgnum( $cust_pkg->pkgnum );
834 if ( $svc_something->isa('FS::svc_acct') ) {
835 foreach ( grep { $opt->{$_.'_ref'} && ${ $opt->{$_.'_ref'} } }
836 qw( seconds upbytes downbytes totalbytes ) ) {
837 $svc_something->$_( $svc_something->$_() + ${ $opt->{$_.'_ref'} } );
838 ${ $opt->{$_.'_ref'} } = 0;
841 $error = $svc_something->insert(%svc_options);
844 $dbh->rollback if $oldAutoCommit;
845 return "inserting svc_ (transaction rolled back): $error";
849 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
854 #deprecated #=item order_pkgs HASHREF [ , SECONDSREF ] [ , OPTION => VALUE ... ]
855 =item order_pkgs HASHREF [ , OPTION => VALUE ... ]
857 Like the insert method on an existing record, this method orders multiple
858 packages and included services atomicaly. Pass a Tie::RefHash data structure
859 to this method containing FS::cust_pkg and FS::svc_I<tablename> objects.
860 There should be a better explanation of this, but until then, here's an
864 tie %hash, 'Tie::RefHash'; #this part is important
866 $cust_pkg => [ $svc_acct ],
869 $cust_main->order_pkgs( \%hash, 'noexport'=>1 );
871 Services can be new, in which case they are inserted, or existing unaudited
872 services, in which case they are linked to the newly-created package.
874 Currently available options are: I<depend_jobnum>, I<noexport>, I<seconds_ref>,
875 I<upbytes_ref>, I<downbytes_ref>, and I<totalbytes_ref>.
877 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
878 on the supplied jobnum (they will not run until the specific job completes).
879 This can be used to defer provisioning until some action completes (such
880 as running the customer's credit card successfully).
882 The I<noexport> option is deprecated. If I<noexport> is set true, no
883 provisioning jobs (exports) are scheduled. (You can schedule them later with
884 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
885 on the cust_main object is not recommended, as existing services will also be
888 If I<seconds_ref>, I<upbytes_ref>, I<downbytes_ref>, or I<totalbytes_ref> is
889 provided, the scalars (provided by references) will be incremented by the
890 values of the prepaid card.`
896 my $cust_pkgs = shift;
897 my $seconds_ref = ref($_[0]) ? shift : ''; #deprecated
899 $seconds_ref ||= $options{'seconds_ref'};
901 warn "$me order_pkgs called with options ".
902 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
905 local $SIG{HUP} = 'IGNORE';
906 local $SIG{INT} = 'IGNORE';
907 local $SIG{QUIT} = 'IGNORE';
908 local $SIG{TERM} = 'IGNORE';
909 local $SIG{TSTP} = 'IGNORE';
910 local $SIG{PIPE} = 'IGNORE';
912 my $oldAutoCommit = $FS::UID::AutoCommit;
913 local $FS::UID::AutoCommit = 0;
916 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
918 foreach my $cust_pkg ( keys %$cust_pkgs ) {
920 my $error = $self->order_pkg(
921 'cust_pkg' => $cust_pkg,
922 'svcs' => $cust_pkgs->{$cust_pkg},
923 'seconds_ref' => $seconds_ref,
924 map { $_ => $options{$_} } qw( upbytes_ref downbytes_ref totalbytes_ref
929 $dbh->rollback if $oldAutoCommit;
935 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
939 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
941 Recharges this (existing) customer with the specified prepaid card (see
942 L<FS::prepay_credit>), specified either by I<identifier> or as an
943 FS::prepay_credit object. If there is an error, returns the error, otherwise
946 Optionally, five scalar references can be passed as well. They will have their
947 values filled in with the amount, number of seconds, and number of upload,
948 download, and total bytes applied by this prepaid card.
952 #the ref bullshit here should be refactored like get_prepay. MyAccount.pm is
953 #the only place that uses these args
954 sub recharge_prepay {
955 my( $self, $prepay_credit, $amountref, $secondsref,
956 $upbytesref, $downbytesref, $totalbytesref ) = @_;
958 local $SIG{HUP} = 'IGNORE';
959 local $SIG{INT} = 'IGNORE';
960 local $SIG{QUIT} = 'IGNORE';
961 local $SIG{TERM} = 'IGNORE';
962 local $SIG{TSTP} = 'IGNORE';
963 local $SIG{PIPE} = 'IGNORE';
965 my $oldAutoCommit = $FS::UID::AutoCommit;
966 local $FS::UID::AutoCommit = 0;
969 my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
971 my $error = $self->get_prepay( $prepay_credit,
972 'amount_ref' => \$amount,
973 'seconds_ref' => \$seconds,
974 'upbytes_ref' => \$upbytes,
975 'downbytes_ref' => \$downbytes,
976 'totalbytes_ref' => \$totalbytes,
978 || $self->increment_seconds($seconds)
979 || $self->increment_upbytes($upbytes)
980 || $self->increment_downbytes($downbytes)
981 || $self->increment_totalbytes($totalbytes)
982 || $self->insert_cust_pay_prepay( $amount,
984 ? $prepay_credit->identifier
989 $dbh->rollback if $oldAutoCommit;
993 if ( defined($amountref) ) { $$amountref = $amount; }
994 if ( defined($secondsref) ) { $$secondsref = $seconds; }
995 if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
996 if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
997 if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
999 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1004 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , OPTION => VALUE ... ]
1006 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
1007 specified either by I<identifier> or as an FS::prepay_credit object.
1009 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
1010 incremented by the values of the prepaid card.
1012 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
1013 check or set this customer's I<agentnum>.
1015 If there is an error, returns the error, otherwise returns false.
1021 my( $self, $prepay_credit, %opt ) = @_;
1023 local $SIG{HUP} = 'IGNORE';
1024 local $SIG{INT} = 'IGNORE';
1025 local $SIG{QUIT} = 'IGNORE';
1026 local $SIG{TERM} = 'IGNORE';
1027 local $SIG{TSTP} = 'IGNORE';
1028 local $SIG{PIPE} = 'IGNORE';
1030 my $oldAutoCommit = $FS::UID::AutoCommit;
1031 local $FS::UID::AutoCommit = 0;
1034 unless ( ref($prepay_credit) ) {
1036 my $identifier = $prepay_credit;
1038 $prepay_credit = qsearchs(
1040 { 'identifier' => $prepay_credit },
1045 unless ( $prepay_credit ) {
1046 $dbh->rollback if $oldAutoCommit;
1047 return "Invalid prepaid card: ". $identifier;
1052 if ( $prepay_credit->agentnum ) {
1053 if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
1054 $dbh->rollback if $oldAutoCommit;
1055 return "prepaid card not valid for agent ". $self->agentnum;
1057 $self->agentnum($prepay_credit->agentnum);
1060 my $error = $prepay_credit->delete;
1062 $dbh->rollback if $oldAutoCommit;
1063 return "removing prepay_credit (transaction rolled back): $error";
1066 ${ $opt{$_.'_ref'} } += $prepay_credit->$_()
1067 for grep $opt{$_.'_ref'}, qw( amount seconds upbytes downbytes totalbytes );
1069 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1074 =item increment_upbytes SECONDS
1076 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1077 the specified number of upbytes. If there is an error, returns the error,
1078 otherwise returns false.
1082 sub increment_upbytes {
1083 _increment_column( shift, 'upbytes', @_);
1086 =item increment_downbytes SECONDS
1088 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1089 the specified number of downbytes. If there is an error, returns the error,
1090 otherwise returns false.
1094 sub increment_downbytes {
1095 _increment_column( shift, 'downbytes', @_);
1098 =item increment_totalbytes SECONDS
1100 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1101 the specified number of totalbytes. If there is an error, returns the error,
1102 otherwise returns false.
1106 sub increment_totalbytes {
1107 _increment_column( shift, 'totalbytes', @_);
1110 =item increment_seconds SECONDS
1112 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1113 the specified number of seconds. If there is an error, returns the error,
1114 otherwise returns false.
1118 sub increment_seconds {
1119 _increment_column( shift, 'seconds', @_);
1122 =item _increment_column AMOUNT
1124 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1125 the specified number of seconds or bytes. If there is an error, returns
1126 the error, otherwise returns false.
1130 sub _increment_column {
1131 my( $self, $column, $amount ) = @_;
1132 warn "$me increment_column called: $column, $amount\n"
1135 return '' unless $amount;
1137 my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
1138 $self->ncancelled_pkgs;
1140 if ( ! @cust_pkg ) {
1141 return 'No packages with primary or single services found'.
1142 ' to apply pre-paid time';
1143 } elsif ( scalar(@cust_pkg) > 1 ) {
1144 #maybe have a way to specify the package/account?
1145 return 'Multiple packages found to apply pre-paid time';
1148 my $cust_pkg = $cust_pkg[0];
1149 warn " found package pkgnum ". $cust_pkg->pkgnum. "\n"
1153 $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
1155 if ( ! @cust_svc ) {
1156 return 'No account found to apply pre-paid time';
1157 } elsif ( scalar(@cust_svc) > 1 ) {
1158 return 'Multiple accounts found to apply pre-paid time';
1161 my $svc_acct = $cust_svc[0]->svc_x;
1162 warn " found service svcnum ". $svc_acct->pkgnum.
1163 ' ('. $svc_acct->email. ")\n"
1166 $column = "increment_$column";
1167 $svc_acct->$column($amount);
1171 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
1173 Inserts a prepayment in the specified amount for this customer. An optional
1174 second argument can specify the prepayment identifier for tracking purposes.
1175 If there is an error, returns the error, otherwise returns false.
1179 sub insert_cust_pay_prepay {
1180 shift->insert_cust_pay('PREP', @_);
1183 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
1185 Inserts a cash payment in the specified amount for this customer. An optional
1186 second argument can specify the payment identifier for tracking purposes.
1187 If there is an error, returns the error, otherwise returns false.
1191 sub insert_cust_pay_cash {
1192 shift->insert_cust_pay('CASH', @_);
1195 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
1197 Inserts a Western Union payment in the specified amount for this customer. An
1198 optional second argument can specify the prepayment identifier for tracking
1199 purposes. If there is an error, returns the error, otherwise returns false.
1203 sub insert_cust_pay_west {
1204 shift->insert_cust_pay('WEST', @_);
1207 sub insert_cust_pay {
1208 my( $self, $payby, $amount ) = splice(@_, 0, 3);
1209 my $payinfo = scalar(@_) ? shift : '';
1211 my $cust_pay = new FS::cust_pay {
1212 'custnum' => $self->custnum,
1213 'paid' => sprintf('%.2f', $amount),
1214 #'_date' => #date the prepaid card was purchased???
1216 'payinfo' => $payinfo,
1224 This method is deprecated. See the I<depend_jobnum> option to the insert and
1225 order_pkgs methods for a better way to defer provisioning.
1227 Re-schedules all exports by calling the B<reexport> method of all associated
1228 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
1229 otherwise returns false.
1236 carp "WARNING: FS::cust_main::reexport is deprectated; ".
1237 "use the depend_jobnum option to insert or order_pkgs to delay export";
1239 local $SIG{HUP} = 'IGNORE';
1240 local $SIG{INT} = 'IGNORE';
1241 local $SIG{QUIT} = 'IGNORE';
1242 local $SIG{TERM} = 'IGNORE';
1243 local $SIG{TSTP} = 'IGNORE';
1244 local $SIG{PIPE} = 'IGNORE';
1246 my $oldAutoCommit = $FS::UID::AutoCommit;
1247 local $FS::UID::AutoCommit = 0;
1250 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
1251 my $error = $cust_pkg->reexport;
1253 $dbh->rollback if $oldAutoCommit;
1258 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1263 =item delete NEW_CUSTNUM
1265 This deletes the customer. If there is an error, returns the error, otherwise
1268 This will completely remove all traces of the customer record. This is not
1269 what you want when a customer cancels service; for that, cancel all of the
1270 customer's packages (see L</cancel>).
1272 If the customer has any uncancelled packages, you need to pass a new (valid)
1273 customer number for those packages to be transferred to. Cancelled packages
1274 will be deleted. Did I mention that this is NOT what you want when a customer
1275 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
1277 You can't delete a customer with invoices (see L<FS::cust_bill>),
1278 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
1279 refunds (see L<FS::cust_refund>).
1286 local $SIG{HUP} = 'IGNORE';
1287 local $SIG{INT} = 'IGNORE';
1288 local $SIG{QUIT} = 'IGNORE';
1289 local $SIG{TERM} = 'IGNORE';
1290 local $SIG{TSTP} = 'IGNORE';
1291 local $SIG{PIPE} = 'IGNORE';
1293 my $oldAutoCommit = $FS::UID::AutoCommit;
1294 local $FS::UID::AutoCommit = 0;
1297 if ( $self->cust_bill ) {
1298 $dbh->rollback if $oldAutoCommit;
1299 return "Can't delete a customer with invoices";
1301 if ( $self->cust_credit ) {
1302 $dbh->rollback if $oldAutoCommit;
1303 return "Can't delete a customer with credits";
1305 if ( $self->cust_pay ) {
1306 $dbh->rollback if $oldAutoCommit;
1307 return "Can't delete a customer with payments";
1309 if ( $self->cust_refund ) {
1310 $dbh->rollback if $oldAutoCommit;
1311 return "Can't delete a customer with refunds";
1314 my @cust_pkg = $self->ncancelled_pkgs;
1316 my $new_custnum = shift;
1317 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1318 $dbh->rollback if $oldAutoCommit;
1319 return "Invalid new customer number: $new_custnum";
1321 foreach my $cust_pkg ( @cust_pkg ) {
1322 my %hash = $cust_pkg->hash;
1323 $hash{'custnum'} = $new_custnum;
1324 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1325 my $error = $new_cust_pkg->replace($cust_pkg,
1326 options => { $cust_pkg->options },
1329 $dbh->rollback if $oldAutoCommit;
1334 my @cancelled_cust_pkg = $self->all_pkgs;
1335 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1336 my $error = $cust_pkg->delete;
1338 $dbh->rollback if $oldAutoCommit;
1343 foreach my $table (qw( cust_main_invoice cust_main_exemption cust_tag )) {
1344 foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
1345 my $error = $record->delete;
1347 $dbh->rollback if $oldAutoCommit;
1353 my $error = $self->SUPER::delete;
1355 $dbh->rollback if $oldAutoCommit;
1359 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1364 =item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
1367 Replaces the OLD_RECORD with this one in the database. If there is an error,
1368 returns the error, otherwise returns false.
1370 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1371 be set as the invoicing list (see L<"invoicing_list">). Errors return as
1372 expected and rollback the entire transaction; it is not necessary to call
1373 check_invoicing_list first. Here's an example:
1375 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1377 Currently available options are: I<tax_exemption>.
1379 The I<tax_exemption> option can be set to an arrayref of tax names.
1380 FS::cust_main_exemption records will be deleted and inserted as appropriate.
1387 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
1389 : $self->replace_old;
1393 warn "$me replace called\n"
1396 my $curuser = $FS::CurrentUser::CurrentUser;
1397 if ( $self->payby eq 'COMP'
1398 && $self->payby ne $old->payby
1399 && ! $curuser->access_right('Complimentary customer')
1402 return "You are not permitted to create complimentary accounts.";
1405 local($ignore_expired_card) = 1
1406 if $old->payby =~ /^(CARD|DCRD)$/
1407 && $self->payby =~ /^(CARD|DCRD)$/
1408 && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1410 local $SIG{HUP} = 'IGNORE';
1411 local $SIG{INT} = 'IGNORE';
1412 local $SIG{QUIT} = 'IGNORE';
1413 local $SIG{TERM} = 'IGNORE';
1414 local $SIG{TSTP} = 'IGNORE';
1415 local $SIG{PIPE} = 'IGNORE';
1417 my $oldAutoCommit = $FS::UID::AutoCommit;
1418 local $FS::UID::AutoCommit = 0;
1421 my $error = $self->SUPER::replace($old);
1424 $dbh->rollback if $oldAutoCommit;
1428 if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
1429 my $invoicing_list = shift @param;
1430 $error = $self->check_invoicing_list( $invoicing_list );
1432 $dbh->rollback if $oldAutoCommit;
1435 $self->invoicing_list( $invoicing_list );
1438 if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
1440 #this could be more efficient than deleting and re-inserting, if it matters
1441 foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
1442 my $error = $cust_tag->delete;
1444 $dbh->rollback if $oldAutoCommit;
1448 foreach my $tagnum ( @{ $self->tagnum || [] } ) {
1449 my $cust_tag = new FS::cust_tag { 'tagnum' => $tagnum,
1450 'custnum' => $self->custnum };
1451 my $error = $cust_tag->insert;
1453 $dbh->rollback if $oldAutoCommit;
1460 my %options = @param;
1462 my $tax_exemption = delete $options{'tax_exemption'};
1463 if ( $tax_exemption ) {
1465 my %cust_main_exemption =
1466 map { $_->taxname => $_ }
1467 qsearch('cust_main_exemption', { 'custnum' => $old->custnum } );
1469 foreach my $taxname ( @$tax_exemption ) {
1471 next if delete $cust_main_exemption{$taxname};
1473 my $cust_main_exemption = new FS::cust_main_exemption {
1474 'custnum' => $self->custnum,
1475 'taxname' => $taxname,
1477 my $error = $cust_main_exemption->insert;
1479 $dbh->rollback if $oldAutoCommit;
1480 return "inserting cust_main_exemption (transaction rolled back): $error";
1484 foreach my $cust_main_exemption ( values %cust_main_exemption ) {
1485 my $error = $cust_main_exemption->delete;
1487 $dbh->rollback if $oldAutoCommit;
1488 return "deleting cust_main_exemption (transaction rolled back): $error";
1494 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
1495 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
1496 # card/check/lec info has changed, want to retry realtime_ invoice events
1497 my $error = $self->retry_realtime;
1499 $dbh->rollback if $oldAutoCommit;
1504 unless ( $import || $skip_fuzzyfiles ) {
1505 $error = $self->queue_fuzzyfiles_update;
1507 $dbh->rollback if $oldAutoCommit;
1508 return "updating fuzzy search cache: $error";
1512 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1517 =item queue_fuzzyfiles_update
1519 Used by insert & replace to update the fuzzy search cache
1523 sub queue_fuzzyfiles_update {
1526 local $SIG{HUP} = 'IGNORE';
1527 local $SIG{INT} = 'IGNORE';
1528 local $SIG{QUIT} = 'IGNORE';
1529 local $SIG{TERM} = 'IGNORE';
1530 local $SIG{TSTP} = 'IGNORE';
1531 local $SIG{PIPE} = 'IGNORE';
1533 my $oldAutoCommit = $FS::UID::AutoCommit;
1534 local $FS::UID::AutoCommit = 0;
1537 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1538 my $error = $queue->insert( map $self->getfield($_), @fuzzyfields );
1540 $dbh->rollback if $oldAutoCommit;
1541 return "queueing job (transaction rolled back): $error";
1544 if ( $self->ship_last ) {
1545 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1546 $error = $queue->insert( map $self->getfield("ship_$_"), @fuzzyfields );
1548 $dbh->rollback if $oldAutoCommit;
1549 return "queueing job (transaction rolled back): $error";
1553 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1560 Checks all fields to make sure this is a valid customer record. If there is
1561 an error, returns the error, otherwise returns false. Called by the insert
1562 and replace methods.
1569 warn "$me check BEFORE: \n". $self->_dump
1573 $self->ut_numbern('custnum')
1574 || $self->ut_number('agentnum')
1575 || $self->ut_textn('agent_custid')
1576 || $self->ut_number('refnum')
1577 || $self->ut_textn('custbatch')
1578 || $self->ut_name('last')
1579 || $self->ut_name('first')
1580 || $self->ut_snumbern('birthdate')
1581 || $self->ut_snumbern('signupdate')
1582 || $self->ut_textn('company')
1583 || $self->ut_text('address1')
1584 || $self->ut_textn('address2')
1585 || $self->ut_text('city')
1586 || $self->ut_textn('county')
1587 || $self->ut_textn('state')
1588 || $self->ut_country('country')
1589 || $self->ut_anything('comments')
1590 || $self->ut_numbern('referral_custnum')
1591 || $self->ut_textn('stateid')
1592 || $self->ut_textn('stateid_state')
1593 || $self->ut_textn('invoice_terms')
1594 || $self->ut_alphan('geocode')
1595 || $self->ut_floatn('cdr_termination_percentage')
1598 #barf. need message catalogs. i18n. etc.
1599 $error .= "Please select an advertising source."
1600 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1601 return $error if $error;
1603 return "Unknown agent"
1604 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1606 return "Unknown refnum"
1607 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1609 return "Unknown referring custnum: ". $self->referral_custnum
1610 unless ! $self->referral_custnum
1611 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1613 if ( $self->censustract ne '' ) {
1614 $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/
1615 or return "Illegal census tract: ". $self->censustract;
1617 $self->censustract("$1.$2");
1620 if ( $self->ss eq '' ) {
1625 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1626 or return "Illegal social security number: ". $self->ss;
1627 $self->ss("$1-$2-$3");
1631 # bad idea to disable, causes billing to fail because of no tax rates later
1632 # unless ( $import ) {
1633 unless ( qsearch('cust_main_county', {
1634 'country' => $self->country,
1637 return "Unknown state/county/country: ".
1638 $self->state. "/". $self->county. "/". $self->country
1639 unless qsearch('cust_main_county',{
1640 'state' => $self->state,
1641 'county' => $self->county,
1642 'country' => $self->country,
1648 $self->ut_phonen('daytime', $self->country)
1649 || $self->ut_phonen('night', $self->country)
1650 || $self->ut_phonen('fax', $self->country)
1651 || $self->ut_zip('zip', $self->country)
1653 return $error if $error;
1655 if ( $conf->exists('cust_main-require_phone')
1656 && ! length($self->daytime) && ! length($self->night)
1659 my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1661 : FS::Msgcat::_gettext('daytime');
1662 my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1664 : FS::Msgcat::_gettext('night');
1666 return "$daytime_label or $night_label is required"
1670 if ( $self->has_ship_address
1671 && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1672 $self->addr_fields )
1676 $self->ut_name('ship_last')
1677 || $self->ut_name('ship_first')
1678 || $self->ut_textn('ship_company')
1679 || $self->ut_text('ship_address1')
1680 || $self->ut_textn('ship_address2')
1681 || $self->ut_text('ship_city')
1682 || $self->ut_textn('ship_county')
1683 || $self->ut_textn('ship_state')
1684 || $self->ut_country('ship_country')
1686 return $error if $error;
1688 #false laziness with above
1689 unless ( qsearchs('cust_main_county', {
1690 'country' => $self->ship_country,
1693 return "Unknown ship_state/ship_county/ship_country: ".
1694 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1695 unless qsearch('cust_main_county',{
1696 'state' => $self->ship_state,
1697 'county' => $self->ship_county,
1698 'country' => $self->ship_country,
1704 $self->ut_phonen('ship_daytime', $self->ship_country)
1705 || $self->ut_phonen('ship_night', $self->ship_country)
1706 || $self->ut_phonen('ship_fax', $self->ship_country)
1707 || $self->ut_zip('ship_zip', $self->ship_country)
1709 return $error if $error;
1711 return "Unit # is required."
1712 if $self->ship_address2 =~ /^\s*$/
1713 && $conf->exists('cust_main-require_address2');
1715 } else { # ship_ info eq billing info, so don't store dup info in database
1717 $self->setfield("ship_$_", '')
1718 foreach $self->addr_fields;
1720 return "Unit # is required."
1721 if $self->address2 =~ /^\s*$/
1722 && $conf->exists('cust_main-require_address2');
1726 #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1727 # or return "Illegal payby: ". $self->payby;
1729 FS::payby->can_payby($self->table, $self->payby)
1730 or return "Illegal payby: ". $self->payby;
1732 $error = $self->ut_numbern('paystart_month')
1733 || $self->ut_numbern('paystart_year')
1734 || $self->ut_numbern('payissue')
1735 || $self->ut_textn('paytype')
1737 return $error if $error;
1739 if ( $self->payip eq '' ) {
1742 $error = $self->ut_ip('payip');
1743 return $error if $error;
1746 # If it is encrypted and the private key is not availaible then we can't
1747 # check the credit card.
1749 my $check_payinfo = 1;
1751 if ($self->is_encrypted($self->payinfo)) {
1755 if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1757 my $payinfo = $self->payinfo;
1758 $payinfo =~ s/\D//g;
1759 $payinfo =~ /^(\d{13,16})$/
1760 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1762 $self->payinfo($payinfo);
1764 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1766 return gettext('unknown_card_type')
1767 if cardtype($self->payinfo) eq "Unknown";
1769 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1771 return 'Banned credit card: banned on '.
1772 time2str('%a %h %o at %r', $ban->_date).
1773 ' by '. $ban->otaker.
1774 ' (ban# '. $ban->bannum. ')';
1777 if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1778 if ( cardtype($self->payinfo) eq 'American Express card' ) {
1779 $self->paycvv =~ /^(\d{4})$/
1780 or return "CVV2 (CID) for American Express cards is four digits.";
1783 $self->paycvv =~ /^(\d{3})$/
1784 or return "CVV2 (CVC2/CID) is three digits.";
1791 my $cardtype = cardtype($payinfo);
1792 if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1794 return "Start date or issue number is required for $cardtype cards"
1795 unless $self->paystart_month && $self->paystart_year or $self->payissue;
1797 return "Start month must be between 1 and 12"
1798 if $self->paystart_month
1799 and $self->paystart_month < 1 || $self->paystart_month > 12;
1801 return "Start year must be 1990 or later"
1802 if $self->paystart_year
1803 and $self->paystart_year < 1990;
1805 return "Issue number must be beween 1 and 99"
1807 and $self->payissue < 1 || $self->payissue > 99;
1810 $self->paystart_month('');
1811 $self->paystart_year('');
1812 $self->payissue('');
1815 } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1817 my $payinfo = $self->payinfo;
1818 $payinfo =~ s/[^\d\@]//g;
1819 if ( $conf->exists('echeck-nonus') ) {
1820 $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1821 $payinfo = "$1\@$2";
1823 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1824 $payinfo = "$1\@$2";
1826 $self->payinfo($payinfo);
1829 my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1831 return 'Banned ACH account: banned on '.
1832 time2str('%a %h %o at %r', $ban->_date).
1833 ' by '. $ban->otaker.
1834 ' (ban# '. $ban->bannum. ')';
1837 } elsif ( $self->payby eq 'LECB' ) {
1839 my $payinfo = $self->payinfo;
1840 $payinfo =~ s/\D//g;
1841 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1843 $self->payinfo($payinfo);
1846 } elsif ( $self->payby eq 'BILL' ) {
1848 $error = $self->ut_textn('payinfo');
1849 return "Illegal P.O. number: ". $self->payinfo if $error;
1852 } elsif ( $self->payby eq 'COMP' ) {
1854 my $curuser = $FS::CurrentUser::CurrentUser;
1855 if ( ! $self->custnum
1856 && ! $curuser->access_right('Complimentary customer')
1859 return "You are not permitted to create complimentary accounts."
1862 $error = $self->ut_textn('payinfo');
1863 return "Illegal comp account issuer: ". $self->payinfo if $error;
1866 } elsif ( $self->payby eq 'PREPAY' ) {
1868 my $payinfo = $self->payinfo;
1869 $payinfo =~ s/\W//g; #anything else would just confuse things
1870 $self->payinfo($payinfo);
1871 $error = $self->ut_alpha('payinfo');
1872 return "Illegal prepayment identifier: ". $self->payinfo if $error;
1873 return "Unknown prepayment identifier"
1874 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1879 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1880 return "Expiration date required"
1881 unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1885 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1886 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1887 } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1888 ( $m, $y ) = ( $2, "19$1" );
1889 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1890 ( $m, $y ) = ( $3, "20$2" );
1892 return "Illegal expiration date: ". $self->paydate;
1894 $self->paydate("$y-$m-01");
1895 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1896 return gettext('expired_card')
1898 && !$ignore_expired_card
1899 && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1902 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1903 ( ! $conf->exists('require_cardname')
1904 || $self->payby !~ /^(CARD|DCRD)$/ )
1906 $self->payname( $self->first. " ". $self->getfield('last') );
1908 $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1909 or return gettext('illegal_name'). " payname: ". $self->payname;
1913 foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
1914 $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1918 $self->otaker(getotaker) unless $self->otaker;
1920 warn "$me check AFTER: \n". $self->_dump
1923 $self->SUPER::check;
1928 Returns a list of fields which have ship_ duplicates.
1933 qw( last first company
1934 address1 address2 city county state zip country
1939 =item has_ship_address
1941 Returns true if this customer record has a separate shipping address.
1945 sub has_ship_address {
1947 scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
1952 Returns a list of key/value pairs, with the following keys: address1, adddress2,
1953 city, county, state, zip, country. The shipping address is used if present.
1957 #geocode? dependent on tax-ship_address config, not available in cust_location
1958 #mostly. not yet then.
1962 my $prefix = $self->has_ship_address ? 'ship_' : '';
1964 map { $_ => $self->get($prefix.$_) }
1965 qw( address1 address2 city county state zip country geocode );
1966 #fields that cust_location has
1969 =item all_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
1971 Returns all packages (see L<FS::cust_pkg>) for this customer.
1977 my $extra_qsearch = ref($_[0]) ? shift : {};
1979 return $self->num_pkgs unless wantarray || keys(%$extra_qsearch);
1982 if ( $self->{'_pkgnum'} ) {
1983 @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
1985 @cust_pkg = $self->_cust_pkg($extra_qsearch);
1988 sort sort_packages @cust_pkg;
1993 Synonym for B<all_pkgs>.
1998 shift->all_pkgs(@_);
2003 Returns all locations (see L<FS::cust_location>) for this customer.
2009 qsearch('cust_location', { 'custnum' => $self->custnum } );
2012 =item location_label [ OPTION => VALUE ... ]
2014 Returns the label of the service location (see analog in L<FS::cust_location>) for this customer.
2022 used to separate the address elements (defaults to ', ')
2024 =item escape_function
2026 a callback used for escaping the text of the address elements
2032 # false laziness with FS::cust_location::line
2034 sub location_label {
2038 my $separator = $opt{join_string} || ', ';
2039 my $escape = $opt{escape_function} || sub{ shift };
2041 my $cydefault = FS::conf->new->config('countrydefault') || 'US';
2042 my $prefix = length($self->ship_last) ? 'ship_' : '';
2045 foreach (qw ( address1 address2 ) ) {
2046 my $method = "$prefix$_";
2047 $line .= ($notfirst ? $separator : ''). &$escape($self->$method)
2052 foreach (qw ( city county state zip ) ) {
2053 my $method = "$prefix$_";
2054 if ( $self->$method ) {
2055 $line .= ' (' if $method eq 'county';
2056 $line .= ($notfirst ? ' ' : $separator). &$escape($self->$method);
2057 $line .= ' )' if $method eq 'county';
2061 $line .= $separator. &$escape(code2country($self->country))
2062 if $self->country ne $cydefault;
2067 =item ncancelled_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
2069 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
2073 sub ncancelled_pkgs {
2075 my $extra_qsearch = ref($_[0]) ? shift : {};
2077 return $self->num_ncancelled_pkgs unless wantarray;
2080 if ( $self->{'_pkgnum'} ) {
2082 warn "$me ncancelled_pkgs: returning cached objects"
2085 @cust_pkg = grep { ! $_->getfield('cancel') }
2086 values %{ $self->{'_pkgnum'}->cache };
2090 warn "$me ncancelled_pkgs: searching for packages with custnum ".
2091 $self->custnum. "\n"
2094 $extra_qsearch->{'extra_sql'} .= ' AND ( cancel IS NULL OR cancel = 0 ) ';
2096 @cust_pkg = $self->_cust_pkg($extra_qsearch);
2100 sort sort_packages @cust_pkg;
2106 my $extra_qsearch = ref($_[0]) ? shift : {};
2108 $extra_qsearch->{'select'} ||= '*';
2109 $extra_qsearch->{'select'} .=
2110 ',( SELECT COUNT(*) FROM cust_svc WHERE cust_pkg.pkgnum = cust_svc.pkgnum )
2114 $_->{'_num_cust_svc'} = $_->get('_num_cust_svc');
2119 'table' => 'cust_pkg',
2120 'hashref' => { 'custnum' => $self->custnum },
2125 # This should be generalized to use config options to determine order.
2128 my $locationsort = ( $a->locationnum || 0 ) <=> ( $b->locationnum || 0 );
2129 return $locationsort if $locationsort;
2131 if ( $a->get('cancel') xor $b->get('cancel') ) {
2132 return -1 if $b->get('cancel');
2133 return 1 if $a->get('cancel');
2134 #shouldn't get here...
2137 my $a_num_cust_svc = $a->num_cust_svc;
2138 my $b_num_cust_svc = $b->num_cust_svc;
2139 return 0 if !$a_num_cust_svc && !$b_num_cust_svc;
2140 return -1 if $a_num_cust_svc && !$b_num_cust_svc;
2141 return 1 if !$a_num_cust_svc && $b_num_cust_svc;
2142 my @a_cust_svc = $a->cust_svc;
2143 my @b_cust_svc = $b->cust_svc;
2144 return 0 if !scalar(@a_cust_svc) && !scalar(@b_cust_svc);
2145 return -1 if scalar(@a_cust_svc) && !scalar(@b_cust_svc);
2146 return 1 if !scalar(@a_cust_svc) && scalar(@b_cust_svc);
2147 $a_cust_svc[0]->svc_x->label cmp $b_cust_svc[0]->svc_x->label;
2152 =item suspended_pkgs
2154 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
2158 sub suspended_pkgs {
2160 grep { $_->susp } $self->ncancelled_pkgs;
2163 =item unflagged_suspended_pkgs
2165 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
2166 customer (thouse packages without the `manual_flag' set).
2170 sub unflagged_suspended_pkgs {
2172 return $self->suspended_pkgs
2173 unless dbdef->table('cust_pkg')->column('manual_flag');
2174 grep { ! $_->manual_flag } $self->suspended_pkgs;
2177 =item unsuspended_pkgs
2179 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
2184 sub unsuspended_pkgs {
2186 grep { ! $_->susp } $self->ncancelled_pkgs;
2189 =item next_bill_date
2191 Returns the next date this customer will be billed, as a UNIX timestamp, or
2192 undef if no active package has a next bill date.
2196 sub next_bill_date {
2198 min( map $_->get('bill'), grep $_->get('bill'), $self->unsuspended_pkgs );
2201 =item num_cancelled_pkgs
2203 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
2208 sub num_cancelled_pkgs {
2209 shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
2212 sub num_ncancelled_pkgs {
2213 shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
2217 my( $self ) = shift;
2218 my $sql = scalar(@_) ? shift : '';
2219 $sql = "AND $sql" if $sql && $sql !~ /^\s*$/ && $sql !~ /^\s*AND/i;
2220 my $sth = dbh->prepare(
2221 "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? $sql"
2222 ) or die dbh->errstr;
2223 $sth->execute($self->custnum) or die $sth->errstr;
2224 $sth->fetchrow_arrayref->[0];
2229 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
2230 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
2231 on success or a list of errors.
2237 grep { $_->unsuspend } $self->suspended_pkgs;
2242 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
2244 Returns a list: an empty list on success or a list of errors.
2250 grep { $_->suspend(@_) } $self->unsuspended_pkgs;
2253 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2255 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
2256 PKGPARTs (see L<FS::part_pkg>). Preferred usage is to pass a hashref instead
2257 of a list of pkgparts; the hashref has the following keys:
2261 =item pkgparts - listref of pkgparts
2263 =item (other options are passed to the suspend method)
2268 Returns a list: an empty list on success or a list of errors.
2272 sub suspend_if_pkgpart {
2274 my (@pkgparts, %opt);
2275 if (ref($_[0]) eq 'HASH'){
2276 @pkgparts = @{$_[0]{pkgparts}};
2281 grep { $_->suspend(%opt) }
2282 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
2283 $self->unsuspended_pkgs;
2286 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2288 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
2289 given PKGPARTs (see L<FS::part_pkg>). Preferred usage is to pass a hashref
2290 instead of a list of pkgparts; the hashref has the following keys:
2294 =item pkgparts - listref of pkgparts
2296 =item (other options are passed to the suspend method)
2300 Returns a list: an empty list on success or a list of errors.
2304 sub suspend_unless_pkgpart {
2306 my (@pkgparts, %opt);
2307 if (ref($_[0]) eq 'HASH'){
2308 @pkgparts = @{$_[0]{pkgparts}};
2313 grep { $_->suspend(%opt) }
2314 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
2315 $self->unsuspended_pkgs;
2318 =item cancel [ OPTION => VALUE ... ]
2320 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
2322 Available options are:
2326 =item quiet - can be set true to supress email cancellation notices.
2328 =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.
2330 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
2332 =item nobill - can be set true to skip billing if it might otherwise be done.
2336 Always returns a list: an empty list on success or a list of errors.
2340 # nb that dates are not specified as valid options to this method
2343 my( $self, %opt ) = @_;
2345 warn "$me cancel called on customer ". $self->custnum. " with options ".
2346 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
2349 return ( 'access denied' )
2350 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
2352 if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
2354 #should try decryption (we might have the private key)
2355 # and if not maybe queue a job for the server that does?
2356 return ( "Can't (yet) ban encrypted credit cards" )
2357 if $self->is_encrypted($self->payinfo);
2359 my $ban = new FS::banned_pay $self->_banned_pay_hashref;
2360 my $error = $ban->insert;
2361 return ( $error ) if $error;
2365 my @pkgs = $self->ncancelled_pkgs;
2367 if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
2369 my $error = $self->bill( pkg_list => [ @pkgs ], cancel => 1 );
2370 warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
2374 warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
2375 scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
2378 grep { $_ } map { $_->cancel(%opt) } $self->ncancelled_pkgs;
2381 sub _banned_pay_hashref {
2392 'payby' => $payby2ban{$self->payby},
2393 'payinfo' => md5_base64($self->payinfo),
2394 #don't ever *search* on reason! #'reason' =>
2400 Returns all notes (see L<FS::cust_main_note>) for this customer.
2407 qsearch( 'cust_main_note',
2408 { 'custnum' => $self->custnum },
2410 'ORDER BY _DATE DESC'
2416 Returns the agent (see L<FS::agent>) for this customer.
2422 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
2427 Returns the agent name (see L<FS::agent>) for this customer.
2433 $self->agent->agent;
2438 Returns any tags associated with this customer, as FS::cust_tag objects,
2439 or an empty list if there are no tags.
2445 qsearch('cust_tag', { 'custnum' => $self->custnum } );
2450 Returns any tags associated with this customer, as FS::part_tag objects,
2451 or an empty list if there are no tags.
2457 map $_->part_tag, $self->cust_tag;
2460 =item bill_and_collect
2462 Cancels and suspends any packages due, generates bills, applies payments and
2463 credits, and applies collection events to run cards, send bills and notices,
2466 By default, warns on errors and continues with the next operation (but see the
2467 "fatal" flag below).
2469 Options are passed as name-value pairs. Currently available options are:
2475 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:
2479 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
2483 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.
2487 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
2491 If set true, re-charges setup fees.
2495 If set any errors prevent subsequent operations from continusing. If set
2496 specifically to "return", returns the error (or false, if there is no error).
2497 Any other true value causes errors to die.
2501 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)
2505 Options are passed to the B<bill> and B<collect> methods verbatim, so all
2506 options of those methods are also available.
2510 sub bill_and_collect {
2511 my( $self, %options ) = @_;
2515 #$options{actual_time} not $options{time} because freeside-daily -d is for
2516 #pre-printing invoices
2518 $options{'actual_time'} ||= time;
2520 $error = $self->cancel_expired_pkgs( $options{actual_time} );
2522 $error = "Error expiring custnum ". $self->custnum. ": $error";
2523 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2524 elsif ( $options{fatal} ) { die $error; }
2525 else { warn $error; }
2528 $error = $self->suspend_adjourned_pkgs( $options{actual_time} );
2530 $error = "Error adjourning custnum ". $self->custnum. ": $error";
2531 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2532 elsif ( $options{fatal} ) { die $error; }
2533 else { warn $error; }
2536 $error = $self->bill( %options );
2538 $error = "Error billing custnum ". $self->custnum. ": $error";
2539 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2540 elsif ( $options{fatal} ) { die $error; }
2541 else { warn $error; }
2544 $error = $self->apply_payments_and_credits;
2546 $error = "Error applying custnum ". $self->custnum. ": $error";
2547 if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2548 elsif ( $options{fatal} ) { die $error; }
2549 else { warn $error; }
2552 unless ( $conf->exists('cancelled_cust-noevents')
2553 && ! $self->num_ncancelled_pkgs
2555 $error = $self->collect( %options );
2557 $error = "Error collecting custnum ". $self->custnum. ": $error";
2558 if ($options{fatal} && $options{fatal} eq 'return') { return $error; }
2559 elsif ($options{fatal} ) { die $error; }
2560 else { warn $error; }
2568 sub cancel_expired_pkgs {
2569 my ( $self, $time, %options ) = @_;
2571 my @cancel_pkgs = $self->ncancelled_pkgs( {
2572 'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
2577 foreach my $cust_pkg ( @cancel_pkgs ) {
2578 my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
2579 my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
2580 'reason_otaker' => $cpr->otaker
2584 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
2587 scalar(@errors) ? join(' / ', @errors) : '';
2591 sub suspend_adjourned_pkgs {
2592 my ( $self, $time, %options ) = @_;
2594 my @susp_pkgs = $self->ncancelled_pkgs( {
2596 " AND ( susp IS NULL OR susp = 0 )
2597 AND ( ( bill IS NOT NULL AND bill != 0 AND bill < $time )
2598 OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
2603 #only because there's no SQL test for is_prepaid :/
2605 grep { ( $_->part_pkg->is_prepaid
2610 && $_->adjourn <= $time
2618 foreach my $cust_pkg ( @susp_pkgs ) {
2619 my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
2620 if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
2621 my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
2622 'reason_otaker' => $cpr->otaker
2626 push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
2629 scalar(@errors) ? join(' / ', @errors) : '';
2635 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
2636 conjunction with the collect method by calling B<bill_and_collect>.
2638 If there is an error, returns the error, otherwise returns false.
2640 Options are passed as name-value pairs. Currently available options are:
2646 If set true, re-charges setup fees.
2650 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:
2654 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
2658 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
2660 $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
2664 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
2668 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.
2672 This boolean value informs the us that the package is being cancelled. This
2673 typically might mean not charging the normal recurring fee but only usage
2674 fees since the last billing. Setup charges may be charged. Not all package
2675 plans support this feature (they tend to charge 0).
2679 Optional terms to be printed on this invoice. Otherwise, customer-specific
2680 terms or the default terms are used.
2687 my( $self, %options ) = @_;
2688 return '' if $self->payby eq 'COMP';
2689 warn "$me bill customer ". $self->custnum. "\n"
2692 my $time = $options{'time'} || time;
2693 my $invoice_time = $options{'invoice_time'} || $time;
2695 $options{'not_pkgpart'} ||= {};
2696 $options{'not_pkgpart'} = { map { $_ => 1 }
2697 split(/\s*,\s*/, $options{'not_pkgpart'})
2699 unless ref($options{'not_pkgpart'});
2701 local $SIG{HUP} = 'IGNORE';
2702 local $SIG{INT} = 'IGNORE';
2703 local $SIG{QUIT} = 'IGNORE';
2704 local $SIG{TERM} = 'IGNORE';
2705 local $SIG{TSTP} = 'IGNORE';
2706 local $SIG{PIPE} = 'IGNORE';
2708 my $oldAutoCommit = $FS::UID::AutoCommit;
2709 local $FS::UID::AutoCommit = 0;
2712 $self->select_for_update; #mutex
2714 my $error = $self->do_cust_event(
2715 'debug' => ( $options{'debug'} || 0 ),
2716 'time' => $invoice_time,
2717 'check_freq' => $options{'check_freq'},
2718 'stage' => 'pre-bill',
2721 $dbh->rollback if $oldAutoCommit;
2725 my @cust_bill_pkg = ();
2728 # find the packages which are due for billing, find out how much they are
2729 # & generate invoice database.
2732 my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 );
2734 my @precommit_hooks = ();
2736 $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks?
2737 foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
2739 next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
2741 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
2743 #? to avoid use of uninitialized value errors... ?
2744 $cust_pkg->setfield('bill', '')
2745 unless defined($cust_pkg->bill);
2747 #my $part_pkg = $cust_pkg->part_pkg;
2749 my $real_pkgpart = $cust_pkg->pkgpart;
2750 my %hash = $cust_pkg->hash;
2752 # we could implement this bit as FS::part_pkg::has_hidden, but we already
2753 # suffer from performance issues
2754 $options{has_hidden} = 0;
2755 my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
2756 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
2758 foreach my $part_pkg ( @part_pkg ) {
2760 $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
2763 $self->_make_lines( 'part_pkg' => $part_pkg,
2764 'cust_pkg' => $cust_pkg,
2765 'precommit_hooks' => \@precommit_hooks,
2766 'line_items' => \@cust_bill_pkg,
2767 'setup' => \$total_setup,
2768 'recur' => \$total_recur,
2769 'tax_matrix' => \%taxlisthash,
2771 'real_pkgpart' => $real_pkgpart,
2772 'options' => \%options,
2775 $dbh->rollback if $oldAutoCommit;
2779 } #foreach my $part_pkg
2781 } #foreach my $cust_pkg
2783 @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
2785 unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items
2786 #but do commit any package date cycling that happened
2787 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2791 if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
2792 !$conf->exists('postal_invoice-recurring_only')
2796 my $postal_pkg = $self->charge_postal_fee();
2797 if ( $postal_pkg && !ref( $postal_pkg ) ) {
2799 $dbh->rollback if $oldAutoCommit;
2800 return "can't charge postal invoice fee for customer ".
2801 $self->custnum. ": $postal_pkg";
2803 } elsif ( $postal_pkg ) {
2805 my $real_pkgpart = $postal_pkg->pkgpart;
2806 # we could implement this bit as FS::part_pkg::has_hidden, but we alre
2808 # suffer from performance issues
2809 $options{has_hidden} = 0;
2810 my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
2811 $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
2813 foreach my $part_pkg ( @part_pkg ) {
2814 my %postal_options = %options;
2815 delete $postal_options{cancel};
2817 $self->_make_lines( 'part_pkg' => $part_pkg,
2818 'cust_pkg' => $postal_pkg,
2819 'precommit_hooks' => \@precommit_hooks,
2820 'line_items' => \@cust_bill_pkg,
2821 'setup' => \$total_setup,
2822 'recur' => \$total_recur,
2823 'tax_matrix' => \%taxlisthash,
2825 'real_pkgpart' => $real_pkgpart,
2826 'options' => \%postal_options,
2829 $dbh->rollback if $oldAutoCommit;
2834 # it's silly to have a zero value postal_pkg, but....
2835 @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
2841 warn "having a look at the taxes we found...\n" if $DEBUG > 2;
2843 # keys are tax names (as printed on invoices / itemdesc )
2844 # values are listrefs of taxlisthash keys (internal identifiers)
2847 # keys are taxlisthash keys (internal identifiers)
2848 # values are (cumulative) amounts
2851 # keys are taxlisthash keys (internal identifiers)
2852 # values are listrefs of cust_bill_pkg_tax_location hashrefs
2853 my %tax_location = ();
2855 # keys are taxlisthash keys (internal identifiers)
2856 # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs
2857 my %tax_rate_location = ();
2859 foreach my $tax ( keys %taxlisthash ) {
2860 my $tax_object = shift @{ $taxlisthash{$tax} };
2861 warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
2862 warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2;
2863 my $hashref_or_error =
2864 $tax_object->taxline( $taxlisthash{$tax},
2865 'custnum' => $self->custnum,
2866 'invoice_time' => $invoice_time
2868 unless ( ref($hashref_or_error) ) {
2869 $dbh->rollback if $oldAutoCommit;
2870 return $hashref_or_error;
2872 unshift @{ $taxlisthash{$tax} }, $tax_object;
2874 my $name = $hashref_or_error->{'name'};
2875 my $amount = $hashref_or_error->{'amount'};
2877 #warn "adding $amount as $name\n";
2878 $taxname{ $name } ||= [];
2879 push @{ $taxname{ $name } }, $tax;
2881 $tax{ $tax } += $amount;
2883 $tax_location{ $tax } ||= [];
2884 if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
2885 push @{ $tax_location{ $tax } },
2887 'taxnum' => $tax_object->taxnum,
2888 'taxtype' => ref($tax_object),
2889 'pkgnum' => $tax_object->get('pkgnum'),
2890 'locationnum' => $tax_object->get('locationnum'),
2891 'amount' => sprintf('%.2f', $amount ),
2895 $tax_rate_location{ $tax } ||= [];
2896 if ( ref($tax_object) eq 'FS::tax_rate' ) {
2897 my $taxratelocationnum =
2898 $tax_object->tax_rate_location->taxratelocationnum;
2899 push @{ $tax_rate_location{ $tax } },
2901 'taxnum' => $tax_object->taxnum,
2902 'taxtype' => ref($tax_object),
2903 'amount' => sprintf('%.2f', $amount ),
2904 'locationtaxid' => $tax_object->location,
2905 'taxratelocationnum' => $taxratelocationnum,
2911 #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
2912 my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
2913 foreach my $tax ( keys %taxlisthash ) {
2914 foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
2915 next unless ref($_) eq 'FS::cust_bill_pkg';
2917 push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
2918 splice( @{ $_->_cust_tax_exempt_pkg } );
2922 #consolidate and create tax line items
2923 warn "consolidating and generating...\n" if $DEBUG > 2;
2924 foreach my $taxname ( keys %taxname ) {
2927 my @cust_bill_pkg_tax_location = ();
2928 my @cust_bill_pkg_tax_rate_location = ();
2929 warn "adding $taxname\n" if $DEBUG > 1;
2930 foreach my $taxitem ( @{ $taxname{$taxname} } ) {
2931 next if $seen{$taxitem}++;
2932 warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
2933 $tax += $tax{$taxitem};
2934 push @cust_bill_pkg_tax_location,
2935 map { new FS::cust_bill_pkg_tax_location $_ }
2936 @{ $tax_location{ $taxitem } };
2937 push @cust_bill_pkg_tax_rate_location,
2938 map { new FS::cust_bill_pkg_tax_rate_location $_ }
2939 @{ $tax_rate_location{ $taxitem } };
2943 $tax = sprintf('%.2f', $tax );
2944 $total_setup = sprintf('%.2f', $total_setup+$tax );
2946 my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
2952 if ( $pkg_category and
2953 $conf->config('invoice_latexsummary') ||
2954 $conf->config('invoice_htmlsummary')
2958 my %hash = ( 'section' => $pkg_category->categoryname );
2959 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
2963 push @cust_bill_pkg, new FS::cust_bill_pkg {
2969 'itemdesc' => $taxname,
2970 'display' => \@display,
2971 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
2972 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
2977 #add tax adjustments
2978 warn "adding tax adjustments...\n" if $DEBUG > 2;
2979 foreach my $cust_tax_adjustment (
2980 qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
2986 my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
2987 $total_setup = sprintf('%.2f', $total_setup+$tax );
2989 my $itemdesc = $cust_tax_adjustment->taxname;
2990 $itemdesc = '' if $itemdesc eq 'Tax';
2992 push @cust_bill_pkg, new FS::cust_bill_pkg {
2998 'itemdesc' => $itemdesc,
2999 'itemcomment' => $cust_tax_adjustment->comment,
3000 'cust_tax_adjustment' => $cust_tax_adjustment,
3001 #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
3006 my $charged = sprintf('%.2f', $total_setup + $total_recur );
3008 my @cust_bill = $self->cust_bill;
3009 my $balance = $self->balance;
3010 my $previous_balance = scalar(@cust_bill)
3011 ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
3014 $previous_balance += $cust_bill[$#cust_bill]->charged
3015 if scalar(@cust_bill);
3016 #my $balance_adjustments =
3017 # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
3019 #create the new invoice
3020 my $cust_bill = new FS::cust_bill ( {
3021 'custnum' => $self->custnum,
3022 '_date' => ( $invoice_time ),
3023 'charged' => $charged,
3024 'billing_balance' => $balance,
3025 'previous_balance' => $previous_balance,
3026 'invoice_terms' => $options{'invoice_terms'},
3028 $error = $cust_bill->insert;
3030 $dbh->rollback if $oldAutoCommit;
3031 return "can't create invoice for customer #". $self->custnum. ": $error";
3034 foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
3035 $cust_bill_pkg->invnum($cust_bill->invnum);
3036 my $error = $cust_bill_pkg->insert;
3038 $dbh->rollback if $oldAutoCommit;
3039 return "can't create invoice line item: $error";
3044 foreach my $hook ( @precommit_hooks ) {
3046 &{$hook}; #($self) ?
3049 $dbh->rollback if $oldAutoCommit;
3050 return "$@ running precommit hook $hook\n";
3054 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3058 #discard bundled packages of 0 value
3059 sub _omit_zero_value_bundles {
3061 my @cust_bill_pkg = ();
3062 my @cust_bill_pkg_bundle = ();
3065 foreach my $cust_bill_pkg ( @_ ) {
3066 if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
3067 push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
3068 @cust_bill_pkg_bundle = ();
3071 $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
3072 push @cust_bill_pkg_bundle, $cust_bill_pkg;
3074 push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
3081 my ($self, %params) = @_;
3083 my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
3084 my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
3085 my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
3086 my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
3087 my $total_setup = $params{setup} or die "no setup accumulator specified";
3088 my $total_recur = $params{recur} or die "no recur accumulator specified";
3089 my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
3090 my $time = $params{'time'} or die "no time specified";
3091 my (%options) = %{$params{options}};
3094 my $real_pkgpart = $params{real_pkgpart};
3095 my %hash = $cust_pkg->hash;
3096 my $old_cust_pkg = new FS::cust_pkg \%hash;
3102 $cust_pkg->pkgpart($part_pkg->pkgpart);
3110 if ( $options{'resetup'}
3111 || ( ! $cust_pkg->setup
3112 && ( ! $cust_pkg->start_date
3113 || $cust_pkg->start_date <= $time
3115 && ( ! $conf->exists('disable_setup_suspended_pkgs')
3116 || ( $conf->exists('disable_setup_suspended_pkgs') &&
3117 ! $cust_pkg->getfield('susp')
3124 warn " bill setup\n" if $DEBUG > 1;
3127 $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
3128 return "$@ running calc_setup for $cust_pkg\n"
3131 $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
3133 $cust_pkg->setfield('setup', $time)
3134 unless $cust_pkg->setup;
3135 #do need it, but it won't get written to the db
3136 #|| $cust_pkg->pkgpart != $real_pkgpart;
3138 $cust_pkg->setfield('start_date', '')
3139 if $cust_pkg->start_date;
3144 # bill recurring fee
3147 #XXX unit stuff here too
3151 if ( ! $cust_pkg->get('susp')
3152 and ! $cust_pkg->get('start_date')
3153 and ( $part_pkg->getfield('freq') ne '0'
3154 && ( $cust_pkg->getfield('bill') || 0 ) <= $time
3156 || ( $part_pkg->plan eq 'voip_cdr'
3157 && $part_pkg->option('bill_every_call')
3159 || ( $options{cancel} )
3162 # XXX should this be a package event? probably. events are called
3163 # at collection time at the moment, though...
3164 $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
3165 if $part_pkg->can('reset_usage');
3166 #don't want to reset usage just cause we want a line item??
3167 #&& $part_pkg->pkgpart == $real_pkgpart;
3169 warn " bill recur\n" if $DEBUG > 1;
3172 # XXX shared with $recur_prog
3173 $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
3177 #over two params! lets at least switch to a hashref for the rest...
3178 my $increment_next_bill = ( $part_pkg->freq ne '0'
3179 && ( $cust_pkg->getfield('bill') || 0 ) <= $time
3180 && !$options{cancel}
3182 my %param = ( 'precommit_hooks' => $precommit_hooks,
3183 'increment_next_bill' => $increment_next_bill,
3186 my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
3187 $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
3188 return "$@ running $method for $cust_pkg\n"
3191 if ( $increment_next_bill ) {
3193 my $next_bill = $part_pkg->add_freq($sdate);
3194 return "unparsable frequency: ". $part_pkg->freq
3195 if $next_bill == -1;
3197 #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
3198 # only for figuring next bill date, nothing else, so, reset $sdate again
3200 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
3201 #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
3202 $cust_pkg->last_bill($sdate);
3204 $cust_pkg->setfield('bill', $next_bill );
3210 warn "\$setup is undefined" unless defined($setup);
3211 warn "\$recur is undefined" unless defined($recur);
3212 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
3215 # If there's line items, create em cust_bill_pkg records
3216 # If $cust_pkg has been modified, update it (if we're a real pkgpart)
3219 if ( $lineitems || $options{has_hidden} ) {
3221 if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
3222 # hmm.. and if just the options are modified in some weird price plan?
3224 warn " package ". $cust_pkg->pkgnum. " modified; updating\n"
3227 my $error = $cust_pkg->replace( $old_cust_pkg,
3228 'options' => { $cust_pkg->options },
3230 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
3231 if $error; #just in case
3234 $setup = sprintf( "%.2f", $setup );
3235 $recur = sprintf( "%.2f", $recur );
3236 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
3237 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
3239 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
3240 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
3245 !$part_pkg->hidden && $options{has_hidden} ) #include some $0 lines
3248 warn " charges (setup=$setup, recur=$recur); adding line items\n"
3251 my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
3253 warn " adding customer package invoice detail: $_\n"
3254 foreach @cust_pkg_detail;
3256 push @details, @cust_pkg_detail;
3258 my $cust_bill_pkg = new FS::cust_bill_pkg {
3259 'pkgnum' => $cust_pkg->pkgnum,
3261 'unitsetup' => $unitsetup,
3263 'unitrecur' => $unitrecur,
3264 'quantity' => $cust_pkg->quantity,
3265 'details' => \@details,
3266 'hidden' => $part_pkg->hidden,
3269 if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
3270 $cust_bill_pkg->sdate( $hash{last_bill} );
3271 $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1
3272 $cust_bill_pkg->edate( $time ) if $options{cancel};
3273 } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) {
3274 $cust_bill_pkg->sdate( $sdate );
3275 $cust_bill_pkg->edate( $cust_pkg->bill );
3276 #$cust_bill_pkg->edate( $time ) if $options{cancel};
3279 $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
3280 unless $part_pkg->pkgpart == $real_pkgpart;
3282 $$total_setup += $setup;
3283 $$total_recur += $recur;
3290 $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
3291 return $error if $error;
3293 push @$cust_bill_pkgs, $cust_bill_pkg;
3295 } #if $setup != 0 || $recur != 0
3305 my $part_pkg = shift;
3306 my $taxlisthash = shift;
3307 my $cust_bill_pkg = shift;
3308 my $cust_pkg = shift;
3309 my $invoice_time = shift;
3310 my $real_pkgpart = shift;
3311 my $options = shift;
3313 my %cust_bill_pkg = ();
3317 #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
3318 push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
3319 push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
3320 push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
3322 if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
3324 if ( $conf->exists('enable_taxproducts')
3325 && ( scalar($part_pkg->part_pkg_taxoverride)
3326 || $part_pkg->has_taxproduct
3331 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
3332 return "fatal: Can't (yet) use tax-pkg_address with taxproducts";
3335 foreach my $class (@classes) {
3336 my $err_or_ref = $self->_gather_taxes( $part_pkg, $class );
3337 return $err_or_ref unless ref($err_or_ref);
3338 $taxes{$class} = $err_or_ref;
3341 unless (exists $taxes{''}) {
3342 my $err_or_ref = $self->_gather_taxes( $part_pkg, '' );
3343 return $err_or_ref unless ref($err_or_ref);
3344 $taxes{''} = $err_or_ref;
3349 my @loc_keys = qw( state county country );
3351 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
3352 my $cust_location = $cust_pkg->cust_location;
3353 %taxhash = map { $_ => $cust_location->$_() } @loc_keys;
3356 ( $conf->exists('tax-ship_address') && length($self->ship_last) )
3359 %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
3362 $taxhash{'taxclass'} = $part_pkg->taxclass;
3364 my @taxes = qsearch( 'cust_main_county', \%taxhash );
3366 my %taxhash_elim = %taxhash;
3368 my @elim = qw( taxclass county state );
3369 while ( !scalar(@taxes) && scalar(@elim) ) {
3370 $taxhash_elim{ shift(@elim) } = '';
3371 @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
3374 @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
3376 if $self->cust_main_exemption; #just to be safe
3378 if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
3380 $_->set('pkgnum', $cust_pkg->pkgnum );
3381 $_->set('locationnum', $cust_pkg->locationnum );
3385 $taxes{''} = [ @taxes ];
3386 $taxes{'setup'} = [ @taxes ];
3387 $taxes{'recur'} = [ @taxes ];
3388 $taxes{$_} = [ @taxes ] foreach (@classes);
3390 # # maybe eliminate this entirely, along with all the 0% records
3391 # unless ( @taxes ) {
3393 # "fatal: can't find tax rate for state/county/country/taxclass ".
3394 # join('/', map $taxhash{$_}, qw(state county country taxclass) );
3397 } #if $conf->exists('enable_taxproducts') ...
3402 my $separate = $conf->exists('separate_usage');
3403 my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
3404 my $usage_mandate = $temp_pkg->part_pkg->option('usage_mandate', 'Hush!');
3405 my $section = $temp_pkg->part_pkg->categoryname;
3406 if ( $separate || $section || $usage_mandate ) {
3408 my %hash = ( 'section' => $section );
3410 $section = $temp_pkg->part_pkg->option('usage_section', 'Hush!');
3411 my $summary = $temp_pkg->part_pkg->option('summarize_usage', 'Hush!');
3413 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
3414 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
3416 push @display, new FS::cust_bill_pkg_display
3419 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
3423 if ($separate && $section && $summary) {
3424 push @display, new FS::cust_bill_pkg_display { type => 'U',
3429 if ($usage_mandate || $section && $summary) {
3430 $hash{post_total} = 'Y';
3433 if ($separate || $usage_mandate) {
3434 $hash{section} = $section if ($separate || $usage_mandate);
3435 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
3439 $cust_bill_pkg->set('display', \@display);
3441 my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
3442 foreach my $key (keys %tax_cust_bill_pkg) {
3443 my @taxes = @{ $taxes{$key} || [] };
3444 my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
3446 my %localtaxlisthash = ();
3447 foreach my $tax ( @taxes ) {
3449 my $taxname = ref( $tax ). ' '. $tax->taxnum;
3450 # $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
3451 # ' locationnum'. $cust_pkg->locationnum
3452 # if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
3454 $taxlisthash->{ $taxname } ||= [ $tax ];
3455 push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
3457 $localtaxlisthash{ $taxname } ||= [ $tax ];
3458 push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
3462 warn "finding taxed taxes...\n" if $DEBUG > 2;
3463 foreach my $tax ( keys %localtaxlisthash ) {
3464 my $tax_object = shift @{ $localtaxlisthash{$tax} };
3465 warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
3467 next unless $tax_object->can('tax_on_tax');
3469 foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
3470 my $totname = ref( $tot ). ' '. $tot->taxnum;
3472 warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
3474 next unless exists( $localtaxlisthash{ $totname } ); # only increase
3476 warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
3477 my $hashref_or_error =
3478 $tax_object->taxline( $localtaxlisthash{$tax},
3479 'custnum' => $self->custnum,
3480 'invoice_time' => $invoice_time,
3482 return $hashref_or_error
3483 unless ref($hashref_or_error);
3485 $taxlisthash->{ $totname } ||= [ $tot ];
3486 push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
3488 # it's silly to have a zero value postal_pkg, but....
3489 @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
3501 my $part_pkg = shift;
3505 my $geocode = $self->geocode('cch');
3507 my @taxclassnums = map { $_->taxclassnum }
3508 $part_pkg->part_pkg_taxoverride($class);
3510 unless (@taxclassnums) {
3511 @taxclassnums = map { $_->taxclassnum }
3512 grep { $_->taxable eq 'Y' }
3513 $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
3515 warn "Found taxclassnum values of ". join(',', @taxclassnums)
3520 join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
3522 @taxes = qsearch({ 'table' => 'tax_rate',
3523 'hashref' => { 'geocode' => $geocode, },
3524 'extra_sql' => $extra_sql,
3526 if scalar(@taxclassnums);
3528 warn "Found taxes ".
3529 join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
3536 =item collect [ HASHREF | OPTION => VALUE ... ]
3538 (Attempt to) collect money for this customer's outstanding invoices (see
3539 L<FS::cust_bill>). Usually used after the bill method.
3541 Actions are now triggered by billing events; see L<FS::part_event> and the
3542 billing events web interface. Old-style invoice events (see
3543 L<FS::part_bill_event>) have been deprecated.
3545 If there is an error, returns the error, otherwise returns false.
3547 Options are passed as name-value pairs.
3549 Currently available options are:
3555 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.
3559 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
3563 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
3567 set true to surpress email card/ACH decline notices.
3571 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)
3577 # allows for one time override of normal customer billing method
3582 my( $self, %options ) = @_;
3583 my $invoice_time = $options{'invoice_time'} || time;
3586 local $SIG{HUP} = 'IGNORE';
3587 local $SIG{INT} = 'IGNORE';
3588 local $SIG{QUIT} = 'IGNORE';
3589 local $SIG{TERM} = 'IGNORE';
3590 local $SIG{TSTP} = 'IGNORE';
3591 local $SIG{PIPE} = 'IGNORE';
3593 my $oldAutoCommit = $FS::UID::AutoCommit;
3594 local $FS::UID::AutoCommit = 0;
3597 $self->select_for_update; #mutex
3600 my $balance = $self->balance;
3601 warn "$me collect customer ". $self->custnum. ": balance $balance\n"
3604 if ( exists($options{'retry_card'}) ) {
3605 carp 'retry_card option passed to collect is deprecated; use retry';
3606 $options{'retry'} ||= $options{'retry_card'};
3608 if ( exists($options{'retry'}) && $options{'retry'} ) {
3609 my $error = $self->retry_realtime;
3611 $dbh->rollback if $oldAutoCommit;
3616 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3618 #never want to roll back an event just because it returned an error
3619 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
3621 $self->do_cust_event(
3622 'debug' => ( $options{'debug'} || 0 ),
3623 'time' => $invoice_time,
3624 'check_freq' => $options{'check_freq'},
3625 'stage' => 'collect',
3630 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
3632 Runs billing events; see L<FS::part_event> and the billing events web
3635 If there is an error, returns the error, otherwise returns false.
3637 Options are passed as name-value pairs.
3639 Currently available options are:
3645 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.
3649 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
3653 "collect" (the default) or "pre-bill"
3657 set true to surpress email card/ACH decline notices.
3661 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)
3667 # allows for one time override of normal customer billing method
3671 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
3674 my( $self, %options ) = @_;
3675 my $time = $options{'time'} || time;
3678 local $SIG{HUP} = 'IGNORE';
3679 local $SIG{INT} = 'IGNORE';
3680 local $SIG{QUIT} = 'IGNORE';
3681 local $SIG{TERM} = 'IGNORE';
3682 local $SIG{TSTP} = 'IGNORE';
3683 local $SIG{PIPE} = 'IGNORE';
3685 my $oldAutoCommit = $FS::UID::AutoCommit;
3686 local $FS::UID::AutoCommit = 0;
3689 $self->select_for_update; #mutex
3692 my $balance = $self->balance;
3693 warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
3696 # if ( exists($options{'retry_card'}) ) {
3697 # carp 'retry_card option passed to collect is deprecated; use retry';
3698 # $options{'retry'} ||= $options{'retry_card'};
3700 # if ( exists($options{'retry'}) && $options{'retry'} ) {
3701 # my $error = $self->retry_realtime;
3703 # $dbh->rollback if $oldAutoCommit;
3708 # false laziness w/pay_batch::import_results
3710 my $due_cust_event = $self->due_cust_event(
3711 'debug' => ( $options{'debug'} || 0 ),
3713 'check_freq' => $options{'check_freq'},
3714 'stage' => ( $options{'stage'} || 'collect' ),
3716 unless( ref($due_cust_event) ) {
3717 $dbh->rollback if $oldAutoCommit;
3718 return $due_cust_event;
3721 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3722 #never want to roll back an event just because it or a different one
3724 local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
3726 foreach my $cust_event ( @$due_cust_event ) {
3730 #re-eval event conditions (a previous event could have changed things)
3731 unless ( $cust_event->test_conditions( 'time' => $time ) ) {
3732 #don't leave stray "new/locked" records around
3733 my $error = $cust_event->delete;
3734 return $error if $error;
3739 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
3740 warn " running cust_event ". $cust_event->eventnum. "\n"
3743 #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
3744 if ( my $error = $cust_event->do_event() ) {
3745 #XXX wtf is this? figure out a proper dealio with return value
3757 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
3759 Inserts database records for and returns an ordered listref of new events due
3760 for this customer, as FS::cust_event objects (see L<FS::cust_event>). If no
3761 events are due, an empty listref is returned. If there is an error, returns a
3762 scalar error message.
3764 To actually run the events, call each event's test_condition method, and if
3765 still true, call the event's do_event method.
3767 Options are passed as a hashref or as a list of name-value pairs. Available
3774 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.
3778 "collect" (the default) or "pre-bill"
3782 "Current time" for the events.
3786 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)
3790 Only return events for the specified eventtable (by default, events of all eventtables are returned)
3794 Explicitly pass the objects to be tested (typically used with eventtable).
3798 Set to true to return the objects, but not actually insert them into the
3805 sub due_cust_event {
3807 my %opt = ref($_[0]) ? %{ $_[0] } : @_;
3810 #my $DEBUG = $opt{'debug'}
3811 local($DEBUG) = $opt{'debug'}
3812 if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
3814 warn "$me due_cust_event called with options ".
3815 join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
3818 $opt{'time'} ||= time;
3820 local $SIG{HUP} = 'IGNORE';
3821 local $SIG{INT} = 'IGNORE';
3822 local $SIG{QUIT} = 'IGNORE';
3823 local $SIG{TERM} = 'IGNORE';
3824 local $SIG{TSTP} = 'IGNORE';
3825 local $SIG{PIPE} = 'IGNORE';
3827 my $oldAutoCommit = $FS::UID::AutoCommit;
3828 local $FS::UID::AutoCommit = 0;
3831 $self->select_for_update #mutex
3832 unless $opt{testonly};
3835 # find possible events (initial search)
3838 my @cust_event = ();
3840 my @eventtable = $opt{'eventtable'}
3841 ? ( $opt{'eventtable'} )
3842 : FS::part_event->eventtables_runorder;
3844 foreach my $eventtable ( @eventtable ) {
3847 if ( $opt{'objects'} ) {
3849 @objects = @{ $opt{'objects'} };
3853 #my @objects = $self->eventtable(); # sub cust_main { @{ [ $self ] }; }
3854 @objects = ( $eventtable eq 'cust_main' )
3856 : ( $self->$eventtable() );
3860 my @e_cust_event = ();
3862 my $cross = "CROSS JOIN $eventtable";
3863 $cross .= ' LEFT JOIN cust_main USING ( custnum )'
3864 unless $eventtable eq 'cust_main';
3866 foreach my $object ( @objects ) {
3868 #this first search uses the condition_sql magic for optimization.
3869 #the more possible events we can eliminate in this step the better
3871 my $cross_where = '';
3872 my $pkey = $object->primary_key;
3873 $cross_where = "$eventtable.$pkey = ". $object->$pkey();
3875 my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
3877 FS::part_event_condition->where_conditions_sql( $eventtable,
3878 'time'=>$opt{'time'}
3880 my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
3882 $extra_sql = "AND $extra_sql" if $extra_sql;
3884 #here is the agent virtualization
3885 $extra_sql .= " AND ( part_event.agentnum IS NULL
3886 OR part_event.agentnum = ". $self->agentnum. ' )';
3888 $extra_sql .= " $order";
3890 warn "searching for events for $eventtable ". $object->$pkey. "\n"
3891 if $opt{'debug'} > 2;
3892 my @part_event = qsearch( {
3893 'debug' => ( $opt{'debug'} > 3 ? 1 : 0 ),
3894 'select' => 'part_event.*',
3895 'table' => 'part_event',
3896 'addl_from' => "$cross $join",
3897 'hashref' => { 'check_freq' => ( $opt{'check_freq'} || '1d' ),
3898 'eventtable' => $eventtable,
3901 'extra_sql' => "AND $cross_where $extra_sql",
3905 my $pkey = $object->primary_key;
3906 warn " ". scalar(@part_event).
3907 " possible events found for $eventtable ". $object->$pkey(). "\n";
3910 push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
3914 warn " ". scalar(@e_cust_event).
3915 " subtotal possible cust events found for $eventtable\n"
3918 push @cust_event, @e_cust_event;
3922 warn " ". scalar(@cust_event).
3923 " total possible cust events found in initial search\n"
3931 $opt{stage} ||= 'collect';
3933 grep { my $stage = $_->part_event->event_stage;
3934 $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
3944 @cust_event = grep $_->test_conditions( 'time' => $opt{'time'},
3945 'stats_hashref' => \%unsat ),
3948 warn " ". scalar(@cust_event). " cust events left satisfying conditions\n"
3951 warn " invalid conditions not eliminated with condition_sql:\n".
3952 join('', map " $_: ".$unsat{$_}."\n", keys %unsat )
3953 if keys %unsat && $DEBUG; # > 1;
3959 unless( $opt{testonly} ) {
3960 foreach my $cust_event ( @cust_event ) {
3962 my $error = $cust_event->insert();
3964 $dbh->rollback if $oldAutoCommit;
3971 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3977 warn " returning events: ". Dumper(@cust_event). "\n"
3984 =item retry_realtime
3986 Schedules realtime / batch credit card / electronic check / LEC billing
3987 events for for retry. Useful if card information has changed or manual
3988 retry is desired. The 'collect' method must be called to actually retry
3991 Implementation details: For either this customer, or for each of this
3992 customer's open invoices, changes the status of the first "done" (with
3993 statustext error) realtime processing event to "failed".
3997 sub retry_realtime {
4000 local $SIG{HUP} = 'IGNORE';
4001 local $SIG{INT} = 'IGNORE';
4002 local $SIG{QUIT} = 'IGNORE';
4003 local $SIG{TERM} = 'IGNORE';
4004 local $SIG{TSTP} = 'IGNORE';
4005 local $SIG{PIPE} = 'IGNORE';
4007 my $oldAutoCommit = $FS::UID::AutoCommit;
4008 local $FS::UID::AutoCommit = 0;
4011 #a little false laziness w/due_cust_event (not too bad, really)
4013 my $join = FS::part_event_condition->join_conditions_sql;
4014 my $order = FS::part_event_condition->order_conditions_sql;
4017 . join ( ' OR ' , map {
4018 "( part_event.eventtable = " . dbh->quote($_)
4019 . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ;
4020 } FS::part_event->eventtables)
4023 #here is the agent virtualization
4024 my $agent_virt = " ( part_event.agentnum IS NULL
4025 OR part_event.agentnum = ". $self->agentnum. ' )';
4027 #XXX this shouldn't be hardcoded, actions should declare it...
4028 my @realtime_events = qw(
4029 cust_bill_realtime_card
4030 cust_bill_realtime_check
4031 cust_bill_realtime_lec
4035 my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'",
4040 my @cust_event = qsearchs({
4041 'table' => 'cust_event',
4042 'select' => 'cust_event.*',
4043 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
4044 'hashref' => { 'status' => 'done' },
4045 'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ".
4046 " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
4049 my %seen_invnum = ();
4050 foreach my $cust_event (@cust_event) {
4052 #max one for the customer, one for each open invoice
4053 my $cust_X = $cust_event->cust_X;
4054 next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
4058 or $cust_event->part_event->eventtable eq 'cust_bill'
4061 my $error = $cust_event->retry;
4063 $dbh->rollback if $oldAutoCommit;
4064 return "error scheduling event for retry: $error";
4069 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4074 # some horrid false laziness here to avoid refactor fallout
4075 # eventually realtime realtime_bop and realtime_refund_bop should go
4076 # away and be replaced by _new_realtime_bop and _new_realtime_refund_bop
4078 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
4080 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
4081 via a Business::OnlinePayment realtime gateway. See
4082 L<http://420.am/business-onlinepayment> for supported gateways.
4084 Available methods are: I<CC>, I<ECHECK> and I<LEC>
4086 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
4088 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
4089 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
4090 if set, will override the value from the customer record.
4092 I<description> is a free-text field passed to the gateway. It defaults to
4093 the value defined by the business-onlinepayment-description configuration
4094 option, or "Internet services" if that is unset.
4096 If an I<invnum> is specified, this payment (if successful) is applied to the
4097 specified invoice. If you don't specify an I<invnum> you might want to
4098 call the B<apply_payments> method or set the I<apply> option.
4100 I<apply> can be set to true to apply a resulting payment.
4102 I<quiet> can be set true to surpress email decline notices.
4104 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
4105 resulting paynum, if any.
4107 I<payunique> is a unique identifier for this payment.
4109 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
4116 return $self->_new_realtime_bop(@_)
4117 if $self->_new_bop_required();
4119 my($method, $amount);
4121 if (ref($_[0]) eq 'HASH') {
4122 %options = %{$_[0]};
4123 $method = $options{method};
4124 $amount = $options{amount};
4126 ( $method, $amount ) = ( shift, shift );
4130 warn "$me realtime_bop: $method $amount\n";
4131 warn " $_ => $options{$_}\n" foreach keys %options;
4134 return "Amount must be greater than 0" unless $amount > 0;
4136 unless ( $options{'description'} ) {
4137 if ( $conf->exists('business-onlinepayment-description') ) {
4138 my $dtempl = $conf->config('business-onlinepayment-description');
4140 my $agent = $self->agent->agent;
4142 $options{'description'} = eval qq("$dtempl");
4144 $options{'description'} = 'Internet services';
4148 return $self->fake_bop($method, $amount, %options) if $options{'fake'};
4150 eval "use Business::OnlinePayment";
4153 my $payinfo = exists($options{'payinfo'})
4154 ? $options{'payinfo'}
4157 my %method2payby = (
4164 # check for banned credit card/ACH
4167 my $ban = qsearchs('banned_pay', {
4168 'payby' => $method2payby{$method},
4169 'payinfo' => md5_base64($payinfo),
4171 return "Banned credit card" if $ban;
4174 # set taxclass and trans_is_recur based on invnum if there is one
4178 my $trans_is_recur = 0;
4179 if ( $options{'invnum'} ) {
4181 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
4182 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
4185 map { $_->part_pkg }
4187 map { $_->cust_pkg }
4188 $cust_bill->cust_bill_pkg;
4190 my @taxclasses = map $_->taxclass, @part_pkg;
4191 $taxclass = $taxclasses[0]
4192 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
4193 #different taxclasses
4195 if grep { $_->freq ne '0' } @part_pkg;
4203 #look for an agent gateway override first
4205 if ( $method eq 'CC' ) {
4206 $cardtype = cardtype($payinfo);
4207 } elsif ( $method eq 'ECHECK' ) {
4210 $cardtype = $method;
4214 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4215 cardtype => $cardtype,
4216 taxclass => $taxclass, } )
4217 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4219 taxclass => $taxclass, } )
4220 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4221 cardtype => $cardtype,
4223 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4225 taxclass => '', } );
4227 my $payment_gateway = '';
4228 my( $processor, $login, $password, $action, @bop_options );
4229 if ( $override ) { #use a payment gateway override
4231 $payment_gateway = $override->payment_gateway;
4233 $processor = $payment_gateway->gateway_module;
4234 $login = $payment_gateway->gateway_username;
4235 $password = $payment_gateway->gateway_password;
4236 $action = $payment_gateway->gateway_action;
4237 @bop_options = $payment_gateway->options;
4239 } else { #use the standard settings from the config
4241 ( $processor, $login, $password, $action, @bop_options ) =
4242 $self->default_payment_gateway($method);
4250 my $address = exists($options{'address1'})
4251 ? $options{'address1'}
4253 my $address2 = exists($options{'address2'})
4254 ? $options{'address2'}
4256 $address .= ", ". $address2 if length($address2);
4258 my $o_payname = exists($options{'payname'})
4259 ? $options{'payname'}
4261 my($payname, $payfirst, $paylast);
4262 if ( $o_payname && $method ne 'ECHECK' ) {
4263 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
4264 or return "Illegal payname $payname";
4265 ($payfirst, $paylast) = ($1, $2);
4267 $payfirst = $self->getfield('first');
4268 $paylast = $self->getfield('last');
4269 $payname = "$payfirst $paylast";
4272 my @invoicing_list = $self->invoicing_list_emailonly;
4273 if ( $conf->exists('emailinvoiceautoalways')
4274 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
4275 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
4276 push @invoicing_list, $self->all_emails;
4279 my $email = ($conf->exists('business-onlinepayment-email-override'))
4280 ? $conf->config('business-onlinepayment-email-override')
4281 : $invoicing_list[0];
4285 my $payip = exists($options{'payip'})
4288 $content{customer_ip} = $payip
4291 $content{invoice_number} = $options{'invnum'}
4292 if exists($options{'invnum'}) && length($options{'invnum'});
4294 $content{email_customer} =
4295 ( $conf->exists('business-onlinepayment-email_customer')
4296 || $conf->exists('business-onlinepayment-email-override') );
4299 if ( $method eq 'CC' ) {
4301 $content{card_number} = $payinfo;
4302 $paydate = exists($options{'paydate'})
4303 ? $options{'paydate'}
4305 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
4306 $content{expiration} = "$2/$1";
4308 my $paycvv = exists($options{'paycvv'})
4309 ? $options{'paycvv'}
4311 $content{cvv2} = $paycvv
4314 my $paystart_month = exists($options{'paystart_month'})
4315 ? $options{'paystart_month'}
4316 : $self->paystart_month;
4318 my $paystart_year = exists($options{'paystart_year'})
4319 ? $options{'paystart_year'}
4320 : $self->paystart_year;
4322 $content{card_start} = "$paystart_month/$paystart_year"
4323 if $paystart_month && $paystart_year;
4325 my $payissue = exists($options{'payissue'})
4326 ? $options{'payissue'}
4328 $content{issue_number} = $payissue if $payissue;
4330 if ( $self->_bop_recurring_billing( 'payinfo' => $payinfo,
4331 'trans_is_recur' => $trans_is_recur,
4335 $content{recurring_billing} = 'YES';
4336 $content{acct_code} = 'rebill'
4337 if $conf->exists('credit_card-recurring_billing_acct_code');
4340 } elsif ( $method eq 'ECHECK' ) {
4341 ( $content{account_number}, $content{routing_code} ) =
4342 split('@', $payinfo);
4343 $content{bank_name} = $o_payname;
4344 $content{bank_state} = exists($options{'paystate'})
4345 ? $options{'paystate'}
4346 : $self->getfield('paystate');
4347 $content{account_type} = exists($options{'paytype'})
4348 ? uc($options{'paytype'}) || 'CHECKING'
4349 : uc($self->getfield('paytype')) || 'CHECKING';
4350 $content{account_name} = $payname;
4351 $content{customer_org} = $self->company ? 'B' : 'I';
4352 $content{state_id} = exists($options{'stateid'})
4353 ? $options{'stateid'}
4354 : $self->getfield('stateid');
4355 $content{state_id_state} = exists($options{'stateid_state'})
4356 ? $options{'stateid_state'}
4357 : $self->getfield('stateid_state');
4358 $content{customer_ssn} = exists($options{'ss'})
4361 } elsif ( $method eq 'LEC' ) {
4362 $content{phone} = $payinfo;
4366 # run transaction(s)
4369 my $balance = exists( $options{'balance'} )
4370 ? $options{'balance'}
4373 $self->select_for_update; #mutex ... just until we get our pending record in
4375 #the checks here are intended to catch concurrent payments
4376 #double-form-submission prevention is taken care of in cust_pay_pending::check
4379 return "The customer's balance has changed; $method transaction aborted."
4380 if $self->balance < $balance;
4381 #&& $self->balance < $amount; #might as well anyway?
4383 #also check and make sure there aren't *other* pending payments for this cust
4385 my @pending = qsearch('cust_pay_pending', {
4386 'custnum' => $self->custnum,
4387 'status' => { op=>'!=', value=>'done' }
4389 return "A payment is already being processed for this customer (".
4390 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
4391 "); $method transaction aborted."
4392 if scalar(@pending);
4394 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
4396 my $cust_pay_pending = new FS::cust_pay_pending {
4397 'custnum' => $self->custnum,
4398 #'invnum' => $options{'invnum'},
4401 'payby' => $method2payby{$method},
4402 'payinfo' => $payinfo,
4403 'paydate' => $paydate,
4404 'recurring_billing' => $content{recurring_billing},
4405 'pkgnum' => $options{'pkgnum'},
4407 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
4409 $cust_pay_pending->payunique( $options{payunique} )
4410 if defined($options{payunique}) && length($options{payunique});
4411 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
4412 return $cpp_new_err if $cpp_new_err;
4414 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
4416 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
4417 $transaction->content(
4420 'password' => $password,
4421 'action' => $action1,
4422 'description' => $options{'description'},
4423 'amount' => $amount,
4424 #'invoice_number' => $options{'invnum'},
4425 'customer_id' => $self->custnum,
4426 'last_name' => $paylast,
4427 'first_name' => $payfirst,
4429 'address' => $address,
4430 'city' => ( exists($options{'city'})
4433 'state' => ( exists($options{'state'})
4436 'zip' => ( exists($options{'zip'})
4439 'country' => ( exists($options{'country'})
4440 ? $options{'country'}
4442 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
4444 'phone' => $self->daytime || $self->night,
4448 $cust_pay_pending->status('pending');
4449 my $cpp_pending_err = $cust_pay_pending->replace;
4450 return $cpp_pending_err if $cpp_pending_err;
4453 my $BOP_TESTING = 0;
4454 my $BOP_TESTING_SUCCESS = 1;
4456 unless ( $BOP_TESTING ) {
4457 $transaction->submit();
4459 if ( $BOP_TESTING_SUCCESS ) {
4460 $transaction->is_success(1);
4461 $transaction->authorization('fake auth');
4463 $transaction->is_success(0);
4464 $transaction->error_message('fake failure');
4468 if ( $transaction->is_success() && $action2 ) {
4470 $cust_pay_pending->status('authorized');
4471 my $cpp_authorized_err = $cust_pay_pending->replace;
4472 return $cpp_authorized_err if $cpp_authorized_err;
4474 my $auth = $transaction->authorization;
4475 my $ordernum = $transaction->can('order_number')
4476 ? $transaction->order_number
4480 new Business::OnlinePayment( $processor, @bop_options );
4487 password => $password,
4488 order_number => $ordernum,
4490 authorization => $auth,
4491 description => $options{'description'},
4494 foreach my $field (qw( authorization_source_code returned_ACI
4495 transaction_identifier validation_code
4496 transaction_sequence_num local_transaction_date
4497 local_transaction_time AVS_result_code )) {
4498 $capture{$field} = $transaction->$field() if $transaction->can($field);
4501 $capture->content( %capture );
4505 unless ( $capture->is_success ) {
4506 my $e = "Authorization successful but capture failed, custnum #".
4507 $self->custnum. ': '. $capture->result_code.
4508 ": ". $capture->error_message;
4515 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
4516 my $cpp_captured_err = $cust_pay_pending->replace;
4517 return $cpp_captured_err if $cpp_captured_err;
4520 # remove paycvv after initial transaction
4523 #false laziness w/misc/process/payment.cgi - check both to make sure working
4525 if ( defined $self->dbdef_table->column('paycvv')
4526 && length($self->paycvv)
4527 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
4529 my $error = $self->remove_cvv;
4531 warn "WARNING: error removing cvv: $error\n";
4539 if ( $transaction->is_success() ) {
4542 if ( $payment_gateway ) { # agent override
4543 $paybatch = $payment_gateway->gatewaynum. '-';
4546 $paybatch .= "$processor:". $transaction->authorization;
4548 $paybatch .= ':'. $transaction->order_number
4549 if $transaction->can('order_number')
4550 && length($transaction->order_number);
4552 my $cust_pay = new FS::cust_pay ( {
4553 'custnum' => $self->custnum,
4554 'invnum' => $options{'invnum'},
4557 'payby' => $method2payby{$method},
4558 'payinfo' => $payinfo,
4559 'paybatch' => $paybatch,
4560 'paydate' => $paydate,
4561 'pkgnum' => $options{'pkgnum'},
4563 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
4564 $cust_pay->payunique( $options{payunique} )
4565 if defined($options{payunique}) && length($options{payunique});
4567 my $oldAutoCommit = $FS::UID::AutoCommit;
4568 local $FS::UID::AutoCommit = 0;
4571 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
4573 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
4576 $cust_pay->invnum(''); #try again with no specific invnum
4577 my $error2 = $cust_pay->insert( $options{'manual'} ?
4578 ( 'manual' => 1 ) : ()
4581 # gah. but at least we have a record of the state we had to abort in
4582 # from cust_pay_pending now.
4583 my $e = "WARNING: $method captured but payment not recorded - ".
4584 "error inserting payment ($processor): $error2".
4585 " (previously tried insert with invnum #$options{'invnum'}" .
4586 ": $error ) - pending payment saved as paypendingnum ".
4587 $cust_pay_pending->paypendingnum. "\n";
4593 if ( $options{'paynum_ref'} ) {
4594 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
4597 $cust_pay_pending->status('done');
4598 $cust_pay_pending->statustext('captured');
4599 $cust_pay_pending->paynum($cust_pay->paynum);
4600 my $cpp_done_err = $cust_pay_pending->replace;
4602 if ( $cpp_done_err ) {
4604 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
4605 my $e = "WARNING: $method captured but payment not recorded - ".
4606 "error updating status for paypendingnum ".
4607 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
4613 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4615 if ( $options{'apply'} ) {
4616 my $apply_error = $self->apply_payments_and_credits;
4617 if ( $apply_error ) {
4618 warn "WARNING: error applying payment: $apply_error\n";
4619 #but we still should return no error cause the payment otherwise went
4624 return ''; #no error
4630 my $perror = "$processor error: ". $transaction->error_message;
4632 unless ( $transaction->error_message ) {
4635 if ( $transaction->can('response_page') ) {
4637 'page' => ( $transaction->can('response_page')
4638 ? $transaction->response_page
4641 'code' => ( $transaction->can('response_code')
4642 ? $transaction->response_code
4645 'headers' => ( $transaction->can('response_headers')
4646 ? $transaction->response_headers
4652 "No additional debugging information available for $processor";
4655 $perror .= "No error_message returned from $processor -- ".
4656 ( ref($t_response) ? Dumper($t_response) : $t_response );
4660 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
4661 && $conf->exists('emaildecline')
4662 && grep { $_ ne 'POST' } $self->invoicing_list
4663 && ! grep { $transaction->error_message =~ /$_/ }
4664 $conf->config('emaildecline-exclude')
4667 # Send a decline alert to the customer.
4668 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
4671 # include the raw error message in the transaction state
4672 $cust_pay_pending->setfield('error', $transaction->error_message);
4673 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
4674 $error = $msg_template->send( 'cust_main' => $self,
4675 'object' => $cust_pay_pending );
4679 my @templ = $conf->config('declinetemplate');
4680 my $template = new Text::Template (
4682 SOURCE => [ map "$_\n", @templ ],
4683 ) or return "($perror) can't create template: $Text::Template::ERROR";
4684 $template->compile()
4685 or return "($perror) can't compile template: $Text::Template::ERROR";
4689 scalar( $conf->config('company_name', $self->agentnum ) ),
4690 'company_address' =>
4691 join("\n", $conf->config('company_address', $self->agentnum ) ),
4692 'error' => $transaction->error_message,
4695 my $error = send_email(
4696 'from' => $conf->config('invoice_from', $self->agentnum ),
4697 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
4698 'subject' => 'Your payment could not be processed',
4699 'body' => [ $template->fill_in(HASH => $templ_hash) ],
4703 $perror .= " (also received error sending decline notification: $error)"
4708 $cust_pay_pending->status('done');
4709 $cust_pay_pending->statustext("declined: $perror");
4710 my $cpp_done_err = $cust_pay_pending->replace;
4711 if ( $cpp_done_err ) {
4712 my $e = "WARNING: $method declined but pending payment not resolved - ".
4713 "error updating status for paypendingnum ".
4714 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
4716 $perror = "$e ($perror)";
4724 sub _bop_recurring_billing {
4725 my( $self, %opt ) = @_;
4727 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
4729 if ( defined($method) && $method eq 'transaction_is_recur' ) {
4731 return 1 if $opt{'trans_is_recur'};
4735 my %hash = ( 'custnum' => $self->custnum,
4740 if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
4741 || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
4752 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
4754 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
4755 via a Business::OnlinePayment realtime gateway. See
4756 L<http://420.am/business-onlinepayment> for supported gateways.
4758 Available methods are: I<CC>, I<ECHECK> and I<LEC>
4760 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
4762 Most gateways require a reference to an original payment transaction to refund,
4763 so you probably need to specify a I<paynum>.
4765 I<amount> defaults to the original amount of the payment if not specified.
4767 I<reason> specifies a reason for the refund.
4769 I<paydate> specifies the expiration date for a credit card overriding the
4770 value from the customer record or the payment record. Specified as yyyy-mm-dd
4772 Implementation note: If I<amount> is unspecified or equal to the amount of the
4773 orignal payment, first an attempt is made to "void" the transaction via
4774 the gateway (to cancel a not-yet settled transaction) and then if that fails,
4775 the normal attempt is made to "refund" ("credit") the transaction via the
4776 gateway is attempted.
4778 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
4779 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
4780 #if set, will override the value from the customer record.
4782 #If an I<invnum> is specified, this payment (if successful) is applied to the
4783 #specified invoice. If you don't specify an I<invnum> you might want to
4784 #call the B<apply_payments> method.
4788 #some false laziness w/realtime_bop, not enough to make it worth merging
4789 #but some useful small subs should be pulled out
4790 sub realtime_refund_bop {
4793 return $self->_new_realtime_refund_bop(@_)
4794 if $self->_new_bop_required();
4796 my( $method, %options ) = @_;
4798 warn "$me realtime_refund_bop: $method refund\n";
4799 warn " $_ => $options{$_}\n" foreach keys %options;
4802 eval "use Business::OnlinePayment";
4806 # look up the original payment and optionally a gateway for that payment
4810 my $amount = $options{'amount'};
4812 my( $processor, $login, $password, @bop_options ) ;
4813 my( $auth, $order_number ) = ( '', '', '' );
4815 if ( $options{'paynum'} ) {
4817 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
4818 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
4819 or return "Unknown paynum $options{'paynum'}";
4820 $amount ||= $cust_pay->paid;
4822 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
4823 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
4824 $cust_pay->paybatch;
4825 my $gatewaynum = '';
4826 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
4828 if ( $gatewaynum ) { #gateway for the payment to be refunded
4830 my $payment_gateway =
4831 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
4832 die "payment gateway $gatewaynum not found"
4833 unless $payment_gateway;
4835 $processor = $payment_gateway->gateway_module;
4836 $login = $payment_gateway->gateway_username;
4837 $password = $payment_gateway->gateway_password;
4838 @bop_options = $payment_gateway->options;
4840 } else { #try the default gateway
4842 my( $conf_processor, $unused_action );
4843 ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
4844 $self->default_payment_gateway($method);
4846 return "processor of payment $options{'paynum'} $processor does not".
4847 " match default processor $conf_processor"
4848 unless $processor eq $conf_processor;
4853 } else { # didn't specify a paynum, so look for agent gateway overrides
4854 # like a normal transaction
4857 if ( $method eq 'CC' ) {
4858 $cardtype = cardtype($self->payinfo);
4859 } elsif ( $method eq 'ECHECK' ) {
4862 $cardtype = $method;
4865 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4866 cardtype => $cardtype,
4868 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4870 taxclass => '', } );
4872 if ( $override ) { #use a payment gateway override
4874 my $payment_gateway = $override->payment_gateway;
4876 $processor = $payment_gateway->gateway_module;
4877 $login = $payment_gateway->gateway_username;
4878 $password = $payment_gateway->gateway_password;
4879 #$action = $payment_gateway->gateway_action;
4880 @bop_options = $payment_gateway->options;
4882 } else { #use the standard settings from the config
4885 ( $processor, $login, $password, $unused_action, @bop_options ) =
4886 $self->default_payment_gateway($method);
4891 return "neither amount nor paynum specified" unless $amount;
4896 'password' => $password,
4897 'order_number' => $order_number,
4898 'amount' => $amount,
4899 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
4901 $content{authorization} = $auth
4902 if length($auth); #echeck/ACH transactions have an order # but no auth
4903 #(at least with authorize.net)
4905 my $disable_void_after;
4906 if ($conf->exists('disable_void_after')
4907 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
4908 $disable_void_after = $1;
4911 #first try void if applicable
4912 if ( $cust_pay && $cust_pay->paid == $amount
4914 ( not defined($disable_void_after) )
4915 || ( time < ($cust_pay->_date + $disable_void_after ) )
4918 warn " attempting void\n" if $DEBUG > 1;
4919 my $void = new Business::OnlinePayment( $processor, @bop_options );
4920 if ( $void->can('info') ) {
4921 if ( $cust_pay->payby eq 'CARD'
4922 && $void->info('CC_void_requires_card') )
4924 $content{'card_number'} = $cust_pay->payinfo
4925 } elsif ( $cust_pay->payby eq 'CHEK'
4926 && $void->info('ECHECK_void_requires_account') )
4928 ( $content{'account_number'}, $content{'routing_code'} ) =
4929 split('@', $cust_pay->payinfo);
4930 $content{'name'} = $self->get('first'). ' '. $self->get('last');
4933 $void->content( 'action' => 'void', %content );
4935 if ( $void->is_success ) {
4936 my $error = $cust_pay->void($options{'reason'});
4938 # gah, even with transactions.
4939 my $e = 'WARNING: Card/ACH voided but database not updated - '.
4940 "error voiding payment: $error";
4944 warn " void successful\n" if $DEBUG > 1;
4949 warn " void unsuccessful, trying refund\n"
4953 my $address = $self->address1;
4954 $address .= ", ". $self->address2 if $self->address2;
4956 my($payname, $payfirst, $paylast);
4957 if ( $self->payname && $method ne 'ECHECK' ) {
4958 $payname = $self->payname;
4959 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
4960 or return "Illegal payname $payname";
4961 ($payfirst, $paylast) = ($1, $2);
4963 $payfirst = $self->getfield('first');
4964 $paylast = $self->getfield('last');
4965 $payname = "$payfirst $paylast";
4968 my @invoicing_list = $self->invoicing_list_emailonly;
4969 if ( $conf->exists('emailinvoiceautoalways')
4970 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
4971 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
4972 push @invoicing_list, $self->all_emails;
4975 my $email = ($conf->exists('business-onlinepayment-email-override'))
4976 ? $conf->config('business-onlinepayment-email-override')
4977 : $invoicing_list[0];
4979 my $payip = exists($options{'payip'})
4982 $content{customer_ip} = $payip
4986 if ( $method eq 'CC' ) {
4989 $content{card_number} = $payinfo = $cust_pay->payinfo;
4990 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
4991 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
4992 ($content{expiration} = "$2/$1"); # where available
4994 $content{card_number} = $payinfo = $self->payinfo;
4995 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
4996 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
4997 $content{expiration} = "$2/$1";
5000 } elsif ( $method eq 'ECHECK' ) {
5003 $payinfo = $cust_pay->payinfo;
5005 $payinfo = $self->payinfo;
5007 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
5008 $content{bank_name} = $self->payname;
5009 $content{account_type} = 'CHECKING';
5010 $content{account_name} = $payname;
5011 $content{customer_org} = $self->company ? 'B' : 'I';
5012 $content{customer_ssn} = $self->ss;
5013 } elsif ( $method eq 'LEC' ) {
5014 $content{phone} = $payinfo = $self->payinfo;
5018 my $refund = new Business::OnlinePayment( $processor, @bop_options );
5019 my %sub_content = $refund->content(
5020 'action' => 'credit',
5021 'customer_id' => $self->custnum,
5022 'last_name' => $paylast,
5023 'first_name' => $payfirst,
5025 'address' => $address,
5026 'city' => $self->city,
5027 'state' => $self->state,
5028 'zip' => $self->zip,
5029 'country' => $self->country,
5031 'phone' => $self->daytime || $self->night,
5034 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
5038 return "$processor error: ". $refund->error_message
5039 unless $refund->is_success();
5041 my %method2payby = (
5047 my $paybatch = "$processor:". $refund->authorization;
5048 $paybatch .= ':'. $refund->order_number
5049 if $refund->can('order_number') && $refund->order_number;
5051 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
5052 my @cust_bill_pay = $cust_pay->cust_bill_pay;
5053 last unless @cust_bill_pay;
5054 my $cust_bill_pay = pop @cust_bill_pay;
5055 my $error = $cust_bill_pay->delete;
5059 my $cust_refund = new FS::cust_refund ( {
5060 'custnum' => $self->custnum,
5061 'paynum' => $options{'paynum'},
5062 'refund' => $amount,
5064 'payby' => $method2payby{$method},
5065 'payinfo' => $payinfo,
5066 'paybatch' => $paybatch,
5067 'reason' => $options{'reason'} || 'card or ACH refund',
5069 my $error = $cust_refund->insert;
5071 $cust_refund->paynum(''); #try again with no specific paynum
5072 my $error2 = $cust_refund->insert;
5074 # gah, even with transactions.
5075 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
5076 "error inserting refund ($processor): $error2".
5077 " (previously tried insert with paynum #$options{'paynum'}" .
5088 # does the configuration indicate the new bop routines are required?
5090 sub _new_bop_required {
5093 my $botpp = 'Business::OnlineThirdPartyPayment';
5096 if ( ( $conf->exists('business-onlinepayment-namespace')
5097 && $conf->config('business-onlinepayment-namespace') eq $botpp
5099 or scalar( grep { $_->gateway_namespace eq $botpp }
5100 qsearch( 'payment_gateway', { 'disabled' => '' } )
5108 =item realtime_collect [ OPTION => VALUE ... ]
5110 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
5111 via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime
5112 gateway. See L<http://420.am/business-onlinepayment> and
5113 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
5115 On failure returns an error message.
5117 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.
5119 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
5121 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
5122 then it is deduced from the customer record.
5124 If no I<amount> is specified, then the customer balance is used.
5126 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
5127 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
5128 if set, will override the value from the customer record.
5130 I<description> is a free-text field passed to the gateway. It defaults to
5131 the value defined by the business-onlinepayment-description configuration
5132 option, or "Internet services" if that is unset.
5134 If an I<invnum> is specified, this payment (if successful) is applied to the
5135 specified invoice. If you don't specify an I<invnum> you might want to
5136 call the B<apply_payments> method or set the I<apply> option.
5138 I<apply> can be set to true to apply a resulting payment.
5140 I<quiet> can be set true to surpress email decline notices.
5142 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
5143 resulting paynum, if any.
5145 I<payunique> is a unique identifier for this payment.
5147 I<session_id> is a session identifier associated with this payment.
5149 I<depend_jobnum> allows payment capture to unlock export jobs
5153 sub realtime_collect {
5154 my( $self, %options ) = @_;
5157 warn "$me realtime_collect:\n";
5158 warn " $_ => $options{$_}\n" foreach keys %options;
5161 $options{amount} = $self->balance unless exists( $options{amount} );
5162 $options{method} = FS::payby->payby2bop($self->payby)
5163 unless exists( $options{method} );
5165 return $self->realtime_bop({%options});
5169 =item _realtime_bop { [ ARG => VALUE ... ] }
5171 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
5172 via a Business::OnlinePayment realtime gateway. See
5173 L<http://420.am/business-onlinepayment> for supported gateways.
5175 Required arguments in the hashref are I<method>, and I<amount>
5177 Available methods are: I<CC>, I<ECHECK> and I<LEC>
5179 Available optional arguments are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
5181 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
5182 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
5183 if set, will override the value from the customer record.
5185 I<description> is a free-text field passed to the gateway. It defaults to
5186 the value defined by the business-onlinepayment-description configuration
5187 option, or "Internet services" if that is unset.
5189 If an I<invnum> is specified, this payment (if successful) is applied to the
5190 specified invoice. If you don't specify an I<invnum> you might want to
5191 call the B<apply_payments> method.
5193 I<quiet> can be set true to surpress email decline notices.
5195 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
5196 resulting paynum, if any.
5198 I<payunique> is a unique identifier for this payment.
5200 I<session_id> is a session identifier associated with this payment.
5202 I<depend_jobnum> allows payment capture to unlock export jobs
5204 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
5208 # some helper routines
5209 sub _payment_gateway {
5210 my ($self, $options) = @_;
5212 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
5213 unless exists($options->{payment_gateway});
5215 $options->{payment_gateway};
5219 my ($self, $options) = @_;
5222 'login' => $options->{payment_gateway}->gateway_username,
5223 'password' => $options->{payment_gateway}->gateway_password,
5228 my ($self, $options) = @_;
5230 $options->{payment_gateway}->gatewaynum
5231 ? $options->{payment_gateway}->options
5232 : @{ $options->{payment_gateway}->get('options') };
5236 my ($self, $options) = @_;
5238 unless ( $options->{'description'} ) {
5239 if ( $conf->exists('business-onlinepayment-description') ) {
5240 my $dtempl = $conf->config('business-onlinepayment-description');
5242 my $agent = $self->agent->agent;
5244 $options->{'description'} = eval qq("$dtempl");
5246 $options->{'description'} = 'Internet services';
5250 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
5251 $options->{invnum} ||= '';
5252 $options->{payname} = $self->payname unless exists( $options->{payname} );
5256 my ($self, $options) = @_;
5259 $content{address} = exists($options->{'address1'})
5260 ? $options->{'address1'}
5262 my $address2 = exists($options->{'address2'})
5263 ? $options->{'address2'}
5265 $content{address} .= ", ". $address2 if length($address2);
5267 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
5268 $content{customer_ip} = $payip if length($payip);
5270 $content{invoice_number} = $options->{'invnum'}
5271 if exists($options->{'invnum'}) && length($options->{'invnum'});
5273 $content{email_customer} =
5274 ( $conf->exists('business-onlinepayment-email_customer')
5275 || $conf->exists('business-onlinepayment-email-override') );
5277 $content{payfirst} = $self->getfield('first');
5278 $content{paylast} = $self->getfield('last');
5280 $content{account_name} = "$content{payfirst} $content{paylast}"
5281 if $options->{method} eq 'ECHECK';
5283 $content{name} = $options->{payname};
5284 $content{name} = $content{account_name} if exists($content{account_name});
5286 $content{city} = exists($options->{city})
5289 $content{state} = exists($options->{state})
5292 $content{zip} = exists($options->{zip})
5295 $content{country} = exists($options->{country})
5296 ? $options->{country}
5298 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
5299 $content{phone} = $self->daytime || $self->night;
5304 my %bop_method2payby = (
5310 sub _new_realtime_bop {
5314 if (ref($_[0]) eq 'HASH') {
5315 %options = %{$_[0]};
5317 my ( $method, $amount ) = ( shift, shift );
5319 $options{method} = $method;
5320 $options{amount} = $amount;
5324 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
5325 warn " $_ => $options{$_}\n" foreach keys %options;
5328 return $self->fake_bop(%options) if $options{'fake'};
5330 $self->_bop_defaults(\%options);
5333 # set trans_is_recur based on invnum if there is one
5336 my $trans_is_recur = 0;
5337 if ( $options{'invnum'} ) {
5339 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
5340 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
5343 map { $_->part_pkg }
5345 map { $_->cust_pkg }
5346 $cust_bill->cust_bill_pkg;
5349 if grep { $_->freq ne '0' } @part_pkg;
5357 my $payment_gateway = $self->_payment_gateway( \%options );
5358 my $namespace = $payment_gateway->gateway_namespace;
5360 eval "use $namespace";
5364 # check for banned credit card/ACH
5367 my $ban = qsearchs('banned_pay', {
5368 'payby' => $bop_method2payby{$options{method}},
5369 'payinfo' => md5_base64($options{payinfo}),
5371 return "Banned credit card" if $ban;
5377 my (%bop_content) = $self->_bop_content(\%options);
5379 if ( $options{method} ne 'ECHECK' ) {
5380 $options{payname} =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
5381 or return "Illegal payname $options{payname}";
5382 ($bop_content{payfirst}, $bop_content{paylast}) = ($1, $2);
5385 my @invoicing_list = $self->invoicing_list_emailonly;
5386 if ( $conf->exists('emailinvoiceautoalways')
5387 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
5388 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
5389 push @invoicing_list, $self->all_emails;
5392 my $email = ($conf->exists('business-onlinepayment-email-override'))
5393 ? $conf->config('business-onlinepayment-email-override')
5394 : $invoicing_list[0];
5398 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
5400 $content{card_number} = $options{payinfo};
5401 $paydate = exists($options{'paydate'})
5402 ? $options{'paydate'}
5404 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
5405 $content{expiration} = "$2/$1";
5407 my $paycvv = exists($options{'paycvv'})
5408 ? $options{'paycvv'}
5410 $content{cvv2} = $paycvv
5413 my $paystart_month = exists($options{'paystart_month'})
5414 ? $options{'paystart_month'}
5415 : $self->paystart_month;
5417 my $paystart_year = exists($options{'paystart_year'})
5418 ? $options{'paystart_year'}
5419 : $self->paystart_year;
5421 $content{card_start} = "$paystart_month/$paystart_year"
5422 if $paystart_month && $paystart_year;
5424 my $payissue = exists($options{'payissue'})
5425 ? $options{'payissue'}
5427 $content{issue_number} = $payissue if $payissue;
5429 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
5430 'trans_is_recur' => $trans_is_recur,
5434 $content{recurring_billing} = 'YES';
5435 $content{acct_code} = 'rebill'
5436 if $conf->exists('credit_card-recurring_billing_acct_code');
5439 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
5440 ( $content{account_number}, $content{routing_code} ) =
5441 split('@', $options{payinfo});
5442 $content{bank_name} = $options{payname};
5443 $content{bank_state} = exists($options{'paystate'})
5444 ? $options{'paystate'}
5445 : $self->getfield('paystate');
5446 $content{account_type} = exists($options{'paytype'})
5447 ? uc($options{'paytype'}) || 'CHECKING'
5448 : uc($self->getfield('paytype')) || 'CHECKING';
5449 $content{customer_org} = $self->company ? 'B' : 'I';
5450 $content{state_id} = exists($options{'stateid'})
5451 ? $options{'stateid'}
5452 : $self->getfield('stateid');
5453 $content{state_id_state} = exists($options{'stateid_state'})
5454 ? $options{'stateid_state'}
5455 : $self->getfield('stateid_state');
5456 $content{customer_ssn} = exists($options{'ss'})
5459 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
5460 $content{phone} = $options{payinfo};
5461 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
5468 # run transaction(s)
5471 my $balance = exists( $options{'balance'} )
5472 ? $options{'balance'}
5475 $self->select_for_update; #mutex ... just until we get our pending record in
5477 #the checks here are intended to catch concurrent payments
5478 #double-form-submission prevention is taken care of in cust_pay_pending::check
5481 return "The customer's balance has changed; $options{method} transaction aborted."
5482 if $self->balance < $balance;
5483 #&& $self->balance < $options{amount}; #might as well anyway?
5485 #also check and make sure there aren't *other* pending payments for this cust
5487 my @pending = qsearch('cust_pay_pending', {
5488 'custnum' => $self->custnum,
5489 'status' => { op=>'!=', value=>'done' }
5491 return "A payment is already being processed for this customer (".
5492 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
5493 "); $options{method} transaction aborted."
5494 if scalar(@pending);
5496 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
5498 my $cust_pay_pending = new FS::cust_pay_pending {
5499 'custnum' => $self->custnum,
5500 #'invnum' => $options{'invnum'},
5501 'paid' => $options{amount},
5503 'payby' => $bop_method2payby{$options{method}},
5504 'payinfo' => $options{payinfo},
5505 'paydate' => $paydate,
5506 'recurring_billing' => $content{recurring_billing},
5507 'pkgnum' => $options{'pkgnum'},
5509 'gatewaynum' => $payment_gateway->gatewaynum || '',
5510 'session_id' => $options{session_id} || '',
5511 'jobnum' => $options{depend_jobnum} || '',
5513 $cust_pay_pending->payunique( $options{payunique} )
5514 if defined($options{payunique}) && length($options{payunique});
5515 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
5516 return $cpp_new_err if $cpp_new_err;
5518 my( $action1, $action2 ) =
5519 split( /\s*\,\s*/, $payment_gateway->gateway_action );
5521 my $transaction = new $namespace( $payment_gateway->gateway_module,
5522 $self->_bop_options(\%options),
5525 $transaction->content(
5526 'type' => $options{method},
5527 $self->_bop_auth(\%options),
5528 'action' => $action1,
5529 'description' => $options{'description'},
5530 'amount' => $options{amount},
5531 #'invoice_number' => $options{'invnum'},
5532 'customer_id' => $self->custnum,
5534 'reference' => $cust_pay_pending->paypendingnum, #for now
5539 $cust_pay_pending->status('pending');
5540 my $cpp_pending_err = $cust_pay_pending->replace;
5541 return $cpp_pending_err if $cpp_pending_err;
5544 my $BOP_TESTING = 0;
5545 my $BOP_TESTING_SUCCESS = 1;
5547 unless ( $BOP_TESTING ) {
5548 $transaction->submit();
5550 if ( $BOP_TESTING_SUCCESS ) {
5551 $transaction->is_success(1);
5552 $transaction->authorization('fake auth');
5554 $transaction->is_success(0);
5555 $transaction->error_message('fake failure');
5559 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
5561 return { reference => $cust_pay_pending->paypendingnum,
5562 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
5564 } elsif ( $transaction->is_success() && $action2 ) {
5566 $cust_pay_pending->status('authorized');
5567 my $cpp_authorized_err = $cust_pay_pending->replace;
5568 return $cpp_authorized_err if $cpp_authorized_err;
5570 my $auth = $transaction->authorization;
5571 my $ordernum = $transaction->can('order_number')
5572 ? $transaction->order_number
5576 new Business::OnlinePayment( $payment_gateway->gateway_module,
5577 $self->_bop_options(\%options),
5582 type => $options{method},
5584 $self->_bop_auth(\%options),
5585 order_number => $ordernum,
5586 amount => $options{amount},
5587 authorization => $auth,
5588 description => $options{'description'},
5591 foreach my $field (qw( authorization_source_code returned_ACI
5592 transaction_identifier validation_code
5593 transaction_sequence_num local_transaction_date
5594 local_transaction_time AVS_result_code )) {
5595 $capture{$field} = $transaction->$field() if $transaction->can($field);
5598 $capture->content( %capture );
5602 unless ( $capture->is_success ) {
5603 my $e = "Authorization successful but capture failed, custnum #".
5604 $self->custnum. ': '. $capture->result_code.
5605 ": ". $capture->error_message;
5613 # remove paycvv after initial transaction
5616 #false laziness w/misc/process/payment.cgi - check both to make sure working
5618 if ( defined $self->dbdef_table->column('paycvv')
5619 && length($self->paycvv)
5620 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
5622 my $error = $self->remove_cvv;
5624 warn "WARNING: error removing cvv: $error\n";
5632 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
5644 if (ref($_[0]) eq 'HASH') {
5645 %options = %{$_[0]};
5647 my ( $method, $amount ) = ( shift, shift );
5649 $options{method} = $method;
5650 $options{amount} = $amount;
5653 if ( $options{'fake_failure'} ) {
5654 return "Error: No error; test failure requested with fake_failure";
5658 #if ( $payment_gateway->gatewaynum ) { # agent override
5659 # $paybatch = $payment_gateway->gatewaynum. '-';
5662 #$paybatch .= "$processor:". $transaction->authorization;
5664 #$paybatch .= ':'. $transaction->order_number
5665 # if $transaction->can('order_number')
5666 # && length($transaction->order_number);
5668 my $paybatch = 'FakeProcessor:54:32';
5670 my $cust_pay = new FS::cust_pay ( {
5671 'custnum' => $self->custnum,
5672 'invnum' => $options{'invnum'},
5673 'paid' => $options{amount},
5675 'payby' => $bop_method2payby{$options{method}},
5676 #'payinfo' => $payinfo,
5677 'payinfo' => '4111111111111111',
5678 'paybatch' => $paybatch,
5679 #'paydate' => $paydate,
5680 'paydate' => '2012-05-01',
5682 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
5684 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
5687 $cust_pay->invnum(''); #try again with no specific invnum
5688 my $error2 = $cust_pay->insert( $options{'manual'} ?
5689 ( 'manual' => 1 ) : ()
5692 # gah, even with transactions.
5693 my $e = 'WARNING: Card/ACH debited but database not updated - '.
5694 "error inserting (fake!) payment: $error2".
5695 " (previously tried insert with invnum #$options{'invnum'}" .
5702 if ( $options{'paynum_ref'} ) {
5703 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
5706 return ''; #no error
5711 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
5713 # Wraps up processing of a realtime credit card, ACH (electronic check) or
5714 # phone bill transaction.
5716 sub _realtime_bop_result {
5717 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
5719 warn "$me _realtime_bop_result: pending transaction ".
5720 $cust_pay_pending->paypendingnum. "\n";
5721 warn " $_ => $options{$_}\n" foreach keys %options;
5724 my $payment_gateway = $options{payment_gateway}
5725 or return "no payment gateway in arguments to _realtime_bop_result";
5727 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
5728 my $cpp_captured_err = $cust_pay_pending->replace;
5729 return $cpp_captured_err if $cpp_captured_err;
5731 if ( $transaction->is_success() ) {
5734 if ( $payment_gateway->gatewaynum ) { # agent override
5735 $paybatch = $payment_gateway->gatewaynum. '-';
5738 $paybatch .= $payment_gateway->gateway_module. ":".
5739 $transaction->authorization;
5741 $paybatch .= ':'. $transaction->order_number
5742 if $transaction->can('order_number')
5743 && length($transaction->order_number);
5745 my $cust_pay = new FS::cust_pay ( {
5746 'custnum' => $self->custnum,
5747 'invnum' => $options{'invnum'},
5748 'paid' => $cust_pay_pending->paid,
5750 'payby' => $cust_pay_pending->payby,
5751 #'payinfo' => $payinfo,
5752 'paybatch' => $paybatch,
5753 'paydate' => $cust_pay_pending->paydate,
5754 'pkgnum' => $cust_pay_pending->pkgnum,
5756 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
5757 $cust_pay->payunique( $options{payunique} )
5758 if defined($options{payunique}) && length($options{payunique});
5760 my $oldAutoCommit = $FS::UID::AutoCommit;
5761 local $FS::UID::AutoCommit = 0;
5764 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
5766 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
5769 $cust_pay->invnum(''); #try again with no specific invnum
5770 my $error2 = $cust_pay->insert( $options{'manual'} ?
5771 ( 'manual' => 1 ) : ()
5774 # gah. but at least we have a record of the state we had to abort in
5775 # from cust_pay_pending now.
5776 my $e = "WARNING: $options{method} captured but payment not recorded -".
5777 " error inserting payment (". $payment_gateway->gateway_module.
5779 " (previously tried insert with invnum #$options{'invnum'}" .
5780 ": $error ) - pending payment saved as paypendingnum ".
5781 $cust_pay_pending->paypendingnum. "\n";
5787 my $jobnum = $cust_pay_pending->jobnum;
5789 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
5791 unless ( $placeholder ) {
5792 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5793 my $e = "WARNING: $options{method} captured but job $jobnum not ".
5794 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
5799 $error = $placeholder->delete;
5802 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5803 my $e = "WARNING: $options{method} captured but could not delete ".
5804 "job $jobnum for paypendingnum ".
5805 $cust_pay_pending->paypendingnum. ": $error\n";
5812 if ( $options{'paynum_ref'} ) {
5813 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
5816 $cust_pay_pending->status('done');
5817 $cust_pay_pending->statustext('captured');
5818 $cust_pay_pending->paynum($cust_pay->paynum);
5819 my $cpp_done_err = $cust_pay_pending->replace;
5821 if ( $cpp_done_err ) {
5823 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5824 my $e = "WARNING: $options{method} captured but payment not recorded - ".
5825 "error updating status for paypendingnum ".
5826 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
5832 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5834 if ( $options{'apply'} ) {
5835 my $apply_error = $self->apply_payments_and_credits;
5836 if ( $apply_error ) {
5837 warn "WARNING: error applying payment: $apply_error\n";
5838 #but we still should return no error cause the payment otherwise went
5843 return ''; #no error
5849 my $perror = $payment_gateway->gateway_module. " error: ".
5850 $transaction->error_message;
5852 my $jobnum = $cust_pay_pending->jobnum;
5854 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
5856 if ( $placeholder ) {
5857 my $error = $placeholder->depended_delete;
5858 $error ||= $placeholder->delete;
5859 warn "error removing provisioning jobs after declined paypendingnum ".
5860 $cust_pay_pending->paypendingnum. "\n";
5862 my $e = "error finding job $jobnum for declined paypendingnum ".
5863 $cust_pay_pending->paypendingnum. "\n";
5869 unless ( $transaction->error_message ) {
5872 if ( $transaction->can('response_page') ) {
5874 'page' => ( $transaction->can('response_page')
5875 ? $transaction->response_page
5878 'code' => ( $transaction->can('response_code')
5879 ? $transaction->response_code
5882 'headers' => ( $transaction->can('response_headers')
5883 ? $transaction->response_headers
5889 "No additional debugging information available for ".
5890 $payment_gateway->gateway_module;
5893 $perror .= "No error_message returned from ".
5894 $payment_gateway->gateway_module. " -- ".
5895 ( ref($t_response) ? Dumper($t_response) : $t_response );
5899 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
5900 && $conf->exists('emaildecline')
5901 && grep { $_ ne 'POST' } $self->invoicing_list
5902 && ! grep { $transaction->error_message =~ /$_/ }
5903 $conf->config('emaildecline-exclude')
5905 my @templ = $conf->config('declinetemplate');
5906 my $template = new Text::Template (
5908 SOURCE => [ map "$_\n", @templ ],
5909 ) or return "($perror) can't create template: $Text::Template::ERROR";
5910 $template->compile()
5911 or return "($perror) can't compile template: $Text::Template::ERROR";
5915 scalar( $conf->config('company_name', $self->agentnum ) ),
5916 'company_address' =>
5917 join("\n", $conf->config('company_address', $self->agentnum ) ),
5918 'error' => $transaction->error_message,
5921 my $error = send_email(
5922 'from' => $conf->config('invoice_from', $self->agentnum ),
5923 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
5924 'subject' => 'Your payment could not be processed',
5925 'body' => [ $template->fill_in(HASH => $templ_hash) ],
5928 $perror .= " (also received error sending decline notification: $error)"
5933 $cust_pay_pending->status('done');
5934 $cust_pay_pending->statustext("declined: $perror");
5935 my $cpp_done_err = $cust_pay_pending->replace;
5936 if ( $cpp_done_err ) {
5937 my $e = "WARNING: $options{method} declined but pending payment not ".
5938 "resolved - error updating status for paypendingnum ".
5939 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
5941 $perror = "$e ($perror)";
5949 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
5951 Verifies successful third party processing of a realtime credit card,
5952 ACH (electronic check) or phone bill transaction via a
5953 Business::OnlineThirdPartyPayment realtime gateway. See
5954 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
5956 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
5958 The additional options I<payname>, I<city>, I<state>,
5959 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
5960 if set, will override the value from the customer record.
5962 I<description> is a free-text field passed to the gateway. It defaults to
5963 "Internet services".
5965 If an I<invnum> is specified, this payment (if successful) is applied to the
5966 specified invoice. If you don't specify an I<invnum> you might want to
5967 call the B<apply_payments> method.
5969 I<quiet> can be set true to surpress email decline notices.
5971 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
5972 resulting paynum, if any.
5974 I<payunique> is a unique identifier for this payment.
5976 Returns a hashref containing elements bill_error (which will be undefined
5977 upon success) and session_id of any associated session.
5981 sub realtime_botpp_capture {
5982 my( $self, $cust_pay_pending, %options ) = @_;
5984 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
5985 warn " $_ => $options{$_}\n" foreach keys %options;
5988 eval "use Business::OnlineThirdPartyPayment";
5992 # select the gateway
5995 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
5997 my $payment_gateway = $cust_pay_pending->gatewaynum
5998 ? qsearchs( 'payment_gateway',
5999 { gatewaynum => $cust_pay_pending->gatewaynum }
6001 : $self->agent->payment_gateway( 'method' => $method,
6002 # 'invnum' => $cust_pay_pending->invnum,
6003 # 'payinfo' => $cust_pay_pending->payinfo,
6006 $options{payment_gateway} = $payment_gateway; # for the helper subs
6012 my @invoicing_list = $self->invoicing_list_emailonly;
6013 if ( $conf->exists('emailinvoiceautoalways')
6014 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
6015 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
6016 push @invoicing_list, $self->all_emails;
6019 my $email = ($conf->exists('business-onlinepayment-email-override'))
6020 ? $conf->config('business-onlinepayment-email-override')
6021 : $invoicing_list[0];
6025 $content{email_customer} =
6026 ( $conf->exists('business-onlinepayment-email_customer')
6027 || $conf->exists('business-onlinepayment-email-override') );
6030 # run transaction(s)
6034 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
6035 $self->_bop_options(\%options),
6038 $transaction->reference({ %options });
6040 $transaction->content(
6042 $self->_bop_auth(\%options),
6043 'action' => 'Post Authorization',
6044 'description' => $options{'description'},
6045 'amount' => $cust_pay_pending->paid,
6046 #'invoice_number' => $options{'invnum'},
6047 'customer_id' => $self->custnum,
6048 'referer' => 'http://cleanwhisker.420.am/',
6049 'reference' => $cust_pay_pending->paypendingnum,
6051 'phone' => $self->daytime || $self->night,
6053 # plus whatever is required for bogus capture avoidance
6056 $transaction->submit();
6059 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
6062 bill_error => $error,
6063 session_id => $cust_pay_pending->session_id,
6068 =item default_payment_gateway DEPRECATED -- use agent->payment_gateway
6072 sub default_payment_gateway {
6073 my( $self, $method ) = @_;
6075 die "Real-time processing not enabled\n"
6076 unless $conf->exists('business-onlinepayment');
6078 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
6081 my $bop_config = 'business-onlinepayment';
6082 $bop_config .= '-ach'
6083 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
6084 my ( $processor, $login, $password, $action, @bop_options ) =
6085 $conf->config($bop_config);
6086 $action ||= 'normal authorization';
6087 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
6088 die "No real-time processor is enabled - ".
6089 "did you set the business-onlinepayment configuration value?\n"
6092 ( $processor, $login, $password, $action, @bop_options )
6097 Removes the I<paycvv> field from the database directly.
6099 If there is an error, returns the error, otherwise returns false.
6105 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
6106 or return dbh->errstr;
6107 $sth->execute($self->custnum)
6108 or return $sth->errstr;
6113 =item _new_realtime_refund_bop METHOD [ OPTION => VALUE ... ]
6115 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
6116 via a Business::OnlinePayment realtime gateway. See
6117 L<http://420.am/business-onlinepayment> for supported gateways.
6119 Available methods are: I<CC>, I<ECHECK> and I<LEC>
6121 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
6123 Most gateways require a reference to an original payment transaction to refund,
6124 so you probably need to specify a I<paynum>.
6126 I<amount> defaults to the original amount of the payment if not specified.
6128 I<reason> specifies a reason for the refund.
6130 I<paydate> specifies the expiration date for a credit card overriding the
6131 value from the customer record or the payment record. Specified as yyyy-mm-dd
6133 Implementation note: If I<amount> is unspecified or equal to the amount of the
6134 orignal payment, first an attempt is made to "void" the transaction via
6135 the gateway (to cancel a not-yet settled transaction) and then if that fails,
6136 the normal attempt is made to "refund" ("credit") the transaction via the
6137 gateway is attempted.
6139 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
6140 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
6141 #if set, will override the value from the customer record.
6143 #If an I<invnum> is specified, this payment (if successful) is applied to the
6144 #specified invoice. If you don't specify an I<invnum> you might want to
6145 #call the B<apply_payments> method.
6149 #some false laziness w/realtime_bop, not enough to make it worth merging
6150 #but some useful small subs should be pulled out
6151 sub _new_realtime_refund_bop {
6155 if (ref($_[0]) ne 'HASH') {
6156 %options = %{$_[0]};
6160 $options{method} = $method;
6164 warn "$me realtime_refund_bop (new): $options{method} refund\n";
6165 warn " $_ => $options{$_}\n" foreach keys %options;
6169 # look up the original payment and optionally a gateway for that payment
6173 my $amount = $options{'amount'};
6175 my( $processor, $login, $password, @bop_options, $namespace ) ;
6176 my( $auth, $order_number ) = ( '', '', '' );
6178 if ( $options{'paynum'} ) {
6180 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
6181 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
6182 or return "Unknown paynum $options{'paynum'}";
6183 $amount ||= $cust_pay->paid;
6185 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
6186 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
6187 $cust_pay->paybatch;
6188 my $gatewaynum = '';
6189 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
6191 if ( $gatewaynum ) { #gateway for the payment to be refunded
6193 my $payment_gateway =
6194 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
6195 die "payment gateway $gatewaynum not found"
6196 unless $payment_gateway;
6198 $processor = $payment_gateway->gateway_module;
6199 $login = $payment_gateway->gateway_username;
6200 $password = $payment_gateway->gateway_password;
6201 $namespace = $payment_gateway->gateway_namespace;
6202 @bop_options = $payment_gateway->options;
6204 } else { #try the default gateway
6207 my $payment_gateway =
6208 $self->agent->payment_gateway('method' => $options{method});
6210 ( $conf_processor, $login, $password, $namespace ) =
6211 map { my $method = "gateway_$_"; $payment_gateway->$method }
6212 qw( module username password namespace );
6214 @bop_options = $payment_gateway->gatewaynum
6215 ? $payment_gateway->options
6216 : @{ $payment_gateway->get('options') };
6218 return "processor of payment $options{'paynum'} $processor does not".
6219 " match default processor $conf_processor"
6220 unless $processor eq $conf_processor;
6225 } else { # didn't specify a paynum, so look for agent gateway overrides
6226 # like a normal transaction
6228 my $payment_gateway =
6229 $self->agent->payment_gateway( 'method' => $options{method},
6230 #'payinfo' => $payinfo,
6232 my( $processor, $login, $password, $namespace ) =
6233 map { my $method = "gateway_$_"; $payment_gateway->$method }
6234 qw( module username password namespace );
6236 my @bop_options = $payment_gateway->gatewaynum
6237 ? $payment_gateway->options
6238 : @{ $payment_gateway->get('options') };
6241 return "neither amount nor paynum specified" unless $amount;
6243 eval "use $namespace";
6247 'type' => $options{method},
6249 'password' => $password,
6250 'order_number' => $order_number,
6251 'amount' => $amount,
6252 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
6254 $content{authorization} = $auth
6255 if length($auth); #echeck/ACH transactions have an order # but no auth
6256 #(at least with authorize.net)
6258 my $disable_void_after;
6259 if ($conf->exists('disable_void_after')
6260 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
6261 $disable_void_after = $1;
6264 #first try void if applicable
6265 if ( $cust_pay && $cust_pay->paid == $amount
6267 ( not defined($disable_void_after) )
6268 || ( time < ($cust_pay->_date + $disable_void_after ) )
6271 warn " attempting void\n" if $DEBUG > 1;
6272 my $void = new Business::OnlinePayment( $processor, @bop_options );
6273 if ( $void->can('info') ) {
6274 if ( $cust_pay->payby eq 'CARD'
6275 && $void->info('CC_void_requires_card') )
6277 $content{'card_number'} = $cust_pay->payinfo;
6278 } elsif ( $cust_pay->payby eq 'CHEK'
6279 && $void->info('ECHECK_void_requires_account') )
6281 ( $content{'account_number'}, $content{'routing_code'} ) =
6282 split('@', $cust_pay->payinfo);
6283 $content{'name'} = $self->get('first'). ' '. $self->get('last');
6286 $void->content( 'action' => 'void', %content );
6288 if ( $void->is_success ) {
6289 my $error = $cust_pay->void($options{'reason'});
6291 # gah, even with transactions.
6292 my $e = 'WARNING: Card/ACH voided but database not updated - '.
6293 "error voiding payment: $error";
6297 warn " void successful\n" if $DEBUG > 1;
6302 warn " void unsuccessful, trying refund\n"
6306 my $address = $self->address1;
6307 $address .= ", ". $self->address2 if $self->address2;
6309 my($payname, $payfirst, $paylast);
6310 if ( $self->payname && $options{method} ne 'ECHECK' ) {
6311 $payname = $self->payname;
6312 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
6313 or return "Illegal payname $payname";
6314 ($payfirst, $paylast) = ($1, $2);
6316 $payfirst = $self->getfield('first');
6317 $paylast = $self->getfield('last');
6318 $payname = "$payfirst $paylast";
6321 my @invoicing_list = $self->invoicing_list_emailonly;
6322 if ( $conf->exists('emailinvoiceautoalways')
6323 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
6324 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
6325 push @invoicing_list, $self->all_emails;
6328 my $email = ($conf->exists('business-onlinepayment-email-override'))
6329 ? $conf->config('business-onlinepayment-email-override')
6330 : $invoicing_list[0];
6332 my $payip = exists($options{'payip'})
6335 $content{customer_ip} = $payip
6339 if ( $options{method} eq 'CC' ) {
6342 $content{card_number} = $payinfo = $cust_pay->payinfo;
6343 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
6344 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
6345 ($content{expiration} = "$2/$1"); # where available
6347 $content{card_number} = $payinfo = $self->payinfo;
6348 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
6349 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
6350 $content{expiration} = "$2/$1";
6353 } elsif ( $options{method} eq 'ECHECK' ) {
6356 $payinfo = $cust_pay->payinfo;
6358 $payinfo = $self->payinfo;
6360 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
6361 $content{bank_name} = $self->payname;
6362 $content{account_type} = 'CHECKING';
6363 $content{account_name} = $payname;
6364 $content{customer_org} = $self->company ? 'B' : 'I';
6365 $content{customer_ssn} = $self->ss;
6366 } elsif ( $options{method} eq 'LEC' ) {
6367 $content{phone} = $payinfo = $self->payinfo;
6371 my $refund = new Business::OnlinePayment( $processor, @bop_options );
6372 my %sub_content = $refund->content(
6373 'action' => 'credit',
6374 'customer_id' => $self->custnum,
6375 'last_name' => $paylast,
6376 'first_name' => $payfirst,
6378 'address' => $address,
6379 'city' => $self->city,
6380 'state' => $self->state,
6381 'zip' => $self->zip,
6382 'country' => $self->country,
6384 'phone' => $self->daytime || $self->night,
6387 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
6391 return "$processor error: ". $refund->error_message
6392 unless $refund->is_success();
6394 my $paybatch = "$processor:". $refund->authorization;
6395 $paybatch .= ':'. $refund->order_number
6396 if $refund->can('order_number') && $refund->order_number;
6398 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
6399 my @cust_bill_pay = $cust_pay->cust_bill_pay;
6400 last unless @cust_bill_pay;
6401 my $cust_bill_pay = pop @cust_bill_pay;
6402 my $error = $cust_bill_pay->delete;
6406 my $cust_refund = new FS::cust_refund ( {
6407 'custnum' => $self->custnum,
6408 'paynum' => $options{'paynum'},
6409 'refund' => $amount,
6411 'payby' => $bop_method2payby{$options{method}},
6412 'payinfo' => $payinfo,
6413 'paybatch' => $paybatch,
6414 'reason' => $options{'reason'} || 'card or ACH refund',
6416 my $error = $cust_refund->insert;
6418 $cust_refund->paynum(''); #try again with no specific paynum
6419 my $error2 = $cust_refund->insert;
6421 # gah, even with transactions.
6422 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
6423 "error inserting refund ($processor): $error2".
6424 " (previously tried insert with paynum #$options{'paynum'}" .
6435 =item batch_card OPTION => VALUE...
6437 Adds a payment for this invoice to the pending credit card batch (see
6438 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
6439 runs the payment using a realtime gateway.
6444 my ($self, %options) = @_;
6447 if (exists($options{amount})) {
6448 $amount = $options{amount};
6450 $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
6452 return '' unless $amount > 0;
6454 my $invnum = delete $options{invnum};
6455 my $payby = $options{invnum} || $self->payby; #dubious
6457 if ($options{'realtime'}) {
6458 return $self->realtime_bop( FS::payby->payby2bop($self->payby),
6464 my $oldAutoCommit = $FS::UID::AutoCommit;
6465 local $FS::UID::AutoCommit = 0;
6468 #this needs to handle mysql as well as Pg, like svc_acct.pm
6469 #(make it into a common function if folks need to do batching with mysql)
6470 $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
6471 or return "Cannot lock pay_batch: " . $dbh->errstr;
6475 'payby' => FS::payby->payby2payment($payby),
6478 my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
6480 unless ( $pay_batch ) {
6481 $pay_batch = new FS::pay_batch \%pay_batch;
6482 my $error = $pay_batch->insert;
6484 $dbh->rollback if $oldAutoCommit;
6485 die "error creating new batch: $error\n";
6489 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
6490 'batchnum' => $pay_batch->batchnum,
6491 'custnum' => $self->custnum,
6494 foreach (qw( address1 address2 city state zip country payby payinfo paydate
6496 $options{$_} = '' unless exists($options{$_});
6499 my $cust_pay_batch = new FS::cust_pay_batch ( {
6500 'batchnum' => $pay_batch->batchnum,
6501 'invnum' => $invnum || 0, # is there a better value?
6502 # this field should be
6504 # cust_bill_pay_batch now
6505 'custnum' => $self->custnum,
6506 'last' => $self->getfield('last'),
6507 'first' => $self->getfield('first'),
6508 'address1' => $options{address1} || $self->address1,
6509 'address2' => $options{address2} || $self->address2,
6510 'city' => $options{city} || $self->city,
6511 'state' => $options{state} || $self->state,
6512 'zip' => $options{zip} || $self->zip,
6513 'country' => $options{country} || $self->country,
6514 'payby' => $options{payby} || $self->payby,
6515 'payinfo' => $options{payinfo} || $self->payinfo,
6516 'exp' => $options{paydate} || $self->paydate,
6517 'payname' => $options{payname} || $self->payname,
6518 'amount' => $amount, # consolidating
6521 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
6522 if $old_cust_pay_batch;
6525 if ($old_cust_pay_batch) {
6526 $error = $cust_pay_batch->replace($old_cust_pay_batch)
6528 $error = $cust_pay_batch->insert;
6532 $dbh->rollback if $oldAutoCommit;
6536 my $unapplied = $self->total_unapplied_credits
6537 + $self->total_unapplied_payments
6538 + $self->in_transit_payments;
6539 foreach my $cust_bill ($self->open_cust_bill) {
6540 #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
6541 my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
6542 'invnum' => $cust_bill->invnum,
6543 'paybatchnum' => $cust_pay_batch->paybatchnum,
6544 'amount' => $cust_bill->owed,
6547 if ($unapplied >= $cust_bill_pay_batch->amount){
6548 $unapplied -= $cust_bill_pay_batch->amount;
6551 $cust_bill_pay_batch->amount(sprintf ( "%.2f",
6552 $cust_bill_pay_batch->amount - $unapplied )); $unapplied = 0;
6554 $error = $cust_bill_pay_batch->insert;
6556 $dbh->rollback if $oldAutoCommit;
6561 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6565 =item apply_payments_and_credits [ OPTION => VALUE ... ]
6567 Applies unapplied payments and credits.
6569 In most cases, this new method should be used in place of sequential
6570 apply_payments and apply_credits methods.
6572 A hash of optional arguments may be passed. Currently "manual" is supported.
6573 If true, a payment receipt is sent instead of a statement when
6574 'payment_receipt_email' configuration option is set.
6576 If there is an error, returns the error, otherwise returns false.
6580 sub apply_payments_and_credits {
6581 my( $self, %options ) = @_;
6583 local $SIG{HUP} = 'IGNORE';
6584 local $SIG{INT} = 'IGNORE';
6585 local $SIG{QUIT} = 'IGNORE';
6586 local $SIG{TERM} = 'IGNORE';
6587 local $SIG{TSTP} = 'IGNORE';
6588 local $SIG{PIPE} = 'IGNORE';
6590 my $oldAutoCommit = $FS::UID::AutoCommit;
6591 local $FS::UID::AutoCommit = 0;
6594 $self->select_for_update; #mutex
6596 foreach my $cust_bill ( $self->open_cust_bill ) {
6597 my $error = $cust_bill->apply_payments_and_credits(%options);
6599 $dbh->rollback if $oldAutoCommit;
6600 return "Error applying: $error";
6604 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6609 =item apply_credits OPTION => VALUE ...
6611 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
6612 to outstanding invoice balances in chronological order (or reverse
6613 chronological order if the I<order> option is set to B<newest>) and returns the
6614 value of any remaining unapplied credits available for refund (see
6615 L<FS::cust_refund>).
6617 Dies if there is an error.
6625 local $SIG{HUP} = 'IGNORE';
6626 local $SIG{INT} = 'IGNORE';
6627 local $SIG{QUIT} = 'IGNORE';
6628 local $SIG{TERM} = 'IGNORE';
6629 local $SIG{TSTP} = 'IGNORE';
6630 local $SIG{PIPE} = 'IGNORE';
6632 my $oldAutoCommit = $FS::UID::AutoCommit;
6633 local $FS::UID::AutoCommit = 0;
6636 $self->select_for_update; #mutex
6638 unless ( $self->total_unapplied_credits ) {
6639 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6643 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
6644 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
6646 my @invoices = $self->open_cust_bill;
6647 @invoices = sort { $b->_date <=> $a->_date } @invoices
6648 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
6650 if ( $conf->exists('pkg-balances') ) {
6651 # limit @credits to those w/ a pkgnum grepped from $self
6653 foreach my $i (@invoices) {
6654 foreach my $li ( $i->cust_bill_pkg ) {
6655 $pkgnums{$li->pkgnum} = 1;
6658 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
6663 foreach my $cust_bill ( @invoices ) {
6665 if ( !defined($credit) || $credit->credited == 0) {
6666 $credit = pop @credits or last;
6670 if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
6671 $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
6673 $owed = $cust_bill->owed;
6675 unless ( $owed > 0 ) {
6676 push @credits, $credit;
6680 my $amount = min( $credit->credited, $owed );
6682 my $cust_credit_bill = new FS::cust_credit_bill ( {
6683 'crednum' => $credit->crednum,
6684 'invnum' => $cust_bill->invnum,
6685 'amount' => $amount,
6687 $cust_credit_bill->pkgnum( $credit->pkgnum )
6688 if $conf->exists('pkg-balances') && $credit->pkgnum;
6689 my $error = $cust_credit_bill->insert;
6691 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
6695 redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
6699 my $total_unapplied_credits = $self->total_unapplied_credits;
6701 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6703 return $total_unapplied_credits;
6706 =item apply_payments [ OPTION => VALUE ... ]
6708 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
6709 to outstanding invoice balances in chronological order.
6711 #and returns the value of any remaining unapplied payments.
6713 A hash of optional arguments may be passed. Currently "manual" is supported.
6714 If true, a payment receipt is sent instead of a statement when
6715 'payment_receipt_email' configuration option is set.
6717 Dies if there is an error.
6721 sub apply_payments {
6722 my( $self, %options ) = @_;
6724 local $SIG{HUP} = 'IGNORE';
6725 local $SIG{INT} = 'IGNORE';
6726 local $SIG{QUIT} = 'IGNORE';
6727 local $SIG{TERM} = 'IGNORE';
6728 local $SIG{TSTP} = 'IGNORE';
6729 local $SIG{PIPE} = 'IGNORE';
6731 my $oldAutoCommit = $FS::UID::AutoCommit;
6732 local $FS::UID::AutoCommit = 0;
6735 $self->select_for_update; #mutex
6739 my @payments = sort { $b->_date <=> $a->_date }
6740 grep { $_->unapplied > 0 }
6743 my @invoices = sort { $a->_date <=> $b->_date}
6744 grep { $_->owed > 0 }
6747 if ( $conf->exists('pkg-balances') ) {
6748 # limit @payments to those w/ a pkgnum grepped from $self
6750 foreach my $i (@invoices) {
6751 foreach my $li ( $i->cust_bill_pkg ) {
6752 $pkgnums{$li->pkgnum} = 1;
6755 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
6760 foreach my $cust_bill ( @invoices ) {
6762 if ( !defined($payment) || $payment->unapplied == 0 ) {
6763 $payment = pop @payments or last;
6767 if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
6768 $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
6770 $owed = $cust_bill->owed;
6772 unless ( $owed > 0 ) {
6773 push @payments, $payment;
6777 my $amount = min( $payment->unapplied, $owed );
6779 my $cust_bill_pay = new FS::cust_bill_pay ( {
6780 'paynum' => $payment->paynum,
6781 'invnum' => $cust_bill->invnum,
6782 'amount' => $amount,
6784 $cust_bill_pay->pkgnum( $payment->pkgnum )
6785 if $conf->exists('pkg-balances') && $payment->pkgnum;
6786 my $error = $cust_bill_pay->insert(%options);
6788 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
6792 redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
6796 my $total_unapplied_payments = $self->total_unapplied_payments;
6798 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6800 return $total_unapplied_payments;
6805 Returns the total owed for this customer on all invoices
6806 (see L<FS::cust_bill/owed>).
6812 $self->total_owed_date(2145859200); #12/31/2037
6815 =item total_owed_date TIME
6817 Returns the total owed for this customer on all invoices with date earlier than
6818 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
6819 see L<Time::Local> and L<Date::Parse> for conversion functions.
6823 sub total_owed_date {
6827 # my $custnum = $self->custnum;
6829 # my $owed_sql = FS::cust_bill->owed_sql;
6832 # SELECT SUM($owed_sql) FROM cust_bill
6833 # WHERE custnum = $custnum
6834 # AND _date <= $time
6837 # my $sth = dbh->prepare($sql) or die dbh->errstr;
6838 # $sth->execute() or die $sth->errstr;
6840 # return sprintf( '%.2f', $sth->fetchrow_arrayref->[0] );
6843 foreach my $cust_bill (
6844 grep { $_->_date <= $time }
6845 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
6847 $total_bill += $cust_bill->owed;
6849 sprintf( "%.2f", $total_bill );
6853 =item total_owed_pkgnum PKGNUM
6855 Returns the total owed on all invoices for this customer's specific package
6856 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
6860 sub total_owed_pkgnum {
6861 my( $self, $pkgnum ) = @_;
6862 $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
6865 =item total_owed_date_pkgnum TIME PKGNUM
6867 Returns the total owed for this customer's specific package when using
6868 experimental package balances on all invoices with date earlier than
6869 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
6870 see L<Time::Local> and L<Date::Parse> for conversion functions.
6874 sub total_owed_date_pkgnum {
6875 my( $self, $time, $pkgnum ) = @_;
6878 foreach my $cust_bill (
6879 grep { $_->_date <= $time }
6880 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
6882 $total_bill += $cust_bill->owed_pkgnum($pkgnum);
6884 sprintf( "%.2f", $total_bill );
6890 Returns the total amount of all payments.
6897 $total += $_->paid foreach $self->cust_pay;
6898 sprintf( "%.2f", $total );
6901 =item total_unapplied_credits
6903 Returns the total outstanding credit (see L<FS::cust_credit>) for this
6904 customer. See L<FS::cust_credit/credited>.
6906 =item total_credited
6908 Old name for total_unapplied_credits. Don't use.
6912 sub total_credited {
6913 #carp "total_credited deprecated, use total_unapplied_credits";
6914 shift->total_unapplied_credits(@_);
6917 sub total_unapplied_credits {
6919 my $total_credit = 0;
6920 $total_credit += $_->credited foreach $self->cust_credit;
6921 sprintf( "%.2f", $total_credit );
6924 =item total_unapplied_credits_pkgnum PKGNUM
6926 Returns the total outstanding credit (see L<FS::cust_credit>) for this
6927 customer. See L<FS::cust_credit/credited>.
6931 sub total_unapplied_credits_pkgnum {
6932 my( $self, $pkgnum ) = @_;
6933 my $total_credit = 0;
6934 $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
6935 sprintf( "%.2f", $total_credit );
6939 =item total_unapplied_payments
6941 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
6942 See L<FS::cust_pay/unapplied>.
6946 sub total_unapplied_payments {
6948 my $total_unapplied = 0;
6949 $total_unapplied += $_->unapplied foreach $self->cust_pay;
6950 sprintf( "%.2f", $total_unapplied );
6953 =item total_unapplied_payments_pkgnum PKGNUM
6955 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
6956 specific package when using experimental package balances. See
6957 L<FS::cust_pay/unapplied>.
6961 sub total_unapplied_payments_pkgnum {
6962 my( $self, $pkgnum ) = @_;
6963 my $total_unapplied = 0;
6964 $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
6965 sprintf( "%.2f", $total_unapplied );
6969 =item total_unapplied_refunds
6971 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
6972 customer. See L<FS::cust_refund/unapplied>.
6976 sub total_unapplied_refunds {
6978 my $total_unapplied = 0;
6979 $total_unapplied += $_->unapplied foreach $self->cust_refund;
6980 sprintf( "%.2f", $total_unapplied );
6985 Returns the balance for this customer (total_owed plus total_unrefunded, minus
6986 total_unapplied_credits minus total_unapplied_payments).
6994 + $self->total_unapplied_refunds
6995 - $self->total_unapplied_credits
6996 - $self->total_unapplied_payments
7000 =item balance_date TIME
7002 Returns the balance for this customer, only considering invoices with date
7003 earlier than TIME (total_owed_date minus total_credited minus
7004 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
7005 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
7014 $self->total_owed_date($time)
7015 + $self->total_unapplied_refunds
7016 - $self->total_unapplied_credits
7017 - $self->total_unapplied_payments
7021 =item balance_date_range START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
7023 Returns the balance for this customer, only considering invoices with date
7024 earlier than START_TIME, and optionally not later than END_TIME
7025 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
7027 Times are specified as SQL fragments or numeric
7028 UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
7029 L<Date::Parse> for conversion functions. The empty string can be passed
7030 to disable that time constraint completely.
7032 Available options are:
7036 =item unapplied_date
7038 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)
7044 sub balance_date_range {
7046 my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
7047 ') FROM cust_main WHERE custnum='. $self->custnum;
7048 sprintf( '%.2f', $self->scalar_sql($sql) );
7051 =item balance_pkgnum PKGNUM
7053 Returns the balance for this customer's specific package when using
7054 experimental package balances (total_owed plus total_unrefunded, minus
7055 total_unapplied_credits minus total_unapplied_payments)
7059 sub balance_pkgnum {
7060 my( $self, $pkgnum ) = @_;
7063 $self->total_owed_pkgnum($pkgnum)
7064 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
7065 # + $self->total_unapplied_refunds_pkgnum($pkgnum)
7066 - $self->total_unapplied_credits_pkgnum($pkgnum)
7067 - $self->total_unapplied_payments_pkgnum($pkgnum)
7071 =item in_transit_payments
7073 Returns the total of requests for payments for this customer pending in
7074 batches in transit to the bank. See L<FS::pay_batch> and L<FS::cust_pay_batch>
7078 sub in_transit_payments {
7080 my $in_transit_payments = 0;
7081 foreach my $pay_batch ( qsearch('pay_batch', {
7084 foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
7085 'batchnum' => $pay_batch->batchnum,
7086 'custnum' => $self->custnum,
7088 $in_transit_payments += $cust_pay_batch->amount;
7091 sprintf( "%.2f", $in_transit_payments );
7096 Returns a hash of useful information for making a payment.
7106 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
7107 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
7108 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
7112 For credit card transactions:
7124 For electronic check transactions:
7139 $return{balance} = $self->balance;
7141 $return{payname} = $self->payname
7142 || ( $self->first. ' '. $self->get('last') );
7144 $return{$_} = $self->get($_) for qw(address1 address2 city state zip);
7146 $return{payby} = $self->payby;
7147 $return{stateid_state} = $self->stateid_state;
7149 if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
7150 $return{card_type} = cardtype($self->payinfo);
7151 $return{payinfo} = $self->paymask;
7153 @return{'month', 'year'} = $self->paydate_monthyear;
7157 if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
7158 my ($payinfo1, $payinfo2) = split '@', $self->paymask;
7159 $return{payinfo1} = $payinfo1;
7160 $return{payinfo2} = $payinfo2;
7161 $return{paytype} = $self->paytype;
7162 $return{paystate} = $self->paystate;
7166 #doubleclick protection
7168 $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
7174 =item paydate_monthyear
7176 Returns a two-element list consisting of the month and year of this customer's
7177 paydate (credit card expiration date for CARD customers)
7181 sub paydate_monthyear {
7183 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
7185 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
7192 =item tax_exemption TAXNAME
7197 my( $self, $taxname ) = @_;
7199 qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
7200 'taxname' => $taxname,
7205 =item cust_main_exemption
7209 sub cust_main_exemption {
7211 qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
7214 =item invoicing_list [ ARRAYREF ]
7216 If an arguement is given, sets these email addresses as invoice recipients
7217 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
7218 (except as warnings), so use check_invoicing_list first.
7220 Returns a list of email addresses (with svcnum entries expanded).
7222 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
7223 check it without disturbing anything by passing nothing.
7225 This interface may change in the future.
7229 sub invoicing_list {
7230 my( $self, $arrayref ) = @_;
7233 my @cust_main_invoice;
7234 if ( $self->custnum ) {
7235 @cust_main_invoice =
7236 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
7238 @cust_main_invoice = ();
7240 foreach my $cust_main_invoice ( @cust_main_invoice ) {
7241 #warn $cust_main_invoice->destnum;
7242 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
7243 #warn $cust_main_invoice->destnum;
7244 my $error = $cust_main_invoice->delete;
7245 warn $error if $error;
7248 if ( $self->custnum ) {
7249 @cust_main_invoice =
7250 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
7252 @cust_main_invoice = ();
7254 my %seen = map { $_->address => 1 } @cust_main_invoice;
7255 foreach my $address ( @{$arrayref} ) {
7256 next if exists $seen{$address} && $seen{$address};
7257 $seen{$address} = 1;
7258 my $cust_main_invoice = new FS::cust_main_invoice ( {
7259 'custnum' => $self->custnum,
7262 my $error = $cust_main_invoice->insert;
7263 warn $error if $error;
7267 if ( $self->custnum ) {
7269 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
7276 =item check_invoicing_list ARRAYREF
7278 Checks these arguements as valid input for the invoicing_list method. If there
7279 is an error, returns the error, otherwise returns false.
7283 sub check_invoicing_list {
7284 my( $self, $arrayref ) = @_;
7286 foreach my $address ( @$arrayref ) {
7288 if ($address eq 'FAX' and $self->getfield('fax') eq '') {
7289 return 'Can\'t add FAX invoice destination with a blank FAX number.';
7292 my $cust_main_invoice = new FS::cust_main_invoice ( {
7293 'custnum' => $self->custnum,
7296 my $error = $self->custnum
7297 ? $cust_main_invoice->check
7298 : $cust_main_invoice->checkdest
7300 return $error if $error;
7304 return "Email address required"
7305 if $conf->exists('cust_main-require_invoicing_list_email')
7306 && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
7311 =item set_default_invoicing_list
7313 Sets the invoicing list to all accounts associated with this customer,
7314 overwriting any previous invoicing list.
7318 sub set_default_invoicing_list {
7320 $self->invoicing_list($self->all_emails);
7325 Returns the email addresses of all accounts provisioned for this customer.
7332 foreach my $cust_pkg ( $self->all_pkgs ) {
7333 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
7335 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
7336 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
7338 $list{$_}=1 foreach map { $_->email } @svc_acct;
7343 =item invoicing_list_addpost
7345 Adds postal invoicing to this customer. If this customer is already configured
7346 to receive postal invoices, does nothing.
7350 sub invoicing_list_addpost {
7352 return if grep { $_ eq 'POST' } $self->invoicing_list;
7353 my @invoicing_list = $self->invoicing_list;
7354 push @invoicing_list, 'POST';
7355 $self->invoicing_list(\@invoicing_list);
7358 =item invoicing_list_emailonly
7360 Returns the list of email invoice recipients (invoicing_list without non-email
7361 destinations such as POST and FAX).
7365 sub invoicing_list_emailonly {
7367 warn "$me invoicing_list_emailonly called"
7369 grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
7372 =item invoicing_list_emailonly_scalar
7374 Returns the list of email invoice recipients (invoicing_list without non-email
7375 destinations such as POST and FAX) as a comma-separated scalar.
7379 sub invoicing_list_emailonly_scalar {
7381 warn "$me invoicing_list_emailonly_scalar called"
7383 join(', ', $self->invoicing_list_emailonly);
7386 =item referral_custnum_cust_main
7388 Returns the customer who referred this customer (or the empty string, if
7389 this customer was not referred).
7391 Note the difference with referral_cust_main method: This method,
7392 referral_custnum_cust_main returns the single customer (if any) who referred
7393 this customer, while referral_cust_main returns an array of customers referred
7398 sub referral_custnum_cust_main {
7400 return '' unless $self->referral_custnum;
7401 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
7404 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
7406 Returns an array of customers referred by this customer (referral_custnum set
7407 to this custnum). If DEPTH is given, recurses up to the given depth, returning
7408 customers referred by customers referred by this customer and so on, inclusive.
7409 The default behavior is DEPTH 1 (no recursion).
7411 Note the difference with referral_custnum_cust_main method: This method,
7412 referral_cust_main, returns an array of customers referred BY this customer,
7413 while referral_custnum_cust_main returns the single customer (if any) who
7414 referred this customer.
7418 sub referral_cust_main {
7420 my $depth = @_ ? shift : 1;
7421 my $exclude = @_ ? shift : {};
7424 map { $exclude->{$_->custnum}++; $_; }
7425 grep { ! $exclude->{ $_->custnum } }
7426 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
7430 map { $_->referral_cust_main($depth-1, $exclude) }
7437 =item referral_cust_main_ncancelled
7439 Same as referral_cust_main, except only returns customers with uncancelled
7444 sub referral_cust_main_ncancelled {
7446 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
7449 =item referral_cust_pkg [ DEPTH ]
7451 Like referral_cust_main, except returns a flat list of all unsuspended (and
7452 uncancelled) packages for each customer. The number of items in this list may
7453 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
7457 sub referral_cust_pkg {
7459 my $depth = @_ ? shift : 1;
7461 map { $_->unsuspended_pkgs }
7462 grep { $_->unsuspended_pkgs }
7463 $self->referral_cust_main($depth);
7466 =item referring_cust_main
7468 Returns the single cust_main record for the customer who referred this customer
7469 (referral_custnum), or false.
7473 sub referring_cust_main {
7475 return '' unless $self->referral_custnum;
7476 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
7479 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
7481 Applies a credit to this customer. If there is an error, returns the error,
7482 otherwise returns false.
7484 REASON can be a text string, an FS::reason object, or a scalar reference to
7485 a reasonnum. If a text string, it will be automatically inserted as a new
7486 reason, and a 'reason_type' option must be passed to indicate the
7487 FS::reason_type for the new reason.
7489 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
7491 Any other options are passed to FS::cust_credit::insert.
7496 my( $self, $amount, $reason, %options ) = @_;
7498 my $cust_credit = new FS::cust_credit {
7499 'custnum' => $self->custnum,
7500 'amount' => $amount,
7503 if ( ref($reason) ) {
7505 if ( ref($reason) eq 'SCALAR' ) {
7506 $cust_credit->reasonnum( $$reason );
7508 $cust_credit->reasonnum( $reason->reasonnum );
7512 $cust_credit->set('reason', $reason)
7515 for (qw( addlinfo eventnum )) {
7516 $cust_credit->$_( delete $options{$_} )
7517 if exists($options{$_});
7520 $cust_credit->insert(%options);
7524 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
7527 An absolute cutoff time. Payments, credits, and refunds I<applied> after this
7528 time will be ignored. Note that START_TIME and END_TIME only limit the date
7529 range for invoices and I<unapplied> payments, credits, and refunds.
7532 Creates a one-time charge for this customer. If there is an error, returns
7533 the error, otherwise returns false.
7535 New-style, with a hashref of options:
7537 my $error = $cust_main->charge(
7541 'start_date' => str2time('7/4/2009'),
7542 'pkg' => 'Description',
7543 'comment' => 'Comment',
7544 'additional' => [], #extra invoice detail
7545 'classnum' => 1, #pkg_class
7547 'setuptax' => '', # or 'Y' for tax exempt
7550 'taxclass' => 'Tax class',
7553 'taxproduct' => 2, #part_pkg_taxproduct
7554 'override' => {}, #XXX describe
7556 #will be filled in with the new object
7557 'cust_pkg_ref' => \$cust_pkg,
7559 #generate an invoice immediately
7561 'invoice_terms' => '', #with these terms
7567 my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
7573 my ( $amount, $quantity, $start_date, $classnum );
7574 my ( $pkg, $comment, $additional );
7575 my ( $setuptax, $taxclass ); #internal taxes
7576 my ( $taxproduct, $override ); #vendor (CCH) taxes
7577 my $cust_pkg_ref = '';
7578 my ( $bill_now, $invoice_terms ) = ( 0, '' );
7579 if ( ref( $_[0] ) ) {
7580 $amount = $_[0]->{amount};
7581 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
7582 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
7583 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
7584 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
7585 : '$'. sprintf("%.2f",$amount);
7586 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
7587 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
7588 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
7589 $additional = $_[0]->{additional} || [];
7590 $taxproduct = $_[0]->{taxproductnum};
7591 $override = { '' => $_[0]->{tax_override} };
7592 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
7593 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
7594 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
7599 $pkg = @_ ? shift : 'One-time charge';
7600 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
7602 $taxclass = @_ ? shift : '';
7606 local $SIG{HUP} = 'IGNORE';
7607 local $SIG{INT} = 'IGNORE';
7608 local $SIG{QUIT} = 'IGNORE';
7609 local $SIG{TERM} = 'IGNORE';
7610 local $SIG{TSTP} = 'IGNORE';
7611 local $SIG{PIPE} = 'IGNORE';
7613 my $oldAutoCommit = $FS::UID::AutoCommit;
7614 local $FS::UID::AutoCommit = 0;
7617 my $part_pkg = new FS::part_pkg ( {
7619 'comment' => $comment,
7623 'classnum' => $classnum ? $classnum : '',
7624 'setuptax' => $setuptax,
7625 'taxclass' => $taxclass,
7626 'taxproductnum' => $taxproduct,
7629 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
7630 ( 0 .. @$additional - 1 )
7632 'additional_count' => scalar(@$additional),
7633 'setup_fee' => $amount,
7636 my $error = $part_pkg->insert( options => \%options,
7637 tax_overrides => $override,
7640 $dbh->rollback if $oldAutoCommit;
7644 my $pkgpart = $part_pkg->pkgpart;
7645 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
7646 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
7647 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
7648 $error = $type_pkgs->insert;
7650 $dbh->rollback if $oldAutoCommit;
7655 my $cust_pkg = new FS::cust_pkg ( {
7656 'custnum' => $self->custnum,
7657 'pkgpart' => $pkgpart,
7658 'quantity' => $quantity,
7659 'start_date' => $start_date,
7662 $error = $cust_pkg->insert;
7664 $dbh->rollback if $oldAutoCommit;
7666 } elsif ( $cust_pkg_ref ) {
7667 ${$cust_pkg_ref} = $cust_pkg;
7671 my $error = $self->bill( 'invoice_terms' => $invoice_terms,
7672 'pkg_list' => [ $cust_pkg ],
7675 $dbh->rollback if $oldAutoCommit;
7680 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
7685 #=item charge_postal_fee
7687 #Applies a one time charge this customer. If there is an error,
7688 #returns the error, returns the cust_pkg charge object or false
7689 #if there was no charge.
7693 # This should be a customer event. For that to work requires that bill
7694 # also be a customer event.
7696 sub charge_postal_fee {
7699 my $pkgpart = $conf->config('postal_invoice-fee_pkgpart');
7700 return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
7702 my $cust_pkg = new FS::cust_pkg ( {
7703 'custnum' => $self->custnum,
7704 'pkgpart' => $pkgpart,
7708 my $error = $cust_pkg->insert;
7709 $error ? $error : $cust_pkg;
7714 Returns all the invoices (see L<FS::cust_bill>) for this customer.
7720 map { $_ } #return $self->num_cust_bill unless wantarray;
7721 sort { $a->_date <=> $b->_date }
7722 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
7725 =item open_cust_bill
7727 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
7732 sub open_cust_bill {
7736 'table' => 'cust_bill',
7737 'hashref' => { 'custnum' => $self->custnum, },
7738 'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
7739 'order_by' => 'ORDER BY _date ASC',
7744 =item cust_statements
7746 Returns all the statements (see L<FS::cust_statement>) for this customer.
7750 sub cust_statement {
7752 map { $_ } #return $self->num_cust_statement unless wantarray;
7753 sort { $a->_date <=> $b->_date }
7754 qsearch('cust_statement', { 'custnum' => $self->custnum, } )
7759 Returns all the credits (see L<FS::cust_credit>) for this customer.
7765 map { $_ } #return $self->num_cust_credit unless wantarray;
7766 sort { $a->_date <=> $b->_date }
7767 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
7770 =item cust_credit_pkgnum
7772 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
7773 package when using experimental package balances.
7777 sub cust_credit_pkgnum {
7778 my( $self, $pkgnum ) = @_;
7779 map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
7780 sort { $a->_date <=> $b->_date }
7781 qsearch( 'cust_credit', { 'custnum' => $self->custnum,
7782 'pkgnum' => $pkgnum,
7789 Returns all the payments (see L<FS::cust_pay>) for this customer.
7795 return $self->num_cust_pay unless wantarray;
7796 sort { $a->_date <=> $b->_date }
7797 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
7802 Returns the number of payments (see L<FS::cust_pay>) for this customer. Also
7803 called automatically when the cust_pay method is used in a scalar context.
7809 my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
7810 my $sth = dbh->prepare($sql) or die dbh->errstr;
7811 $sth->execute($self->custnum) or die $sth->errstr;
7812 $sth->fetchrow_arrayref->[0];
7815 =item cust_pay_pkgnum
7817 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
7818 package when using experimental package balances.
7822 sub cust_pay_pkgnum {
7823 my( $self, $pkgnum ) = @_;
7824 map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
7825 sort { $a->_date <=> $b->_date }
7826 qsearch( 'cust_pay', { 'custnum' => $self->custnum,
7827 'pkgnum' => $pkgnum,
7834 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
7840 map { $_ } #return $self->num_cust_pay_void unless wantarray;
7841 sort { $a->_date <=> $b->_date }
7842 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
7845 =item cust_pay_batch
7847 Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
7851 sub cust_pay_batch {
7853 map { $_ } #return $self->num_cust_pay_batch unless wantarray;
7854 sort { $a->paybatchnum <=> $b->paybatchnum }
7855 qsearch( 'cust_pay_batch', { 'custnum' => $self->custnum } )
7858 =item cust_pay_pending
7860 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
7861 (without status "done").
7865 sub cust_pay_pending {
7867 return $self->num_cust_pay_pending unless wantarray;
7868 sort { $a->_date <=> $b->_date }
7869 qsearch( 'cust_pay_pending', {
7870 'custnum' => $self->custnum,
7871 'status' => { op=>'!=', value=>'done' },
7876 =item num_cust_pay_pending
7878 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
7879 customer (without status "done"). Also called automatically when the
7880 cust_pay_pending method is used in a scalar context.
7884 sub num_cust_pay_pending {
7886 my $sql = " SELECT COUNT(*) FROM cust_pay_pending ".
7887 " WHERE custnum = ? AND status != 'done' ";
7888 my $sth = dbh->prepare($sql) or die dbh->errstr;
7889 $sth->execute($self->custnum) or die $sth->errstr;
7890 $sth->fetchrow_arrayref->[0];
7895 Returns all the refunds (see L<FS::cust_refund>) for this customer.
7901 map { $_ } #return $self->num_cust_refund unless wantarray;
7902 sort { $a->_date <=> $b->_date }
7903 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
7906 =item display_custnum
7908 Returns the displayed customer number for this customer: agent_custid if
7909 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
7913 sub display_custnum {
7915 if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
7916 return $self->agent_custid;
7918 return $self->custnum;
7924 Returns a name string for this customer, either "Company (Last, First)" or
7931 my $name = $self->contact;
7932 $name = $self->company. " ($name)" if $self->company;
7938 Returns a name string for this (service/shipping) contact, either
7939 "Company (Last, First)" or "Last, First".
7945 if ( $self->get('ship_last') ) {
7946 my $name = $self->ship_contact;
7947 $name = $self->ship_company. " ($name)" if $self->ship_company;
7956 Returns a name string for this customer, either "Company" or "First Last".
7962 $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
7965 =item ship_name_short
7967 Returns a name string for this (service/shipping) contact, either "Company"
7972 sub ship_name_short {
7974 if ( $self->get('ship_last') ) {
7975 $self->ship_company !~ /^\s*$/
7976 ? $self->ship_company
7977 : $self->ship_contact_firstlast;
7979 $self->name_company_or_firstlast;
7985 Returns this customer's full (billing) contact name only, "Last, First"
7991 $self->get('last'). ', '. $self->first;
7996 Returns this customer's full (shipping) contact name only, "Last, First"
8002 $self->get('ship_last')
8003 ? $self->get('ship_last'). ', '. $self->ship_first
8007 =item contact_firstlast
8009 Returns this customers full (billing) contact name only, "First Last".
8013 sub contact_firstlast {
8015 $self->first. ' '. $self->get('last');
8018 =item ship_contact_firstlast
8020 Returns this customer's full (shipping) contact name only, "First Last".
8024 sub ship_contact_firstlast {
8026 $self->get('ship_last')
8027 ? $self->first. ' '. $self->get('ship_last')
8028 : $self->contact_firstlast;
8033 Returns this customer's full country name
8039 code2country($self->country);
8042 =item geocode DATA_VENDOR
8044 Returns a value for the customer location as encoded by DATA_VENDOR.
8045 Currently this only makes sense for "CCH" as DATA_VENDOR.
8050 my ($self, $data_vendor) = (shift, shift); #always cch for now
8052 my $geocode = $self->get('geocode'); #XXX only one data_vendor for geocode
8053 return $geocode if $geocode;
8055 my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) )
8059 my($zip,$plus4) = split /-/, $self->get("${prefix}zip")
8060 if $self->country eq 'US';
8064 #CCH specific location stuff
8065 my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'";
8067 my @cust_tax_location =
8069 'table' => 'cust_tax_location',
8070 'hashref' => { 'zip' => $zip, 'data_vendor' => $data_vendor },
8071 'extra_sql' => $extra_sql,
8072 'order_by' => 'ORDER BY plus4hi',#overlapping with distinct ends
8075 $geocode = $cust_tax_location[0]->geocode
8076 if scalar(@cust_tax_location);
8085 Returns a status string for this customer, currently:
8089 =item prospect - No packages have ever been ordered
8091 =item active - One or more recurring packages is active
8093 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
8095 =item suspended - All non-cancelled recurring packages are suspended
8097 =item cancelled - All recurring packages are cancelled
8103 sub status { shift->cust_status(@_); }
8107 for my $status (qw( prospect active inactive suspended cancelled )) {
8108 my $method = $status.'_sql';
8109 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
8110 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
8111 $sth->execute( ($self->custnum) x $numnum )
8112 or die "Error executing 'SELECT $sql': ". $sth->errstr;
8113 return $status if $sth->fetchrow_arrayref->[0];
8117 =item ucfirst_cust_status
8119 =item ucfirst_status
8121 Returns the status with the first character capitalized.
8125 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
8127 sub ucfirst_cust_status {
8129 ucfirst($self->cust_status);
8134 Returns a hex triplet color string for this customer's status.
8138 use vars qw(%statuscolor);
8139 tie %statuscolor, 'Tie::IxHash',
8140 'prospect' => '7e0079', #'000000', #black? naw, purple
8141 'active' => '00CC00', #green
8142 'inactive' => '0000CC', #blue
8143 'suspended' => 'FF9900', #yellow
8144 'cancelled' => 'FF0000', #red
8147 sub statuscolor { shift->cust_statuscolor(@_); }
8149 sub cust_statuscolor {
8151 $statuscolor{$self->cust_status};
8156 Returns an array of hashes representing the customer's RT tickets.
8163 my $num = $conf->config('cust_main-max_tickets') || 10;
8166 if ( $conf->config('ticket_system') ) {
8167 unless ( $conf->config('ticket_system-custom_priority_field') ) {
8169 @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
8173 foreach my $priority (
8174 $conf->config('ticket_system-custom_priority_field-values'), ''
8176 last if scalar(@tickets) >= $num;
8178 @{ FS::TicketSystem->customer_tickets( $self->custnum,
8179 $num - scalar(@tickets),
8189 # Return services representing svc_accts in customer support packages
8190 sub support_services {
8192 my %packages = map { $_ => 1 } $conf->config('support_packages');
8194 grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
8195 grep { $_->part_svc->svcdb eq 'svc_acct' }
8196 map { $_->cust_svc }
8197 grep { exists $packages{ $_->pkgpart } }
8198 $self->ncancelled_pkgs;
8202 # Return a list of latitude/longitude for one of the services (if any)
8203 sub service_coordinates {
8207 grep { $_->latitude && $_->longitude }
8209 map { $_->cust_svc }
8210 $self->ncancelled_pkgs;
8212 scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
8217 =head1 CLASS METHODS
8223 Class method that returns the list of possible status strings for customers
8224 (see L<the status method|/status>). For example:
8226 @statuses = FS::cust_main->statuses();
8231 #my $self = shift; #could be class...
8237 Returns an SQL expression identifying prospective cust_main records (customers
8238 with no packages ever ordered)
8242 use vars qw($select_count_pkgs);
8243 $select_count_pkgs =
8244 "SELECT COUNT(*) FROM cust_pkg
8245 WHERE cust_pkg.custnum = cust_main.custnum";
8247 sub select_count_pkgs_sql {
8251 sub prospect_sql { "
8252 0 = ( $select_count_pkgs )
8257 Returns an SQL expression identifying active cust_main records (customers with
8258 active recurring packages).
8263 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
8269 Returns an SQL expression identifying inactive cust_main records (customers with
8270 no active recurring packages, but otherwise unsuspended/uncancelled).
8274 sub inactive_sql { "
8275 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
8277 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
8283 Returns an SQL expression identifying suspended cust_main records.
8288 sub suspended_sql { susp_sql(@_); }
8290 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
8292 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
8298 Returns an SQL expression identifying cancelled cust_main records.
8302 sub cancelled_sql { cancel_sql(@_); }
8305 my $recurring_sql = FS::cust_pkg->recurring_sql;
8306 my $cancelled_sql = FS::cust_pkg->cancelled_sql;
8309 0 < ( $select_count_pkgs )
8310 AND 0 < ( $select_count_pkgs AND $recurring_sql AND $cancelled_sql )
8311 AND 0 = ( $select_count_pkgs AND $recurring_sql
8312 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
8314 AND 0 = ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
8320 =item uncancelled_sql
8322 Returns an SQL expression identifying un-cancelled cust_main records.
8326 sub uncancelled_sql { uncancel_sql(@_); }
8327 sub uncancel_sql { "
8329 ( 0 < ( $select_count_pkgs
8330 AND ( cust_pkg.cancel IS NULL
8331 OR cust_pkg.cancel = 0
8334 OR 0 = ( $select_count_pkgs )
8340 Returns an SQL fragment to retreive the balance.
8345 ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
8346 WHERE cust_bill.custnum = cust_main.custnum )
8347 - ( SELECT COALESCE( SUM(paid), 0 ) FROM cust_pay
8348 WHERE cust_pay.custnum = cust_main.custnum )
8349 - ( SELECT COALESCE( SUM(amount), 0 ) FROM cust_credit
8350 WHERE cust_credit.custnum = cust_main.custnum )
8351 + ( SELECT COALESCE( SUM(refund), 0 ) FROM cust_refund
8352 WHERE cust_refund.custnum = cust_main.custnum )
8355 =item balance_date_sql START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
8357 Returns an SQL fragment to retreive the balance for this customer, only
8358 considering invoices with date earlier than START_TIME, and optionally not
8359 later than END_TIME (total_owed_date minus total_unapplied_credits minus
8360 total_unapplied_payments).
8362 Times are specified as SQL fragments or numeric
8363 UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
8364 L<Date::Parse> for conversion functions. The empty string can be passed
8365 to disable that time constraint completely.
8367 Available options are:
8371 =item unapplied_date
8373 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)
8378 set to true to remove all customer comparison clauses, for totals
8383 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
8388 JOIN clause (typically used with the total option)
8394 sub balance_date_sql {
8395 my( $class, $start, $end, %opt ) = @_;
8397 my $cutoff = $opt{'cutoff'};
8399 my $owed = FS::cust_bill->owed_sql($cutoff);
8400 my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
8401 my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
8402 my $unapp_pay = FS::cust_pay->unapplied_sql($cutoff);
8404 my $j = $opt{'join'} || '';
8406 my $owed_wh = $class->_money_table_where( 'cust_bill', $start,$end,%opt );
8407 my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
8408 my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
8409 my $pay_wh = $class->_money_table_where( 'cust_pay', $start,$end,%opt );
8411 " ( SELECT COALESCE(SUM($owed), 0) FROM cust_bill $j $owed_wh )
8412 + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
8413 - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
8414 - ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $j $pay_wh )
8419 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
8421 Returns an SQL fragment to retreive the total unapplied payments for this
8422 customer, only considering invoices with date earlier than START_TIME, and
8423 optionally not later than END_TIME.
8425 Times are specified as SQL fragments or numeric
8426 UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
8427 L<Date::Parse> for conversion functions. The empty string can be passed
8428 to disable that time constraint completely.
8430 Available options are:
8434 sub unapplied_payments_date_sql {
8435 my( $class, $start, $end, %opt ) = @_;
8437 my $cutoff = $opt{'cutoff'};
8439 my $unapp_pay = FS::cust_pay->unapplied_sql($cutoff);
8441 my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
8442 'unapplied_date'=>1 );
8444 " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
8447 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
8449 Helper method for balance_date_sql; name (and usage) subject to change
8450 (suggestions welcome).
8452 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
8453 cust_refund, cust_credit or cust_pay).
8455 If TABLE is "cust_bill" or the unapplied_date option is true, only
8456 considers records with date earlier than START_TIME, and optionally not
8457 later than END_TIME .
8461 sub _money_table_where {
8462 my( $class, $table, $start, $end, %opt ) = @_;
8465 push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
8466 if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
8467 push @where, "$table._date <= $start" if defined($start) && length($start);
8468 push @where, "$table._date > $end" if defined($end) && length($end);
8470 push @where, @{$opt{'where'}} if $opt{'where'};
8471 my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
8477 =item search HASHREF
8481 Returns a qsearch hash expression to search for parameters specified in
8482 HASHREF. Valid parameters are
8490 =item cancelled_pkgs
8496 listref of start date, end date
8506 =item current_balance
8508 listref (list returned by FS::UI::Web::parse_lt_gt($cgi, 'current_balance'))
8512 =item flattened_pkgs
8521 my ($class, $params) = @_;
8532 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
8534 "cust_main.agentnum = $1";
8541 #prospect active inactive suspended cancelled
8542 if ( grep { $params->{'status'} eq $_ } FS::cust_main->statuses() ) {
8543 my $method = $params->{'status'}. '_sql';
8544 #push @where, $class->$method();
8545 push @where, FS::cust_main->$method();
8549 # parse cancelled package checkbox
8554 $pkgwhere .= "AND (cancel = 0 or cancel is null)"
8555 unless $params->{'cancelled_pkgs'};
8558 # parse without census tract checkbox
8561 push @where, "(censustract = '' or censustract is null)"
8562 if $params->{'no_censustract'};
8568 foreach my $field (qw( signupdate )) {
8570 next unless exists($params->{$field});
8572 my($beginning, $ending) = @{$params->{$field}};
8575 "cust_main.$field IS NOT NULL",
8576 "cust_main.$field >= $beginning",
8577 "cust_main.$field <= $ending";
8579 $orderby ||= "ORDER BY cust_main.$field";
8587 if ( $params->{'payby'} ) {
8589 my @payby = ref( $params->{'payby'} )
8590 ? @{ $params->{'payby'} }
8591 : ( $params->{'payby'} );
8593 @payby = grep /^([A-Z]{4})$/, @{ $params->{'payby'} };
8595 push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )'
8600 my @payby = grep /^([A-Z]{4})$/, @{ $params->{'payby'} };
8602 push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )';
8606 # paydate_year / paydate_month
8609 if ( $params->{'paydate_year'} =~ /^(\d{4})$/ ) {
8611 $params->{'paydate_month'} =~ /^(\d\d?)$/
8612 or die "paydate_year without paydate_month?";
8616 'paydate IS NOT NULL',
8618 "CAST(paydate AS timestamp) < CAST('$year-$month-01' AS timestamp )"
8626 if ( $params->{'invoice_terms'} =~ /^([\w ]+)$/ ) {
8628 if ( $1 eq 'NULL' ) {
8630 "( cust_main.invoice_terms IS NULL OR cust_main.invoice_terms = '' )";
8633 "cust_main.invoice_terms IS NOT NULL",
8634 "cust_main.invoice_terms = '$1'";
8642 if ( $params->{'current_balance'} ) {
8644 #my $balance_sql = $class->balance_sql();
8645 my $balance_sql = FS::cust_main->balance_sql();
8647 my @current_balance =
8648 ref( $params->{'current_balance'} )
8649 ? @{ $params->{'current_balance'} }
8650 : ( $params->{'current_balance'} );
8652 push @where, map { s/current_balance/$balance_sql/; $_ }
8661 if ( $params->{'custbatch'} =~ /^([\w\/\-\:\.]+)$/ and $1 ) {
8663 "cust_main.custbatch = '$1'";
8667 # setup queries, subs, etc. for the search
8670 $orderby ||= 'ORDER BY custnum';
8672 # here is the agent virtualization
8673 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
8675 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
8677 my $addl_from = 'LEFT JOIN cust_pkg USING ( custnum ) ';
8679 my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql";
8681 my $select = join(', ',
8682 'cust_main.custnum',
8683 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
8686 my(@extra_headers) = ();
8687 my(@extra_fields) = ();
8689 if ($params->{'flattened_pkgs'}) {
8691 if ($dbh->{Driver}->{Name} eq 'Pg') {
8693 $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";
8695 }elsif ($dbh->{Driver}->{Name} =~ /^mysql/i) {
8696 $select .= ", GROUP_CONCAT(pkg SEPARATOR '|') as magic";
8697 $addl_from .= " LEFT JOIN part_pkg using ( pkgpart )";
8699 warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
8700 "omitting packing information from report.";
8703 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";
8705 my $sth = dbh->prepare($header_query) or die dbh->errstr;
8706 $sth->execute() or die $sth->errstr;
8707 my $headerrow = $sth->fetchrow_arrayref;
8708 my $headercount = $headerrow ? $headerrow->[0] : 0;
8709 while($headercount) {
8710 unshift @extra_headers, "Package ". $headercount;
8711 unshift @extra_fields, eval q!sub {my $c = shift;
8712 my @a = split '\|', $c->magic;
8713 my $p = $a[!.--$headercount. q!];
8721 'table' => 'cust_main',
8722 'select' => $select,
8724 'extra_sql' => $extra_sql,
8725 'order_by' => $orderby,
8726 'count_query' => $count_query,
8727 'extra_headers' => \@extra_headers,
8728 'extra_fields' => \@extra_fields,
8733 =item email_search_result HASHREF
8737 Emails a notice to the specified customers.
8739 Valid parameters are those of the L<search> method, plus the following:
8761 Optional job queue job for status updates.
8765 Returns an error message, or false for success.
8767 If an error occurs during any email, stops the enture send and returns that
8768 error. Presumably if you're getting SMTP errors aborting is better than
8769 retrying everything.
8773 sub email_search_result {
8774 my($class, $params) = @_;
8776 my $from = delete $params->{from};
8777 my $subject = delete $params->{subject};
8778 my $html_body = delete $params->{html_body};
8779 my $text_body = delete $params->{text_body};
8781 my $job = delete $params->{'job'};
8783 $params->{'payby'} = [ split(/\0/, $params->{'payby'}) ]
8784 unless ref($params->{'payby'});
8786 my $sql_query = $class->search($params);
8788 my $count_query = delete($sql_query->{'count_query'});
8789 my $count_sth = dbh->prepare($count_query)
8790 or die "Error preparing $count_query: ". dbh->errstr;
8792 or die "Error executing $count_query: ". $count_sth->errstr;
8793 my $count_arrayref = $count_sth->fetchrow_arrayref;
8794 my $num_cust = $count_arrayref->[0];
8796 #my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
8797 #my @extra_fields = @{ delete($sql_query->{'extra_fields'}) };
8800 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
8802 #eventually order+limit magic to reduce memory use?
8803 foreach my $cust_main ( qsearch($sql_query) ) {
8805 my $to = $cust_main->invoicing_list_emailonly_scalar;
8808 my $error = send_email(
8812 'subject' => $subject,
8813 'html_body' => $html_body,
8814 'text_body' => $text_body,
8817 return $error if $error;
8819 if ( $job ) { #progressbar foo
8821 if ( time - $min_sec > $last ) {
8822 my $error = $job->update_statustext(
8823 int( 100 * $num / $num_cust )
8825 die $error if $error;
8835 use Storable qw(thaw);
8838 sub process_email_search_result {
8840 #warn "$me process_re_X $method for job $job\n" if $DEBUG;
8842 my $param = thaw(decode_base64(shift));
8843 warn Dumper($param) if $DEBUG;
8845 $param->{'job'} = $job;
8847 $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
8848 unless ref($param->{'payby'});
8850 my $error = FS::cust_main->email_search_result( $param );
8851 die $error if $error;
8855 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
8857 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
8858 records. Currently, I<first>, I<last>, I<company> and/or I<address1> may be
8859 specified (the appropriate ship_ field is also searched).
8861 Additional options are the same as FS::Record::qsearch
8866 my( $self, $fuzzy, $hash, @opt) = @_;
8871 check_and_rebuild_fuzzyfiles();
8872 foreach my $field ( keys %$fuzzy ) {
8874 my $all = $self->all_X($field);
8875 next unless scalar(@$all);
8878 $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
8881 foreach ( keys %match ) {
8882 push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
8883 push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
8886 push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
8889 # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
8891 @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main;
8899 Returns a masked version of the named field
8904 my ($self,$field) = @_;
8908 'x'x(length($self->getfield($field))-4).
8909 substr($self->getfield($field), (length($self->getfield($field))-4));
8919 =item smart_search OPTION => VALUE ...
8921 Accepts the following options: I<search>, the string to search for. The string
8922 will be searched for as a customer number, phone number, name or company name,
8923 as an exact, or, in some cases, a substring or fuzzy match (see the source code
8924 for the exact heuristics used); I<no_fuzzy_on_exact>, causes smart_search to
8925 skip fuzzy matching when an exact match is found.
8927 Any additional options are treated as an additional qualifier on the search
8930 Returns a (possibly empty) array of FS::cust_main objects.
8937 #here is the agent virtualization
8938 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
8942 my $skip_fuzzy = delete $options{'no_fuzzy_on_exact'};
8943 my $search = delete $options{'search'};
8944 ( my $alphanum_search = $search ) =~ s/\W//g;
8946 if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search
8948 #false laziness w/Record::ut_phone
8949 my $phonen = "$1-$2-$3";
8950 $phonen .= " x$4" if $4;
8952 push @cust_main, qsearch( {
8953 'table' => 'cust_main',
8954 'hashref' => { %options },
8955 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
8957 join(' OR ', map "$_ = '$phonen'",
8958 qw( daytime night fax
8959 ship_daytime ship_night ship_fax )
8962 " AND $agentnums_sql", #agent virtualization
8965 unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
8966 #try looking for matches with extensions unless one was specified
8968 push @cust_main, qsearch( {
8969 'table' => 'cust_main',
8970 'hashref' => { %options },
8971 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
8973 join(' OR ', map "$_ LIKE '$phonen\%'",
8975 ship_daytime ship_night )
8978 " AND $agentnums_sql", #agent virtualization
8983 # custnum search (also try agent_custid), with some tweaking options if your
8984 # legacy cust "numbers" have letters
8987 if ( $search =~ /^\s*(\d+)\s*$/
8988 || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
8989 && $search =~ /^\s*(\w\w?\d+)\s*$/
8991 || ( $conf->exists('address1-search' )
8992 && $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D
8999 if ( $num =~ /^(\d+)$/ && $num <= 2147483647 ) { #need a bigint custnum? wow
9000 push @cust_main, qsearch( {
9001 'table' => 'cust_main',
9002 'hashref' => { 'custnum' => $num, %options },
9003 'extra_sql' => " AND $agentnums_sql", #agent virtualization
9007 push @cust_main, qsearch( {
9008 'table' => 'cust_main',
9009 'hashref' => { 'agent_custid' => $num, %options },
9010 'extra_sql' => " AND $agentnums_sql", #agent virtualization
9013 if ( $conf->exists('address1-search') ) {
9014 my $len = length($num);
9016 foreach my $prefix ( '', 'ship_' ) {
9017 push @cust_main, qsearch( {
9018 'table' => 'cust_main',
9019 'hashref' => { %options, },
9021 ( keys(%options) ? ' AND ' : ' WHERE ' ).
9022 " LOWER(SUBSTRING(${prefix}address1 FROM 1 FOR $len)) = '$num' ".
9023 " AND $agentnums_sql",
9028 } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
9030 my($company, $last, $first) = ( $1, $2, $3 );
9032 # "Company (Last, First)"
9033 #this is probably something a browser remembered,
9034 #so just do an exact search (but case-insensitive, so USPS standardization
9035 #doesn't throw a wrench in the works)
9037 foreach my $prefix ( '', 'ship_' ) {
9038 push @cust_main, qsearch( {
9039 'table' => 'cust_main',
9040 'hashref' => { %options },
9042 ( keys(%options) ? ' AND ' : ' WHERE ' ).
9044 " LOWER(${prefix}first) = ". dbh->quote(lc($first)),
9045 " LOWER(${prefix}last) = ". dbh->quote(lc($last)),
9046 " LOWER(${prefix}company) = ". dbh->quote(lc($company)),
9052 } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
9053 # try (ship_){last,company}
9057 # # remove "(Last, First)" in "Company (Last, First)", otherwise the
9058 # # full strings the browser remembers won't work
9059 # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name
9061 use Lingua::EN::NameParse;
9062 my $NameParse = new Lingua::EN::NameParse(
9064 allow_reversed => 1,
9067 my($last, $first) = ( '', '' );
9068 #maybe disable this too and just rely on NameParse?
9069 if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First
9071 ($last, $first) = ( $1, $2 );
9073 #} elsif ( $value =~ /^(.+)\s+(.+)$/ ) {
9074 } elsif ( ! $NameParse->parse($value) ) {
9076 my %name = $NameParse->components;
9077 $first = $name{'given_name_1'};
9078 $last = $name{'surname_1'};
9082 if ( $first && $last ) {
9084 my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) );
9087 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
9089 ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
9090 OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
9093 push @cust_main, qsearch( {
9094 'table' => 'cust_main',
9095 'hashref' => \%options,
9096 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
9099 # or it just be something that was typed in... (try that in a sec)
9103 my $q_value = dbh->quote($value);
9106 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
9107 $sql .= " ( LOWER(last) = $q_value
9108 OR LOWER(company) = $q_value
9109 OR LOWER(ship_last) = $q_value
9110 OR LOWER(ship_company) = $q_value
9112 $sql .= " OR LOWER(address1) = $q_value
9113 OR LOWER(ship_address1) = $q_value
9115 if $conf->exists('address1-search');
9118 push @cust_main, qsearch( {
9119 'table' => 'cust_main',
9120 'hashref' => \%options,
9121 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
9124 #no exact match, trying substring/fuzzy
9125 #always do substring & fuzzy (unless they're explicity config'ed off)
9126 #getting complaints searches are not returning enough
9127 unless ( @cust_main && $skip_fuzzy || $conf->exists('disable-fuzzy') ) {
9129 #still some false laziness w/search (was search/cust_main.cgi)
9134 { 'company' => { op=>'ILIKE', value=>"%$value%" }, },
9135 { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
9138 if ( $first && $last ) {
9141 { 'first' => { op=>'ILIKE', value=>"%$first%" },
9142 'last' => { op=>'ILIKE', value=>"%$last%" },
9144 { 'ship_first' => { op=>'ILIKE', value=>"%$first%" },
9145 'ship_last' => { op=>'ILIKE', value=>"%$last%" },
9152 { 'last' => { op=>'ILIKE', value=>"%$value%" }, },
9153 { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, },
9157 if ( $conf->exists('address1-search') ) {
9159 { 'address1' => { op=>'ILIKE', value=>"%$value%" }, },
9160 { 'ship_address1' => { op=>'ILIKE', value=>"%$value%" }, },
9164 foreach my $hashref ( @hashrefs ) {
9166 push @cust_main, qsearch( {
9167 'table' => 'cust_main',
9168 'hashref' => { %$hashref,
9171 'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton
9180 " AND $agentnums_sql", #extra_sql #agent virtualization
9183 if ( $first && $last ) {
9184 push @cust_main, FS::cust_main->fuzzy_search(
9185 { 'last' => $last, #fuzzy hashref
9186 'first' => $first }, #
9190 foreach my $field ( 'last', 'company' ) {
9192 FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
9194 if ( $conf->exists('address1-search') ) {
9196 FS::cust_main->fuzzy_search( { 'address1' => $value }, @fuzopts );
9203 #eliminate duplicates
9205 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
9213 Accepts the following options: I<email>, the email address to search for. The
9214 email address will be searched for as an email invoice destination and as an
9217 #Any additional options are treated as an additional qualifier on the search
9218 #(i.e. I<agentnum>).
9220 Returns a (possibly empty) array of FS::cust_main objects (but usually just
9230 my $email = delete $options{'email'};
9232 #we're only being used by RT at the moment... no agent virtualization yet
9233 #my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
9237 if ( $email =~ /([^@]+)\@([^@]+)/ ) {
9239 my ( $user, $domain ) = ( $1, $2 );
9241 warn "$me smart_search: searching for $user in domain $domain"
9247 'table' => 'cust_main_invoice',
9248 'hashref' => { 'dest' => $email },
9255 map $_->cust_svc->cust_pkg,
9257 'table' => 'svc_acct',
9258 'hashref' => { 'username' => $user, },
9260 'AND ( SELECT domain FROM svc_domain
9261 WHERE svc_acct.domsvc = svc_domain.svcnum
9262 ) = '. dbh->quote($domain),
9268 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
9270 warn "$me smart_search: found ". scalar(@cust_main). " unique customers"
9277 =item check_and_rebuild_fuzzyfiles
9281 sub check_and_rebuild_fuzzyfiles {
9282 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9283 rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
9286 =item rebuild_fuzzyfiles
9290 sub rebuild_fuzzyfiles {
9292 use Fcntl qw(:flock);
9294 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9295 mkdir $dir, 0700 unless -d $dir;
9297 foreach my $fuzzy ( @fuzzyfields ) {
9299 open(LOCK,">>$dir/cust_main.$fuzzy")
9300 or die "can't open $dir/cust_main.$fuzzy: $!";
9302 or die "can't lock $dir/cust_main.$fuzzy: $!";
9304 open (CACHE,">$dir/cust_main.$fuzzy.tmp")
9305 or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
9307 foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
9308 my $sth = dbh->prepare("SELECT $field FROM cust_main".
9309 " WHERE $field != '' AND $field IS NOT NULL");
9310 $sth->execute or die $sth->errstr;
9312 while ( my $row = $sth->fetchrow_arrayref ) {
9313 print CACHE $row->[0]. "\n";
9318 close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
9320 rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
9331 my( $self, $field ) = @_;
9332 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9333 open(CACHE,"<$dir/cust_main.$field")
9334 or die "can't open $dir/cust_main.$field: $!";
9335 my @array = map { chomp; $_; } <CACHE>;
9340 =item append_fuzzyfiles FIRSTNAME LASTNAME COMPANY ADDRESS1
9344 sub append_fuzzyfiles {
9345 #my( $first, $last, $company ) = @_;
9347 &check_and_rebuild_fuzzyfiles;
9349 use Fcntl qw(:flock);
9351 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9353 foreach my $field (@fuzzyfields) {
9358 open(CACHE,">>$dir/cust_main.$field")
9359 or die "can't open $dir/cust_main.$field: $!";
9360 flock(CACHE,LOCK_EX)
9361 or die "can't lock $dir/cust_main.$field: $!";
9363 print CACHE "$value\n";
9365 flock(CACHE,LOCK_UN)
9366 or die "can't unlock $dir/cust_main.$field: $!";
9381 #warn join('-',keys %$param);
9382 my $fh = $param->{filehandle};
9383 my @fields = @{$param->{fields}};
9385 eval "use Text::CSV_XS;";
9388 my $csv = new Text::CSV_XS;
9395 local $SIG{HUP} = 'IGNORE';
9396 local $SIG{INT} = 'IGNORE';
9397 local $SIG{QUIT} = 'IGNORE';
9398 local $SIG{TERM} = 'IGNORE';
9399 local $SIG{TSTP} = 'IGNORE';
9400 local $SIG{PIPE} = 'IGNORE';
9402 my $oldAutoCommit = $FS::UID::AutoCommit;
9403 local $FS::UID::AutoCommit = 0;
9406 #while ( $columns = $csv->getline($fh) ) {
9408 while ( defined($line=<$fh>) ) {
9410 $csv->parse($line) or do {
9411 $dbh->rollback if $oldAutoCommit;
9412 return "can't parse: ". $csv->error_input();
9415 my @columns = $csv->fields();
9416 #warn join('-',@columns);
9419 foreach my $field ( @fields ) {
9420 $row{$field} = shift @columns;
9423 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
9424 unless ( $cust_main ) {
9425 $dbh->rollback if $oldAutoCommit;
9426 return "unknown custnum $row{'custnum'}";
9429 if ( $row{'amount'} > 0 ) {
9430 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
9432 $dbh->rollback if $oldAutoCommit;
9436 } elsif ( $row{'amount'} < 0 ) {
9437 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
9440 $dbh->rollback if $oldAutoCommit;
9450 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
9452 return "Empty file!" unless $imported;
9458 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
9460 Deprecated. Use event notification and message templates
9461 (L<FS::msg_template>) instead.
9463 Sends a templated email notification to the customer (see L<Text::Template>).
9465 OPTIONS is a hash and may include
9467 I<from> - the email sender (default is invoice_from)
9469 I<to> - comma-separated scalar or arrayref of recipients
9470 (default is invoicing_list)
9472 I<bcc> - blind-copy recipient address (default is none)
9474 I<subject> - The subject line of the sent email notification
9475 (default is "Notice from company_name")
9477 I<extra_fields> - a hashref of name/value pairs which will be substituted
9480 The following variables are vavailable in the template.
9482 I<$first> - the customer first name
9483 I<$last> - the customer last name
9484 I<$company> - the customer company
9485 I<$payby> - a description of the method of payment for the customer
9486 # would be nice to use FS::payby::shortname
9487 I<$payinfo> - the account information used to collect for this customer
9488 I<$expdate> - the expiration of the customer payment in seconds from epoch
9493 my ($self, $template, %options) = @_;
9495 return unless $conf->exists($template);
9497 my $from = $conf->config('invoice_from', $self->agentnum)
9498 if $conf->exists('invoice_from', $self->agentnum);
9499 $from = $options{from} if exists($options{from});
9501 my $to = join(',', $self->invoicing_list_emailonly);
9502 $to = $options{to} if exists($options{to});
9504 my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
9505 if $conf->exists('company_name', $self->agentnum);
9506 $subject = $options{subject} if exists($options{subject});
9508 my $notify_template = new Text::Template (TYPE => 'ARRAY',
9509 SOURCE => [ map "$_\n",
9510 $conf->config($template)]
9512 or die "can't create new Text::Template object: Text::Template::ERROR";
9513 $notify_template->compile()
9514 or die "can't compile template: Text::Template::ERROR";
9516 $FS::notify_template::_template::company_name =
9517 $conf->config('company_name', $self->agentnum);
9518 $FS::notify_template::_template::company_address =
9519 join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
9521 my $paydate = $self->paydate || '2037-12-31';
9522 $FS::notify_template::_template::first = $self->first;
9523 $FS::notify_template::_template::last = $self->last;
9524 $FS::notify_template::_template::company = $self->company;
9525 $FS::notify_template::_template::payinfo = $self->mask_payinfo;
9526 my $payby = $self->payby;
9527 my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
9528 my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
9530 #credit cards expire at the end of the month/year of their exp date
9531 if ($payby eq 'CARD' || $payby eq 'DCRD') {
9532 $FS::notify_template::_template::payby = 'credit card';
9533 ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
9534 $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
9536 }elsif ($payby eq 'COMP') {
9537 $FS::notify_template::_template::payby = 'complimentary account';
9539 $FS::notify_template::_template::payby = 'current method';
9541 $FS::notify_template::_template::expdate = $expire_time;
9543 for (keys %{$options{extra_fields}}){
9545 ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
9548 send_email(from => $from,
9550 bcc => $options{bcc},
9551 subject => $subject,
9552 body => $notify_template->fill_in( PACKAGE =>
9553 'FS::notify_template::_template' ),
9558 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
9560 Generates a templated notification to the customer (see L<Text::Template>).
9562 OPTIONS is a hash and may include
9564 I<extra_fields> - a hashref of name/value pairs which will be substituted
9565 into the template. These values may override values mentioned below
9566 and those from the customer record.
9568 The following variables are available in the template instead of or in addition
9569 to the fields of the customer record.
9571 I<$payby> - a description of the method of payment for the customer
9572 # would be nice to use FS::payby::shortname
9573 I<$payinfo> - the masked account information used to collect for this customer
9574 I<$expdate> - the expiration of the customer payment method in seconds from epoch
9575 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
9579 # a lot like cust_bill::print_latex
9580 sub generate_letter {
9581 my ($self, $template, %options) = @_;
9583 return unless $conf->exists($template);
9585 my $letter_template = new Text::Template
9587 SOURCE => [ map "$_\n", $conf->config($template)],
9588 DELIMITERS => [ '[@--', '--@]' ],
9590 or die "can't create new Text::Template object: Text::Template::ERROR";
9592 $letter_template->compile()
9593 or die "can't compile template: Text::Template::ERROR";
9595 my %letter_data = map { $_ => $self->$_ } $self->fields;
9596 $letter_data{payinfo} = $self->mask_payinfo;
9598 #my $paydate = $self->paydate || '2037-12-31';
9599 my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
9601 my $payby = $self->payby;
9602 my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
9603 my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
9605 #credit cards expire at the end of the month/year of their exp date
9606 if ($payby eq 'CARD' || $payby eq 'DCRD') {
9607 $letter_data{payby} = 'credit card';
9608 ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
9609 $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
9611 }elsif ($payby eq 'COMP') {
9612 $letter_data{payby} = 'complimentary account';
9614 $letter_data{payby} = 'current method';
9616 $letter_data{expdate} = $expire_time;
9618 for (keys %{$options{extra_fields}}){
9619 $letter_data{$_} = $options{extra_fields}->{$_};
9622 unless(exists($letter_data{returnaddress})){
9623 my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
9624 $self->agent_template)
9626 if ( length($retadd) ) {
9627 $letter_data{returnaddress} = $retadd;
9628 } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
9629 $letter_data{returnaddress} =
9630 join( "\n", map { s/( {2,})/'~' x length($1)/eg;
9634 ( $conf->config('company_name', $self->agentnum),
9635 $conf->config('company_address', $self->agentnum),
9639 $letter_data{returnaddress} = '~';
9643 $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
9645 $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
9647 my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
9649 my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
9653 ) or die "can't open temp file: $!\n";
9654 print $lh $conf->config_binary('logo.eps', $self->agentnum)
9655 or die "can't write temp file: $!\n";
9657 $letter_data{'logo_file'} = $lh->filename;
9659 my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
9663 ) or die "can't open temp file: $!\n";
9665 $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
9667 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
9668 return ($1, $letter_data{'logo_file'});
9672 =item print_ps TEMPLATE
9674 Returns an postscript letter filled in from TEMPLATE, as a scalar.
9680 my($file, $lfile) = $self->generate_letter(@_);
9681 my $ps = FS::Misc::generate_ps($file);
9682 unlink($file.'.tex');
9688 =item print TEMPLATE
9690 Prints the filled in template.
9692 TEMPLATE is the name of a L<Text::Template> to fill in and print.
9696 sub queueable_print {
9699 my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
9700 or die "invalid customer number: " . $opt{custvnum};
9702 my $error = $self->print( $opt{template} );
9703 die $error if $error;
9707 my ($self, $template) = (shift, shift);
9708 do_print [ $self->print_ps($template) ];
9711 #these three subs should just go away once agent stuff is all config overrides
9713 sub agent_template {
9715 $self->_agent_plandata('agent_templatename');
9718 sub agent_invoice_from {
9720 $self->_agent_plandata('agent_invoice_from');
9723 sub _agent_plandata {
9724 my( $self, $option ) = @_;
9726 #yuck. this whole thing needs to be reconciled better with 1.9's idea of
9727 #agent-specific Conf
9729 use FS::part_event::Condition;
9731 my $agentnum = $self->agentnum;
9734 if ( driver_name =~ /^Pg/i ) {
9736 } elsif ( driver_name =~ /^mysql/i ) {
9739 die "don't know how to use regular expressions in ". driver_name. " databases";
9742 my $part_event_option =
9744 'select' => 'part_event_option.*',
9745 'table' => 'part_event_option',
9747 LEFT JOIN part_event USING ( eventpart )
9748 LEFT JOIN part_event_option AS peo_agentnum
9749 ON ( part_event.eventpart = peo_agentnum.eventpart
9750 AND peo_agentnum.optionname = 'agentnum'
9751 AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
9753 LEFT JOIN part_event_condition
9754 ON ( part_event.eventpart = part_event_condition.eventpart
9755 AND part_event_condition.conditionname = 'cust_bill_age'
9757 LEFT JOIN part_event_condition_option
9758 ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
9759 AND part_event_condition_option.optionname = 'age'
9762 #'hashref' => { 'optionname' => $option },
9763 #'hashref' => { 'part_event_option.optionname' => $option },
9765 " WHERE part_event_option.optionname = ". dbh->quote($option).
9766 " AND action = 'cust_bill_send_agent' ".
9767 " AND ( disabled IS NULL OR disabled != 'Y' ) ".
9768 " AND peo_agentnum.optionname = 'agentnum' ".
9769 " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
9771 CASE WHEN part_event_condition_option.optionname IS NULL
9773 ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
9775 , part_event.weight".
9779 unless ( $part_event_option ) {
9780 return $self->agent->invoice_template || ''
9781 if $option eq 'agent_templatename';
9785 $part_event_option->optionvalue;
9789 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
9791 Subroutine (not a method), designed to be called from the queue.
9793 Takes a list of options and values.
9795 Pulls up the customer record via the custnum option and calls bill_and_collect.
9800 my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
9802 my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
9803 warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
9805 $cust_main->bill_and_collect( %args );
9808 sub _upgrade_data { #class method
9809 my ($class, %opts) = @_;
9811 my $sql = 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL';
9812 my $sth = dbh->prepare($sql) or die dbh->errstr;
9813 $sth->execute or die $sth->errstr;
9823 The delete method should possibly take an FS::cust_main object reference
9824 instead of a scalar customer number.
9826 Bill and collect options should probably be passed as references instead of a
9829 There should probably be a configuration file with a list of allowed credit
9832 No multiple currency support (probably a larger project than just this module).
9834 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
9836 Birthdates rely on negative epoch values.
9838 The payby for card/check batches is broken. With mixed batching, bad
9841 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
9845 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
9846 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
9847 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.