4 use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin
6 use vars qw( $DEBUG $me $conf @encrypted_fields
7 $unsuspendauto $ignore_noapply
10 use Business::CreditCard;
12 use FS::Misc::DateTime qw( parse_datetime ); #for batch_import
13 use FS::Record qw( dbh qsearch qsearchs );
14 use FS::UID qw( driver_name );
17 use FS::cust_main_Mixin;
18 use FS::payinfo_transaction_Mixin;
20 use FS::cust_bill_pay;
21 use FS::cust_pay_refund;
24 use FS::cust_pay_void;
25 use FS::upgrade_journal;
30 $me = '[FS::cust_pay]';
34 #ask FS::UID to run this stuff for us later
35 FS::UID->install_callback( sub {
37 $unsuspendauto = $conf->exists('unsuspendauto');
40 @encrypted_fields = ('payinfo');
41 sub nohistory_fields { ('payinfo'); }
45 FS::cust_pay - Object methods for cust_pay objects
51 $record = new FS::cust_pay \%hash;
52 $record = new FS::cust_pay { 'column' => 'value' };
54 $error = $record->insert;
56 $error = $new_record->replace($old_record);
58 $error = $record->delete;
60 $error = $record->check;
64 An FS::cust_pay object represents a payment; the transfer of money from a
65 customer. FS::cust_pay inherits from FS::Record. The following fields are
72 primary key (assigned automatically for new payments)
76 customer (see L<FS::cust_main>)
80 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
81 L<Time::Local> and L<Date::Parse> for conversion functions.
85 Amount of this payment
89 order taker (see L<FS::access_user>)
93 Payment Type (See L<FS::payinfo_Mixin> for valid values)
97 Payment Information (See L<FS::payinfo_Mixin> for data format)
101 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
105 obsolete text field for tracking card processing or other batch grouping
109 Optional unique identifer to prevent duplicate transactions.
113 books closed flag, empty or `Y'
117 Desired pkgnum when using experimental package balances.
121 The bank where the payment was deposited.
125 The name of the depositor.
129 The deposit account number.
137 The number of the batch this payment came from (see L<FS::pay_batch>),
138 or null if it was processed through a realtime gateway or entered manually.
142 The number of the realtime or batch gateway L<FS::payment_gateway>) this
143 payment was processed through. Null if it was entered manually or processed
144 by the "system default" gateway, which doesn't have a number.
148 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
149 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
150 redundant with C<gatewaynum>.
154 The authorization number returned by the credit card network.
158 The transaction ID returned by the gateway, if any. This is usually what
159 you would use to initiate a void or refund of the payment.
169 Creates a new payment. To add the payment to the databse, see L<"insert">.
173 sub table { 'cust_pay'; }
174 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum; }
175 sub cust_unlinked_msg {
177 "WARNING: can't find cust_main.custnum ". $self->custnum.
178 ' (cust_pay.paynum '. $self->paynum. ')';
181 =item insert [ OPTION => VALUE ... ]
183 Adds this payment to the database.
185 For backwards-compatibility and convenience, if the additional field invnum
186 is defined, an FS::cust_bill_pay record for the full amount of the payment
187 will be created. In this case, custnum is optional.
189 If the additional field discount_term is defined then a prepayment discount
190 is taken for that length of time. It is an error for the customer to owe
191 after this payment is made.
193 A hash of optional arguments may be passed. The following arguments are
200 If true, a payment receipt is sent instead of a statement when
201 'payment_receipt_email' configuration option is set.
203 About the "manual" flag: Normally, if the 'payment_receipt' config option
204 is set, and the customer has an invoice email address, inserting a payment
205 causes a I<statement> to be emailed to the customer. If the payment is
206 considered "manual" (or if the customer has no invoices), then it will
207 instead send a I<payment receipt>. "manual" should be true whenever a
208 payment is created directly from the web interface, from a user-initiated
209 realtime payment, or from a third-party payment via self-service. It should
210 be I<false> when creating a payment from a billing event or from a batch.
214 Don't send an email receipt. (Note: does not currently work when
215 payment_receipt-trigger is set to something other than default / cust_bill)
222 my($self, %options) = @_;
224 local $SIG{HUP} = 'IGNORE';
225 local $SIG{INT} = 'IGNORE';
226 local $SIG{QUIT} = 'IGNORE';
227 local $SIG{TERM} = 'IGNORE';
228 local $SIG{TSTP} = 'IGNORE';
229 local $SIG{PIPE} = 'IGNORE';
231 my $oldAutoCommit = $FS::UID::AutoCommit;
232 local $FS::UID::AutoCommit = 0;
236 if ( $self->invnum ) {
237 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
239 $dbh->rollback if $oldAutoCommit;
240 return "Unknown cust_bill.invnum: ". $self->invnum;
242 if ($self->custnum && ($cust_bill->custnum ne $self->custnum)) {
243 $dbh->rollback if $oldAutoCommit;
244 return "Invoice custnum ".$cust_bill->custnum
245 ." does not match specified custnum ".$self->custnum
246 ." for invoice ".$self->invnum;
248 $self->custnum($cust_bill->custnum );
251 my $error = $self->check;
252 return $error if $error;
254 my $cust_main = $self->cust_main;
255 my $old_balance = $cust_main->balance;
257 $error = $self->SUPER::insert;
259 $dbh->rollback if $oldAutoCommit;
260 return "error inserting cust_pay: $error";
263 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
264 if ( my $months = $self->discount_term ) {
265 # XXX this should be moved out somewhere, but discount_term_values
267 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
268 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
270 # %billing_pkgs contains this customer's active monthly packages.
271 # Recurring fees for those packages will be credited and then rebilled
272 # for the full discount term. Other packages on the last invoice
273 # (canceled, non-monthly recurring, or one-time charges) will be
275 my %billing_pkgs = map { $_->pkgnum => $_ }
276 grep { $_->part_pkg->freq eq '1' }
277 $cust_main->billing_pkgs;
278 my $credit = 0; # sum of recurring charges from that invoice
279 my $last_bill_date = 0; # the real bill date
280 foreach my $item ( $cust_bill->cust_bill_pkg ) {
281 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
282 $credit += $item->recur;
283 $last_bill_date = $item->cust_pkg->last_bill
284 if defined($item->cust_pkg)
285 and $item->cust_pkg->last_bill > $last_bill_date
288 my $cust_credit = new FS::cust_credit {
289 'custnum' => $self->custnum,
290 'amount' => sprintf('%.2f', $credit),
291 'reason' => 'customer chose to prepay for discount',
293 $error = $cust_credit->insert('reason_type' => $credit_type);
295 $dbh->rollback if $oldAutoCommit;
296 return "error inserting prepayment credit: $error";
300 # bill for the entire term
301 $_->bill($_->last_bill) foreach (values %billing_pkgs);
302 $error = $cust_main->bill(
303 # no recurring_only, we want unbilled packages with start dates to
305 'no_usage_reset' => 1,
306 'time' => $last_bill_date, # not $cust_bill->_date
307 'pkg_list' => [ values %billing_pkgs ],
308 'freq_override' => $months,
311 $dbh->rollback if $oldAutoCommit;
312 return "error inserting cust_pay: $error";
314 $error = $cust_main->apply_payments_and_credits;
316 $dbh->rollback if $oldAutoCommit;
317 return "error inserting cust_pay: $error";
319 my $new_balance = $cust_main->balance;
320 if ($new_balance > 0) {
321 $dbh->rollback if $oldAutoCommit;
322 return "balance after prepay discount attempt: $new_balance";
324 # user friendly: override the "apply only to this invoice" mode
331 if ( $self->invnum ) {
332 my $cust_bill_pay = new FS::cust_bill_pay {
333 'invnum' => $self->invnum,
334 'paynum' => $self->paynum,
335 'amount' => $self->paid,
336 '_date' => $self->_date,
338 $error = $cust_bill_pay->insert(%options);
340 if ( $ignore_noapply ) {
341 warn "warning: error inserting cust_bill_pay: $error ".
342 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
344 $dbh->rollback if $oldAutoCommit;
345 return "error inserting cust_bill_pay: $error";
350 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
352 #false laziness w/ cust_credit::insert
353 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
354 my @errors = $cust_main->unsuspend;
356 # side-fx with nested transactions? upstack rolls back?
357 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
363 #bill setup fees for voip_cdr bill_every_call packages
364 #some false laziness w/search in freeside-cdrd
366 'LEFT JOIN part_pkg USING ( pkgpart ) '.
367 "LEFT JOIN part_pkg_option
368 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
369 AND part_pkg_option.optionname = 'bill_every_call' )";
371 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
372 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
374 my @cust_pkg = qsearch({
375 'table' => 'cust_pkg',
376 'addl_from' => $addl_from,
377 'hashref' => { 'custnum' => $self->custnum,
381 'extra_sql' => $extra_sql,
385 warn "voip_cdr bill_every_call packages found; billing customer\n";
386 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
388 warn "WARNING: Error billing customer: $bill_error\n";
391 #end of billing setup fees for voip_cdr bill_every_call packages
393 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
396 my $trigger = $conf->config('payment_receipt-trigger',
397 $self->cust_main->agentnum) || 'cust_pay';
398 if ( $trigger eq 'cust_pay' ) {
399 my $error = $self->send_receipt(
400 'manual' => $options{'manual'},
401 'noemail' => $options{'noemail'},
402 'cust_bill' => $cust_bill,
403 'cust_main' => $cust_main,
405 warn "can't send payment receipt/statement: $error" if $error;
412 =item void [ REASON ]
414 Voids this payment: deletes the payment and all associated applications and
415 adds a record of the voided payment to the FS::cust_pay_void table.
422 local $SIG{HUP} = 'IGNORE';
423 local $SIG{INT} = 'IGNORE';
424 local $SIG{QUIT} = 'IGNORE';
425 local $SIG{TERM} = 'IGNORE';
426 local $SIG{TSTP} = 'IGNORE';
427 local $SIG{PIPE} = 'IGNORE';
429 my $oldAutoCommit = $FS::UID::AutoCommit;
430 local $FS::UID::AutoCommit = 0;
433 my $cust_pay_void = new FS::cust_pay_void ( {
434 map { $_ => $self->get($_) } $self->fields
436 $cust_pay_void->reason(shift) if scalar(@_);
437 my $error = $cust_pay_void->insert;
439 my $cust_pay_pending =
440 qsearchs('cust_pay_pending', { paynum => $self->paynum });
441 if ( $cust_pay_pending ) {
442 $cust_pay_pending->set('void_paynum', $self->paynum);
443 $cust_pay_pending->set('paynum', '');
444 $error ||= $cust_pay_pending->replace;
447 $error ||= $self->delete;
450 $dbh->rollback if $oldAutoCommit;
454 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
462 Unless the closed flag is set, deletes this payment and all associated
463 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
464 cases, you want to use the void method instead to leave a record of the
469 # very similar to FS::cust_credit::delete
472 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
474 local $SIG{HUP} = 'IGNORE';
475 local $SIG{INT} = 'IGNORE';
476 local $SIG{QUIT} = 'IGNORE';
477 local $SIG{TERM} = 'IGNORE';
478 local $SIG{TSTP} = 'IGNORE';
479 local $SIG{PIPE} = 'IGNORE';
481 my $oldAutoCommit = $FS::UID::AutoCommit;
482 local $FS::UID::AutoCommit = 0;
485 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
486 my $error = $app->delete;
488 $dbh->rollback if $oldAutoCommit;
493 my $error = $self->SUPER::delete(@_);
495 $dbh->rollback if $oldAutoCommit;
499 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
505 =item replace [ OLD_RECORD ]
507 You can, but probably shouldn't modify payments...
509 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
510 supplied, replaces this record. If there is an error, returns the error,
511 otherwise returns false.
517 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
518 $self->SUPER::replace(@_);
523 Checks all fields to make sure this is a valid payment. If there is an error,
524 returns the error, otherwise returns false. Called by the insert method.
531 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
534 $self->ut_numbern('paynum')
535 || $self->ut_numbern('custnum')
536 || $self->ut_numbern('_date')
537 || $self->ut_money('paid')
538 || $self->ut_alphan('otaker')
539 || $self->ut_textn('paybatch')
540 || $self->ut_textn('payunique')
541 || $self->ut_enum('closed', [ '', 'Y' ])
542 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
543 || $self->ut_textn('bank')
544 || $self->ut_alphan('depositor')
545 || $self->ut_numbern('account')
546 || $self->ut_numbern('teller')
547 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
548 || $self->payinfo_check()
550 return $error if $error;
552 return "paid must be > 0 " if $self->paid <= 0;
554 return "unknown cust_main.custnum: ". $self->custnum
556 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
558 $self->_date(time) unless $self->_date;
560 return "invalid discount_term"
561 if ($self->discount_term && $self->discount_term < 2);
563 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
564 foreach (qw(bank depositor account teller)) {
565 return "$_ required" if $self->get($_) eq '';
569 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
570 # # UNIQUE index should catch this too, without race conditions, but this
571 # # should give a better error message the other 99.9% of the time...
572 # if ( length($self->payunique)
573 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
574 # #well, it *could* be a better error message
575 # return "duplicate transaction".
576 # " - a payment with unique identifer ". $self->payunique.
583 =item send_receipt HASHREF | OPTION => VALUE ...
585 Sends a payment receipt for this payment..
593 Flag indicating the payment is being made manually.
597 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
602 Customer (FS::cust_main) object (for efficiency).
606 Don't send an email receipt.
616 my $opt = ref($_[0]) ? shift : { @_ };
618 my $cust_bill = $opt->{'cust_bill'};
619 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
621 my $conf = new FS::Conf;
623 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
625 my @invoicing_list = $cust_main->invoicing_list_emailonly;
626 return '' unless @invoicing_list;
628 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
632 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
633 #|| ! $conf->exists('invoice_html_statement')
637 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
640 my %substitutions = ();
641 $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
643 my $queue = new FS::queue {
644 'job' => 'FS::Misc::process_send_email',
645 'paynum' => $self->paynum,
646 'custnum' => $cust_main->custnum,
648 $error = $queue->insert(
649 FS::msg_template->by_key($msgnum)->prepare(
650 'cust_main' => $cust_main,
652 'from_config' => 'payment_receipt_from',
653 'substitutions' => \%substitutions,
655 'msgtype' => 'receipt', # override msg_template's default
658 } elsif ( $conf->exists('payment_receipt_email') ) {
660 my $receipt_template = new Text::Template (
662 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
664 warn "can't create payment receipt template: $Text::Template::ERROR";
668 my $payby = $self->payby;
669 my $payinfo = $self->payinfo;
670 $payby =~ s/^BILL$/Check/ if $payinfo;
671 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
672 $payinfo = $self->paymask
674 $payinfo = $self->decrypt($payinfo);
676 $payby =~ s/^CHEK$/Electronic check/;
679 'date' => time2str("%a %B %o, %Y", $self->_date),
680 'name' => $cust_main->name,
681 'paynum' => $self->paynum,
682 'paid' => sprintf("%.2f", $self->paid),
683 'payby' => ucfirst(lc($payby)),
684 'payinfo' => $payinfo,
685 'balance' => $cust_main->balance,
686 'company_name' => $conf->config('company_name', $cust_main->agentnum),
689 $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
691 if ( $opt->{'cust_pkg'} ) {
692 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
693 #setup date, other things?
696 my $queue = new FS::queue {
697 'job' => 'FS::Misc::process_send_generated_email',
698 'paynum' => $self->paynum,
699 'custnum' => $cust_main->custnum,
700 'msgtype' => 'receipt',
702 $error = $queue->insert(
703 'from' => $conf->invoice_from_full( $cust_main->agentnum ),
704 #invoice_from??? well as good as any
705 'to' => \@invoicing_list,
706 'subject' => 'Payment receipt',
707 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
712 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
716 #not manual and no noemail flag (here or on the customer)
717 } elsif ( ! $opt->{'noemail'} && ! $cust_main->invoice_noemail ) {
719 my $queue = new FS::queue {
720 'job' => 'FS::cust_bill::queueable_email',
721 'paynum' => $self->paynum,
722 'custnum' => $cust_main->custnum,
726 'invnum' => $cust_bill->invnum,
730 if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
731 $opt{'mode'} = $mode;
733 # backward compatibility, no good fix for this yet as some people may
734 # still have "invoice_latex_statement" and such options
735 $opt{'template'} = 'statement';
736 $opt{'notice_name'} = 'Statement';
739 $error = $queue->insert(%opt);
743 warn "send_receipt: $error\n" if $error;
748 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
755 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
756 sort { $a->_date <=> $b->_date
757 || $a->invnum <=> $b->invnum }
758 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
762 =item cust_pay_refund
764 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
769 sub cust_pay_refund {
771 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
772 sort { $a->_date <=> $b->_date }
773 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
780 Returns the amount of this payment that is still unapplied; which is
781 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
782 applications (see L<FS::cust_pay_refund>).
788 my $amount = $self->paid;
789 $amount -= $_->amount foreach ( $self->cust_bill_pay );
790 $amount -= $_->amount foreach ( $self->cust_pay_refund );
791 sprintf("%.2f", $amount );
796 Returns the amount of this payment that has not been refuned; which is
797 paid minus all refund applications (see L<FS::cust_pay_refund>).
803 my $amount = $self->paid;
804 $amount -= $_->amount foreach ( $self->cust_pay_refund );
805 sprintf("%.2f", $amount );
810 Returns the "paid" field.
825 =item batch_insert CUST_PAY_OBJECT, ...
827 Class method which inserts multiple payments. Takes a list of FS::cust_pay
828 objects. Returns a list, each element representing the status of inserting the
829 corresponding payment - empty. If there is an error inserting any payment, the
830 entire transaction is rolled back, i.e. all payments are inserted or none are.
832 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
833 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
834 those objects will be inserted with the paynum of the payment, and for
835 each one, an error message or an empty string will be inserted into the
840 my @errors = FS::cust_pay->batch_insert(@cust_pay);
841 my $num_errors = scalar(grep $_, @errors);
842 if ( $num_errors == 0 ) {
843 #success; all payments were inserted
845 #failure; no payments were inserted.
851 my $self = shift; #class method
853 local $SIG{HUP} = 'IGNORE';
854 local $SIG{INT} = 'IGNORE';
855 local $SIG{QUIT} = 'IGNORE';
856 local $SIG{TERM} = 'IGNORE';
857 local $SIG{TSTP} = 'IGNORE';
858 local $SIG{PIPE} = 'IGNORE';
860 my $oldAutoCommit = $FS::UID::AutoCommit;
861 local $FS::UID::AutoCommit = 0;
867 foreach my $cust_pay (@_) {
868 my $error = $cust_pay->insert( 'manual' => 1 );
869 push @errors, $error;
870 $num_errors++ if $error;
872 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
874 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
875 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
879 $cust_bill_pay->set('paynum', $cust_pay->paynum);
880 my $apply_error = $cust_bill_pay->insert;
881 push @errors, $apply_error || '';
882 $num_errors++ if $apply_error;
886 } elsif ( !$error ) { #normal case: apply payments as usual
887 $cust_pay->cust_main->apply_payments;
893 $dbh->rollback if $oldAutoCommit;
895 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
904 Returns an SQL fragment to retreive the unapplied amount.
909 my ($class, $start, $end) = @_;
910 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
911 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
912 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
913 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
917 ( SELECT SUM(amount) FROM cust_bill_pay
918 WHERE cust_pay.paynum = cust_bill_pay.paynum
919 $bill_start $bill_end )
923 ( SELECT SUM(amount) FROM cust_pay_refund
924 WHERE cust_pay.paynum = cust_pay_refund.paynum
925 $refund_start $refund_end )
934 my @fields = grep { $_ ne 'payinfo' } $self->fields;
935 +{ ( map { $_=>$self->$_ } @fields ),
941 # Used by FS::Upgrade to migrate to a new database.
945 sub _upgrade_data { #class method
946 my ($class, %opt) = @_;
948 warn "$me upgrading $class\n" if $DEBUG;
950 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
953 # otaker/ivan upgrade
956 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
958 #not the most efficient, but hey, it only has to run once
960 my $where = " WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' )
962 AND EXISTS ( SELECT 1 FROM cust_main
963 WHERE cust_main.custnum = cust_pay.custnum )
966 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
968 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
969 $sth->execute or die $sth->errstr;
970 my $total = $sth->fetchrow_arrayref->[0];
971 #warn "$total cust_pay records to update\n"
973 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
978 my @cust_pay = qsearch( {
979 'table' => 'cust_pay',
981 'extra_sql' => $where,
982 'order_by' => 'ORDER BY paynum',
985 foreach my $cust_pay (@cust_pay) {
987 my $h_cust_pay = $cust_pay->h_search('insert');
989 next if $cust_pay->otaker eq $h_cust_pay->history_user;
990 #$cust_pay->otaker($h_cust_pay->history_user);
991 $cust_pay->set('otaker', $h_cust_pay->history_user);
993 $cust_pay->set('otaker', 'legacy');
996 my $error = $cust_pay->replace;
999 warn " *** WARNING: Error updating order taker for payment paynum ".
1000 $cust_pay->paynun. ": $error\n";
1005 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1006 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1012 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1016 # payinfo N/A upgrade
1019 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1021 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1023 my @na_cust_pay = qsearch( {
1024 'table' => 'cust_pay',
1025 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1026 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1029 foreach my $na ( @na_cust_pay ) {
1031 next unless $na->payinfo eq 'N/A';
1033 my $cust_pay_pending =
1034 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1035 unless ( $cust_pay_pending ) {
1036 warn " *** WARNING: not-yet recoverable N/A card for payment ".
1037 $na->paynum. " (no cust_pay_pending)\n";
1040 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1041 my $error = $na->replace;
1043 warn " *** WARNING: Error updating payinfo for payment paynum ".
1044 $na->paynun. ": $error\n";
1050 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1054 # otaker->usernum upgrade
1057 $class->_upgrade_otaker(%opt);
1059 # if we do this anywhere else, it should become an FS::Upgrade method
1060 my $num_to_upgrade = $class->count('paybatch is not null');
1061 my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1062 if ( $num_to_upgrade > 0 ) {
1063 warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1064 if ( $opt{queue} ) {
1065 if ( $num_jobs > 0 ) {
1066 warn "Upgrade already queued.\n";
1068 warn "Scheduling upgrade.\n";
1069 my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1073 process_upgrade_paybatch();
1078 sub process_upgrade_paybatch {
1080 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1081 local $FS::UID::AutoCommit = 1;
1084 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1086 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1087 my $search = FS::Cursor->new( {
1088 'table' => 'cust_pay',
1089 'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1091 while (my $cust_pay = $search->fetch) {
1092 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1093 $cust_pay->set('paybatch' => '');
1094 my $error = $cust_pay->replace;
1095 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1100 # migrate gateway info from the misused 'paybatch' field
1103 # not only cust_pay, but also voided and refunded payments
1104 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1105 local $FS::Record::nowarn_classload=1;
1106 # really inefficient, but again, only has to run once
1107 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1108 my $and_batchnum_is_null =
1109 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1110 my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1111 my $search = FS::Cursor->new({
1113 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1114 "AND (paybatch IS NOT NULL ".
1115 "OR (paybatch IS NULL AND auth IS NULL
1116 $and_batchnum_is_null ) )
1117 ORDER BY $pkey DESC"
1119 while ( my $object = $search->fetch ) {
1120 if ( $object->paybatch eq '' ) {
1121 # repair for a previous upgrade that didn't save 'auth'
1122 my $pkey = $object->primary_key;
1123 # find the last history record that had a paybatch value
1125 table => "h_$table",
1127 $pkey => $object->$pkey,
1128 paybatch => { op=>'!=', value=>''},
1129 history_action => 'replace_old',
1131 order_by => 'ORDER BY history_date DESC LIMIT 1',
1134 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1137 # if the paybatch didn't have an auth string, then it's fine
1138 $h->paybatch =~ /:(\w+):/ or next;
1139 # set paybatch to what it was in that record
1140 $object->set('paybatch', $h->paybatch)
1141 # and then upgrade it like the old records
1144 my $parsed = $object->_parse_paybatch;
1145 if (keys %$parsed) {
1146 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1147 $object->set('auth' => $parsed->{authorization});
1148 $object->set('paybatch', '');
1149 my $error = $object->replace;
1150 warn "error parsing CARD/CHEK paybatch fields on $object #".
1151 $object->get($object->primary_key).":\n $error\n"
1156 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1166 =item process_batch_import
1170 sub process_batch_import {
1175 my $custnum = $hash{'custnum'};
1176 my $agentnum = $hash{'agentnum'};
1177 my $agent_custid = $hash{'agent_custid'};
1179 $hash{'_date'} = parse_datetime($hash{'_date'})
1180 if $hash{'_date'} && $hash{'_date'} =~ /\D/;
1181 #remove custnum_prefix
1182 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1183 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1186 && $custnum =~ /^$custnum_prefix(0*([1-9]\d*))$/
1187 && length($1) == $custnum_length
1191 # check agentnum against custnum and
1192 # translate agent_custid into regular custnum
1193 if ($custnum && $agent_custid) {
1194 die "can't specify both custnum and agent_custid\n";
1195 } elsif ($agentnum || $agent_custid) {
1196 # here is the agent virtualization
1197 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1199 $search{'agentnum'} = $agentnum
1201 $search{'agent_custid'} = $agent_custid
1203 $search{'custnum'} = $custnum
1205 my $cust_main = qsearchs({
1206 'table' => 'cust_main',
1207 'hashref' => \%search,
1208 'extra_sql' => $extra_sql,
1210 die "can't find customer with" .
1211 ($agentnum ? " agentnum $agentnum" : '') .
1212 ($custnum ? " custnum $custnum" : '') .
1213 ($agent_custid ? " agent_custid $agent_custid" : '') . "\n"
1215 die "mismatched customer number\n"
1216 if $custnum && ($custnum ne $cust_main->custnum);
1217 $custnum = $cust_main->custnum;
1219 $hash{'custnum'} = $custnum;
1220 delete($hash{'agent_custid'});
1224 my $opt = { 'table' => 'cust_pay',
1225 'params' => [ '_date', 'agentnum', 'payby', 'paybatch' ],
1226 #agent_custid isn't a cust_pay field, see hash callback
1227 'formats' => { 'simple' => [ qw(custnum agent_custid paid payinfo invnum) ] },
1228 'format_types' => { 'simple' => '' }, #force infer from file extension
1229 'default_csv' => 1, #if it's not .xls, it'll read as csv, regardless of extension
1230 'format_hash_callbacks' => { 'simple' => $hashcb },
1231 'postinsert_callback' => sub {
1232 my $cust_pay = shift;
1233 my $cust_main = $cust_pay->cust_main ||
1234 return "can't find customer to which payments apply";
1235 my $error = $cust_main->apply_payments_and_credits;
1237 ? "can't apply payments to customer ".$cust_pay->custnum."$error"
1242 FS::Record::process_batch_import( $job, $opt, @_ );
1246 =item batch_import HASHREF
1248 Inserts new payments.
1255 my $fh = $param->{filehandle};
1256 my $format = $param->{'format'};
1258 my $agentnum = $param->{agentnum};
1259 my $_date = $param->{_date};
1260 $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1261 my $paybatch = $param->{'paybatch'};
1263 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1264 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1266 # here is the agent virtualization
1267 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1271 if ( $format eq 'simple' ) {
1272 @fields = qw( custnum agent_custid paid payinfo invnum );
1274 } elsif ( $format eq 'extended' ) {
1275 die "unimplemented\n";
1279 die "unknown format $format";
1282 eval "use Text::CSV_XS;";
1285 my $csv = new Text::CSV_XS;
1289 local $SIG{HUP} = 'IGNORE';
1290 local $SIG{INT} = 'IGNORE';
1291 local $SIG{QUIT} = 'IGNORE';
1292 local $SIG{TERM} = 'IGNORE';
1293 local $SIG{TSTP} = 'IGNORE';
1294 local $SIG{PIPE} = 'IGNORE';
1296 my $oldAutoCommit = $FS::UID::AutoCommit;
1297 local $FS::UID::AutoCommit = 0;
1301 while ( defined($line=<$fh>) ) {
1303 $csv->parse($line) or do {
1304 $dbh->rollback if $oldAutoCommit;
1305 return "can't parse: ". $csv->error_input();
1308 my @columns = $csv->fields();
1312 paybatch => $paybatch,
1314 $cust_pay{_date} = $_date if $_date;
1317 foreach my $field ( @fields ) {
1319 if ( $field eq 'agent_custid'
1321 && $columns[0] =~ /\S+/ )
1324 my $agent_custid = $columns[0];
1325 my %hash = ( 'agent_custid' => $agent_custid,
1326 'agentnum' => $agentnum,
1329 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1330 $dbh->rollback if $oldAutoCommit;
1331 return "can't specify custnum with agent_custid $agent_custid";
1334 $cust_main = qsearchs({
1335 'table' => 'cust_main',
1336 'hashref' => \%hash,
1337 'extra_sql' => $extra_sql,
1340 unless ( $cust_main ) {
1341 $dbh->rollback if $oldAutoCommit;
1342 return "can't find customer with agent_custid $agent_custid";
1346 $columns[0] = $cust_main->custnum;
1349 $cust_pay{$field} = shift @columns;
1352 if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1353 && length($1) == $custnum_length ) {
1354 $cust_pay{custnum} = $2;
1357 my $custnum = $cust_pay{custnum};
1359 my $cust_pay = new FS::cust_pay( \%cust_pay );
1360 my $error = $cust_pay->insert;
1362 if ( ! $error && $cust_pay->custnum != $custnum ) {
1363 #invnum was defined, and ->insert set custnum to the customer for that
1364 #invoice, but it wasn't the one the import specified.
1365 $dbh->rollback if $oldAutoCommit;
1366 $error = "specified invoice #". $cust_pay{invnum}.
1367 " is for custnum ". $cust_pay->custnum.
1368 ", not specified custnum $custnum";
1372 $dbh->rollback if $oldAutoCommit;
1373 return "can't insert payment for $line: $error";
1376 if ( $format eq 'simple' ) {
1377 # include agentnum for less surprise?
1378 $cust_main = qsearchs({
1379 'table' => 'cust_main',
1380 'hashref' => { 'custnum' => $cust_pay->custnum },
1381 'extra_sql' => $extra_sql,
1385 unless ( $cust_main ) {
1386 $dbh->rollback if $oldAutoCommit;
1387 return "can't find customer to which payments apply at line: $line";
1390 $error = $cust_main->apply_payments_and_credits;
1392 $dbh->rollback if $oldAutoCommit;
1393 return "can't apply payments to customer for $line: $error";
1401 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1403 return "Empty file!" unless $imported;
1413 Delete and replace methods.
1417 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1418 schema.html from the base documentation.