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 Flag to only allow manual application of payment, empty or 'Y'
125 The bank where the payment was deposited.
129 The name of the depositor.
133 The deposit account number.
141 The number of the batch this payment came from (see L<FS::pay_batch>),
142 or null if it was processed through a realtime gateway or entered manually.
146 The number of the realtime or batch gateway L<FS::payment_gateway>) this
147 payment was processed through. Null if it was entered manually or processed
148 by the "system default" gateway, which doesn't have a number.
152 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
153 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
154 redundant with C<gatewaynum>.
158 The authorization number returned by the credit card network.
162 The transaction ID returned by the gateway, if any. This is usually what
163 you would use to initiate a void or refund of the payment.
173 Creates a new payment. To add the payment to the databse, see L<"insert">.
177 sub table { 'cust_pay'; }
178 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum; }
179 sub cust_unlinked_msg {
181 "WARNING: can't find cust_main.custnum ". $self->custnum.
182 ' (cust_pay.paynum '. $self->paynum. ')';
185 =item insert [ OPTION => VALUE ... ]
187 Adds this payment to the database.
189 For backwards-compatibility and convenience, if the additional field invnum
190 is defined, an FS::cust_bill_pay record for the full amount of the payment
191 will be created. In this case, custnum is optional.
193 If the additional field discount_term is defined then a prepayment discount
194 is taken for that length of time. It is an error for the customer to owe
195 after this payment is made.
197 A hash of optional arguments may be passed. The following arguments are
204 If true, a payment receipt is sent instead of a statement when
205 'payment_receipt_email' configuration option is set.
207 About the "manual" flag: Normally, if the 'payment_receipt' config option
208 is set, and the customer has an invoice email address, inserting a payment
209 causes a I<statement> to be emailed to the customer. If the payment is
210 considered "manual" (or if the customer has no invoices), then it will
211 instead send a I<payment receipt>. "manual" should be true whenever a
212 payment is created directly from the web interface, from a user-initiated
213 realtime payment, or from a third-party payment via self-service. It should
214 be I<false> when creating a payment from a billing event or from a batch.
218 Don't send an email receipt. (Note: does not currently work when
219 payment_receipt-trigger is set to something other than default / cust_bill)
226 my($self, %options) = @_;
228 local $SIG{HUP} = 'IGNORE';
229 local $SIG{INT} = 'IGNORE';
230 local $SIG{QUIT} = 'IGNORE';
231 local $SIG{TERM} = 'IGNORE';
232 local $SIG{TSTP} = 'IGNORE';
233 local $SIG{PIPE} = 'IGNORE';
235 my $oldAutoCommit = $FS::UID::AutoCommit;
236 local $FS::UID::AutoCommit = 0;
240 if ( $self->invnum ) {
241 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
243 $dbh->rollback if $oldAutoCommit;
244 return "Unknown cust_bill.invnum: ". $self->invnum;
246 if ($self->custnum && ($cust_bill->custnum ne $self->custnum)) {
247 $dbh->rollback if $oldAutoCommit;
248 return "Invoice custnum ".$cust_bill->custnum
249 ." does not match specified custnum ".$self->custnum
250 ." for invoice ".$self->invnum;
252 $self->custnum($cust_bill->custnum );
255 my $error = $self->check;
256 return $error if $error;
258 my $cust_main = $self->cust_main;
259 my $old_balance = $cust_main->balance;
261 $error = $self->SUPER::insert;
263 $dbh->rollback if $oldAutoCommit;
264 return "error inserting cust_pay: $error";
267 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
268 if ( my $months = $self->discount_term ) {
269 # XXX this should be moved out somewhere, but discount_term_values
271 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
272 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
274 # %billing_pkgs contains this customer's active monthly packages.
275 # Recurring fees for those packages will be credited and then rebilled
276 # for the full discount term. Other packages on the last invoice
277 # (canceled, non-monthly recurring, or one-time charges) will be
279 my %billing_pkgs = map { $_->pkgnum => $_ }
280 grep { $_->part_pkg->freq eq '1' }
281 $cust_main->billing_pkgs;
282 my $credit = 0; # sum of recurring charges from that invoice
283 my $last_bill_date = 0; # the real bill date
284 foreach my $item ( $cust_bill->cust_bill_pkg ) {
285 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
286 $credit += $item->recur;
287 $last_bill_date = $item->cust_pkg->last_bill
288 if defined($item->cust_pkg)
289 and $item->cust_pkg->last_bill > $last_bill_date
292 my $cust_credit = new FS::cust_credit {
293 'custnum' => $self->custnum,
294 'amount' => sprintf('%.2f', $credit),
295 'reason' => 'customer chose to prepay for discount',
297 $error = $cust_credit->insert('reason_type' => $credit_type);
299 $dbh->rollback if $oldAutoCommit;
300 return "error inserting prepayment credit: $error";
304 # bill for the entire term
305 $_->bill($_->last_bill) foreach (values %billing_pkgs);
306 $error = $cust_main->bill(
307 # no recurring_only, we want unbilled packages with start dates to
309 'no_usage_reset' => 1,
310 'time' => $last_bill_date, # not $cust_bill->_date
311 'pkg_list' => [ values %billing_pkgs ],
312 'freq_override' => $months,
315 $dbh->rollback if $oldAutoCommit;
316 return "error inserting cust_pay: $error";
318 $error = $cust_main->apply_payments_and_credits;
320 $dbh->rollback if $oldAutoCommit;
321 return "error inserting cust_pay: $error";
323 my $new_balance = $cust_main->balance;
324 if ($new_balance > 0) {
325 $dbh->rollback if $oldAutoCommit;
326 return "balance after prepay discount attempt: $new_balance";
328 # user friendly: override the "apply only to this invoice" mode
335 if ( $self->invnum ) {
336 my $cust_bill_pay = new FS::cust_bill_pay {
337 'invnum' => $self->invnum,
338 'paynum' => $self->paynum,
339 'amount' => $self->paid,
340 '_date' => $self->_date,
342 $error = $cust_bill_pay->insert(%options);
344 if ( $ignore_noapply ) {
345 warn "warning: error inserting cust_bill_pay: $error ".
346 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
348 $dbh->rollback if $oldAutoCommit;
349 return "error inserting cust_bill_pay: $error";
354 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
356 #false laziness w/ cust_credit::insert
357 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
358 my @errors = $cust_main->unsuspend;
360 # side-fx with nested transactions? upstack rolls back?
361 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
367 #bill setup fees for voip_cdr bill_every_call packages
368 #some false laziness w/search in freeside-cdrd
370 'LEFT JOIN part_pkg USING ( pkgpart ) '.
371 "LEFT JOIN part_pkg_option
372 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
373 AND part_pkg_option.optionname = 'bill_every_call' )";
375 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
376 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
378 my @cust_pkg = qsearch({
379 'table' => 'cust_pkg',
380 'addl_from' => $addl_from,
381 'hashref' => { 'custnum' => $self->custnum,
385 'extra_sql' => $extra_sql,
389 warn "voip_cdr bill_every_call packages found; billing customer\n";
390 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
392 warn "WARNING: Error billing customer: $bill_error\n";
395 #end of billing setup fees for voip_cdr bill_every_call packages
397 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
400 my $trigger = $conf->config('payment_receipt-trigger',
401 $self->cust_main->agentnum) || 'cust_pay';
402 if ( $trigger eq 'cust_pay' ) {
403 my $error = $self->send_receipt(
404 'manual' => $options{'manual'},
405 'noemail' => $options{'noemail'},
406 'cust_bill' => $cust_bill,
407 'cust_main' => $cust_main,
409 warn "can't send payment receipt/statement: $error" if $error;
416 =item void [ REASON ]
418 Voids this payment: deletes the payment and all associated applications and
419 adds a record of the voided payment to the FS::cust_pay_void table.
426 local $SIG{HUP} = 'IGNORE';
427 local $SIG{INT} = 'IGNORE';
428 local $SIG{QUIT} = 'IGNORE';
429 local $SIG{TERM} = 'IGNORE';
430 local $SIG{TSTP} = 'IGNORE';
431 local $SIG{PIPE} = 'IGNORE';
433 my $oldAutoCommit = $FS::UID::AutoCommit;
434 local $FS::UID::AutoCommit = 0;
437 my $cust_pay_void = new FS::cust_pay_void ( {
438 map { $_ => $self->get($_) } $self->fields
440 $cust_pay_void->reason(shift) if scalar(@_);
441 my $error = $cust_pay_void->insert;
443 my $cust_pay_pending =
444 qsearchs('cust_pay_pending', { paynum => $self->paynum });
445 if ( $cust_pay_pending ) {
446 $cust_pay_pending->set('void_paynum', $self->paynum);
447 $cust_pay_pending->set('paynum', '');
448 $error ||= $cust_pay_pending->replace;
451 $error ||= $self->delete;
454 $dbh->rollback if $oldAutoCommit;
458 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
466 Unless the closed flag is set, deletes this payment and all associated
467 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
468 cases, you want to use the void method instead to leave a record of the
473 # very similar to FS::cust_credit::delete
476 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
478 local $SIG{HUP} = 'IGNORE';
479 local $SIG{INT} = 'IGNORE';
480 local $SIG{QUIT} = 'IGNORE';
481 local $SIG{TERM} = 'IGNORE';
482 local $SIG{TSTP} = 'IGNORE';
483 local $SIG{PIPE} = 'IGNORE';
485 my $oldAutoCommit = $FS::UID::AutoCommit;
486 local $FS::UID::AutoCommit = 0;
489 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
490 my $error = $app->delete;
492 $dbh->rollback if $oldAutoCommit;
497 my $error = $self->SUPER::delete(@_);
499 $dbh->rollback if $oldAutoCommit;
503 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
509 =item replace [ OLD_RECORD ]
511 You can, but probably shouldn't modify payments...
513 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
514 supplied, replaces this record. If there is an error, returns the error,
515 otherwise returns false.
521 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
522 $self->SUPER::replace(@_);
527 Checks all fields to make sure this is a valid payment. If there is an error,
528 returns the error, otherwise returns false. Called by the insert method.
535 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
538 $self->ut_numbern('paynum')
539 || $self->ut_numbern('custnum')
540 || $self->ut_numbern('_date')
541 || $self->ut_money('paid')
542 || $self->ut_alphan('otaker')
543 || $self->ut_textn('paybatch')
544 || $self->ut_textn('payunique')
545 || $self->ut_enum('closed', [ '', 'Y' ])
546 || $self->ut_flag('no_auto_apply')
547 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
548 || $self->ut_textn('bank')
549 || $self->ut_alphan('depositor')
550 || $self->ut_numbern('account')
551 || $self->ut_numbern('teller')
552 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
553 || $self->payinfo_check()
555 return $error if $error;
557 return "paid must be > 0 " if $self->paid <= 0;
559 return "unknown cust_main.custnum: ". $self->custnum
561 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
563 $self->_date(time) unless $self->_date;
565 return "invalid discount_term"
566 if ($self->discount_term && $self->discount_term < 2);
568 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
569 foreach (qw(bank depositor account teller)) {
570 return "$_ required" if $self->get($_) eq '';
574 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
575 # # UNIQUE index should catch this too, without race conditions, but this
576 # # should give a better error message the other 99.9% of the time...
577 # if ( length($self->payunique)
578 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
579 # #well, it *could* be a better error message
580 # return "duplicate transaction".
581 # " - a payment with unique identifer ". $self->payunique.
588 =item send_receipt HASHREF | OPTION => VALUE ...
590 Sends a payment receipt for this payment..
598 Flag indicating the payment is being made manually.
602 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
607 Customer (FS::cust_main) object (for efficiency).
611 Don't send an email receipt.
621 my $opt = ref($_[0]) ? shift : { @_ };
623 my $cust_bill = $opt->{'cust_bill'};
624 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
626 my $conf = new FS::Conf;
628 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
630 my @invoicing_list = $cust_main->invoicing_list_emailonly;
631 return '' unless @invoicing_list;
633 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
637 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
638 #|| ! $conf->exists('invoice_html_statement')
642 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
645 my %substitutions = ();
646 $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
648 my $queue = new FS::queue {
649 'job' => 'FS::Misc::process_send_email',
650 'paynum' => $self->paynum,
651 'custnum' => $cust_main->custnum,
653 $error = $queue->insert(
654 FS::msg_template->by_key($msgnum)->prepare(
655 'cust_main' => $cust_main,
657 'from_config' => 'payment_receipt_from',
658 'substitutions' => \%substitutions,
660 'msgtype' => 'receipt', # override msg_template's default
663 } elsif ( $conf->exists('payment_receipt_email') ) {
665 my $receipt_template = new Text::Template (
667 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
669 warn "can't create payment receipt template: $Text::Template::ERROR";
673 my $payby = $self->payby;
674 my $payinfo = $self->payinfo;
675 $payby =~ s/^BILL$/Check/ if $payinfo;
676 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
677 $payinfo = $self->paymask
679 $payinfo = $self->decrypt($payinfo);
681 $payby =~ s/^CHEK$/Electronic check/;
684 'date' => time2str("%a %B %o, %Y", $self->_date),
685 'name' => $cust_main->name,
686 'paynum' => $self->paynum,
687 'paid' => sprintf("%.2f", $self->paid),
688 'payby' => ucfirst(lc($payby)),
689 'payinfo' => $payinfo,
690 'balance' => $cust_main->balance,
691 'company_name' => $conf->config('company_name', $cust_main->agentnum),
694 $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
696 if ( $opt->{'cust_pkg'} ) {
697 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
698 #setup date, other things?
701 my $queue = new FS::queue {
702 'job' => 'FS::Misc::process_send_generated_email',
703 'paynum' => $self->paynum,
704 'custnum' => $cust_main->custnum,
705 'msgtype' => 'receipt',
707 $error = $queue->insert(
708 'from' => $conf->invoice_from_full( $cust_main->agentnum ),
709 #invoice_from??? well as good as any
710 'to' => \@invoicing_list,
711 'subject' => 'Payment receipt',
712 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
717 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
721 #not manual and no noemail flag (here or on the customer)
722 } elsif ( ! $opt->{'noemail'} && ! $cust_main->invoice_noemail ) {
724 my $queue = new FS::queue {
725 'job' => 'FS::cust_bill::queueable_email',
726 'paynum' => $self->paynum,
727 'custnum' => $cust_main->custnum,
731 'invnum' => $cust_bill->invnum,
735 if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
736 $opt{'mode'} = $mode;
738 # backward compatibility, no good fix for this yet as some people may
739 # still have "invoice_latex_statement" and such options
740 $opt{'template'} = 'statement';
741 $opt{'notice_name'} = 'Statement';
744 $error = $queue->insert(%opt);
748 warn "send_receipt: $error\n" if $error;
753 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
760 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
761 sort { $a->_date <=> $b->_date
762 || $a->invnum <=> $b->invnum }
763 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
767 =item cust_pay_refund
769 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
774 sub cust_pay_refund {
776 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
777 sort { $a->_date <=> $b->_date }
778 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
785 Returns the amount of this payment that is still unapplied; which is
786 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
787 applications (see L<FS::cust_pay_refund>).
793 my $amount = $self->paid;
794 $amount -= $_->amount foreach ( $self->cust_bill_pay );
795 $amount -= $_->amount foreach ( $self->cust_pay_refund );
796 sprintf("%.2f", $amount );
801 Returns the amount of this payment that has not been refuned; which is
802 paid minus all refund applications (see L<FS::cust_pay_refund>).
808 my $amount = $self->paid;
809 $amount -= $_->amount foreach ( $self->cust_pay_refund );
810 sprintf("%.2f", $amount );
815 Returns the "paid" field.
830 =item batch_insert CUST_PAY_OBJECT, ...
832 Class method which inserts multiple payments. Takes a list of FS::cust_pay
833 objects. Returns a list, each element representing the status of inserting the
834 corresponding payment - empty. If there is an error inserting any payment, the
835 entire transaction is rolled back, i.e. all payments are inserted or none are.
837 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
838 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
839 those objects will be inserted with the paynum of the payment, and for
840 each one, an error message or an empty string will be inserted into the
845 my @errors = FS::cust_pay->batch_insert(@cust_pay);
846 my $num_errors = scalar(grep $_, @errors);
847 if ( $num_errors == 0 ) {
848 #success; all payments were inserted
850 #failure; no payments were inserted.
856 my $self = shift; #class method
858 local $SIG{HUP} = 'IGNORE';
859 local $SIG{INT} = 'IGNORE';
860 local $SIG{QUIT} = 'IGNORE';
861 local $SIG{TERM} = 'IGNORE';
862 local $SIG{TSTP} = 'IGNORE';
863 local $SIG{PIPE} = 'IGNORE';
865 my $oldAutoCommit = $FS::UID::AutoCommit;
866 local $FS::UID::AutoCommit = 0;
872 foreach my $cust_pay (@_) {
873 my $error = $cust_pay->insert( 'manual' => 1 );
874 push @errors, $error;
875 $num_errors++ if $error;
877 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
879 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
880 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
884 $cust_bill_pay->set('paynum', $cust_pay->paynum);
885 my $apply_error = $cust_bill_pay->insert;
886 push @errors, $apply_error || '';
887 $num_errors++ if $apply_error;
891 } elsif ( !$error ) { #normal case: apply payments as usual
892 $cust_pay->cust_main->apply_payments;
898 $dbh->rollback if $oldAutoCommit;
900 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
909 Returns an SQL fragment to retreive the unapplied amount.
914 my ($class, $start, $end) = @_;
915 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
916 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
917 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
918 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
922 ( SELECT SUM(amount) FROM cust_bill_pay
923 WHERE cust_pay.paynum = cust_bill_pay.paynum
924 $bill_start $bill_end )
928 ( SELECT SUM(amount) FROM cust_pay_refund
929 WHERE cust_pay.paynum = cust_pay_refund.paynum
930 $refund_start $refund_end )
939 my @fields = grep { $_ ne 'payinfo' } $self->fields;
940 +{ ( map { $_=>$self->$_ } @fields ),
946 # Used by FS::Upgrade to migrate to a new database.
950 sub _upgrade_data { #class method
951 my ($class, %opt) = @_;
953 warn "$me upgrading $class\n" if $DEBUG;
955 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
958 # otaker/ivan upgrade
961 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
963 #not the most efficient, but hey, it only has to run once
965 my $where = " WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' )
967 AND EXISTS ( SELECT 1 FROM cust_main
968 WHERE cust_main.custnum = cust_pay.custnum )
971 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
973 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
974 $sth->execute or die $sth->errstr;
975 my $total = $sth->fetchrow_arrayref->[0];
976 #warn "$total cust_pay records to update\n"
978 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
983 my @cust_pay = qsearch( {
984 'table' => 'cust_pay',
986 'extra_sql' => $where,
987 'order_by' => 'ORDER BY paynum',
990 foreach my $cust_pay (@cust_pay) {
992 my $h_cust_pay = $cust_pay->h_search('insert');
994 next if $cust_pay->otaker eq $h_cust_pay->history_user;
995 #$cust_pay->otaker($h_cust_pay->history_user);
996 $cust_pay->set('otaker', $h_cust_pay->history_user);
998 $cust_pay->set('otaker', 'legacy');
1001 my $error = $cust_pay->replace;
1004 warn " *** WARNING: Error updating order taker for payment paynum ".
1005 $cust_pay->paynun. ": $error\n";
1010 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1011 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1017 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1021 # payinfo N/A upgrade
1024 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1026 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1028 my @na_cust_pay = qsearch( {
1029 'table' => 'cust_pay',
1030 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1031 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1034 foreach my $na ( @na_cust_pay ) {
1036 next unless $na->payinfo eq 'N/A';
1038 my $cust_pay_pending =
1039 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1040 unless ( $cust_pay_pending ) {
1041 warn " *** WARNING: not-yet recoverable N/A card for payment ".
1042 $na->paynum. " (no cust_pay_pending)\n";
1045 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1046 my $error = $na->replace;
1048 warn " *** WARNING: Error updating payinfo for payment paynum ".
1049 $na->paynun. ": $error\n";
1055 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1059 # otaker->usernum upgrade
1062 $class->_upgrade_otaker(%opt);
1064 # if we do this anywhere else, it should become an FS::Upgrade method
1065 my $num_to_upgrade = $class->count('paybatch is not null');
1066 my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1067 if ( $num_to_upgrade > 0 ) {
1068 warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1069 if ( $opt{queue} ) {
1070 if ( $num_jobs > 0 ) {
1071 warn "Upgrade already queued.\n";
1073 warn "Scheduling upgrade.\n";
1074 my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1078 process_upgrade_paybatch();
1083 sub process_upgrade_paybatch {
1085 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1086 local $FS::UID::AutoCommit = 1;
1089 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1091 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1092 my $search = FS::Cursor->new( {
1093 'table' => 'cust_pay',
1094 'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1096 while (my $cust_pay = $search->fetch) {
1097 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1098 $cust_pay->set('paybatch' => '');
1099 my $error = $cust_pay->replace;
1100 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1105 # migrate gateway info from the misused 'paybatch' field
1108 # not only cust_pay, but also voided and refunded payments
1109 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1110 local $FS::Record::nowarn_classload=1;
1111 # really inefficient, but again, only has to run once
1112 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1113 my $and_batchnum_is_null =
1114 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1115 my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1116 my $search = FS::Cursor->new({
1118 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1119 "AND (paybatch IS NOT NULL ".
1120 "OR (paybatch IS NULL AND auth IS NULL
1121 $and_batchnum_is_null ) )
1122 ORDER BY $pkey DESC"
1124 while ( my $object = $search->fetch ) {
1125 if ( $object->paybatch eq '' ) {
1126 # repair for a previous upgrade that didn't save 'auth'
1127 my $pkey = $object->primary_key;
1128 # find the last history record that had a paybatch value
1130 table => "h_$table",
1132 $pkey => $object->$pkey,
1133 paybatch => { op=>'!=', value=>''},
1134 history_action => 'replace_old',
1136 order_by => 'ORDER BY history_date DESC LIMIT 1',
1139 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1142 # if the paybatch didn't have an auth string, then it's fine
1143 $h->paybatch =~ /:(\w+):/ or next;
1144 # set paybatch to what it was in that record
1145 $object->set('paybatch', $h->paybatch)
1146 # and then upgrade it like the old records
1149 my $parsed = $object->_parse_paybatch;
1150 if (keys %$parsed) {
1151 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1152 $object->set('auth' => $parsed->{authorization});
1153 $object->set('paybatch', '');
1154 my $error = $object->replace;
1155 warn "error parsing CARD/CHEK paybatch fields on $object #".
1156 $object->get($object->primary_key).":\n $error\n"
1161 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1171 =item process_batch_import
1175 sub process_batch_import {
1180 my $custnum = $hash{'custnum'};
1181 my $agentnum = $hash{'agentnum'};
1182 my $agent_custid = $hash{'agent_custid'};
1184 $hash{'_date'} = parse_datetime($hash{'_date'})
1185 if $hash{'_date'} && $hash{'_date'} =~ /\D/;
1186 #remove custnum_prefix
1187 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1188 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1191 && $custnum =~ /^$custnum_prefix(0*([1-9]\d*))$/
1192 && length($1) == $custnum_length
1196 # check agentnum against custnum and
1197 # translate agent_custid into regular custnum
1198 if ($custnum && $agent_custid) {
1199 die "can't specify both custnum and agent_custid\n";
1200 } elsif ($agentnum || $agent_custid) {
1201 # here is the agent virtualization
1202 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1204 $search{'agentnum'} = $agentnum
1206 $search{'agent_custid'} = $agent_custid
1208 $search{'custnum'} = $custnum
1210 my $cust_main = qsearchs({
1211 'table' => 'cust_main',
1212 'hashref' => \%search,
1213 'extra_sql' => $extra_sql,
1215 die "can't find customer with" .
1216 ($agentnum ? " agentnum $agentnum" : '') .
1217 ($custnum ? " custnum $custnum" : '') .
1218 ($agent_custid ? " agent_custid $agent_custid" : '') . "\n"
1220 die "mismatched customer number\n"
1221 if $custnum && ($custnum ne $cust_main->custnum);
1222 $custnum = $cust_main->custnum;
1224 $hash{'custnum'} = $custnum;
1225 delete($hash{'agent_custid'});
1230 'table' => 'cust_pay',
1231 'params' => [ '_date', 'agentnum', 'payby', 'paybatch' ],
1232 #agent_custid isn't a cust_pay field, see hash callback
1233 'formats' => { 'simple' =>
1234 [ qw(custnum agent_custid paid payinfo invnum) ] },
1235 'format_types' => { 'simple' => '' }, #force infer from file extension
1236 'default_csv' => 1, #if not .xls, will read as csv, regardless of extension
1237 'format_hash_callbacks' => { 'simple' => $hashcb },
1238 'insert_args_callback' => sub { ( 'manual'=>1 ); },
1239 'postinsert_callback' => sub {
1240 my $cust_pay = shift;
1241 my $cust_main = $cust_pay->cust_main
1242 or return "can't find customer to which payments apply";
1243 my $error = $cust_main->apply_payments_and_credits;
1245 ? "can't apply payments to customer ".$cust_pay->custnum."$error"
1250 FS::Record::process_batch_import( $job, $opt, @_ );
1254 =item batch_import HASHREF
1256 Inserts new payments.
1263 my $fh = $param->{filehandle};
1264 my $format = $param->{'format'};
1266 my $agentnum = $param->{agentnum};
1267 my $_date = $param->{_date};
1268 $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1269 my $paybatch = $param->{'paybatch'};
1271 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1272 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1274 # here is the agent virtualization
1275 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1279 if ( $format eq 'simple' ) {
1280 @fields = qw( custnum agent_custid paid payinfo invnum );
1282 } elsif ( $format eq 'extended' ) {
1283 die "unimplemented\n";
1287 die "unknown format $format";
1290 eval "use Text::CSV_XS;";
1293 my $csv = new Text::CSV_XS;
1297 local $SIG{HUP} = 'IGNORE';
1298 local $SIG{INT} = 'IGNORE';
1299 local $SIG{QUIT} = 'IGNORE';
1300 local $SIG{TERM} = 'IGNORE';
1301 local $SIG{TSTP} = 'IGNORE';
1302 local $SIG{PIPE} = 'IGNORE';
1304 my $oldAutoCommit = $FS::UID::AutoCommit;
1305 local $FS::UID::AutoCommit = 0;
1309 while ( defined($line=<$fh>) ) {
1311 $csv->parse($line) or do {
1312 $dbh->rollback if $oldAutoCommit;
1313 return "can't parse: ". $csv->error_input();
1316 my @columns = $csv->fields();
1320 paybatch => $paybatch,
1322 $cust_pay{_date} = $_date if $_date;
1325 foreach my $field ( @fields ) {
1327 if ( $field eq 'agent_custid'
1329 && $columns[0] =~ /\S+/ )
1332 my $agent_custid = $columns[0];
1333 my %hash = ( 'agent_custid' => $agent_custid,
1334 'agentnum' => $agentnum,
1337 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1338 $dbh->rollback if $oldAutoCommit;
1339 return "can't specify custnum with agent_custid $agent_custid";
1342 $cust_main = qsearchs({
1343 'table' => 'cust_main',
1344 'hashref' => \%hash,
1345 'extra_sql' => $extra_sql,
1348 unless ( $cust_main ) {
1349 $dbh->rollback if $oldAutoCommit;
1350 return "can't find customer with agent_custid $agent_custid";
1354 $columns[0] = $cust_main->custnum;
1357 $cust_pay{$field} = shift @columns;
1360 if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1361 && length($1) == $custnum_length ) {
1362 $cust_pay{custnum} = $2;
1365 my $custnum = $cust_pay{custnum};
1367 my $cust_pay = new FS::cust_pay( \%cust_pay );
1368 my $error = $cust_pay->insert;
1370 if ( ! $error && $cust_pay->custnum != $custnum ) {
1371 #invnum was defined, and ->insert set custnum to the customer for that
1372 #invoice, but it wasn't the one the import specified.
1373 $dbh->rollback if $oldAutoCommit;
1374 $error = "specified invoice #". $cust_pay{invnum}.
1375 " is for custnum ". $cust_pay->custnum.
1376 ", not specified custnum $custnum";
1380 $dbh->rollback if $oldAutoCommit;
1381 return "can't insert payment for $line: $error";
1384 if ( $format eq 'simple' ) {
1385 # include agentnum for less surprise?
1386 $cust_main = qsearchs({
1387 'table' => 'cust_main',
1388 'hashref' => { 'custnum' => $cust_pay->custnum },
1389 'extra_sql' => $extra_sql,
1393 unless ( $cust_main ) {
1394 $dbh->rollback if $oldAutoCommit;
1395 return "can't find customer to which payments apply at line: $line";
1398 $error = $cust_main->apply_payments_and_credits;
1400 $dbh->rollback if $oldAutoCommit;
1401 return "can't apply payments to customer for $line: $error";
1409 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1411 return "Empty file!" unless $imported;
1421 Delete and replace methods.
1425 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1426 schema.html from the base documentation.