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::UID qw( getotaker driver_name );
13 use FS::Misc qw( send_email );
14 use FS::Misc::DateTime qw( parse_datetime ); #for batch_import
15 use FS::Record qw( dbh qsearch qsearchs );
18 use FS::cust_main_Mixin;
19 use FS::payinfo_transaction_Mixin;
21 use FS::cust_bill_pay;
22 use FS::cust_pay_refund;
25 use FS::cust_pay_void;
26 use FS::upgrade_journal;
31 $me = '[FS::cust_pay]';
35 #ask FS::UID to run this stuff for us later
36 FS::UID->install_callback( sub {
38 $unsuspendauto = $conf->exists('unsuspendauto');
41 @encrypted_fields = ('payinfo');
42 sub nohistory_fields { ('payinfo'); }
46 FS::cust_pay - Object methods for cust_pay objects
52 $record = new FS::cust_pay \%hash;
53 $record = new FS::cust_pay { 'column' => 'value' };
55 $error = $record->insert;
57 $error = $new_record->replace($old_record);
59 $error = $record->delete;
61 $error = $record->check;
65 An FS::cust_pay object represents a payment; the transfer of money from a
66 customer. FS::cust_pay inherits from FS::Record. The following fields are
73 primary key (assigned automatically for new payments)
77 customer (see L<FS::cust_main>)
81 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
82 L<Time::Local> and L<Date::Parse> for conversion functions.
86 Amount of this payment
90 order taker (see L<FS::access_user>)
94 Payment Type (See L<FS::payinfo_Mixin> for valid values)
98 Payment Information (See L<FS::payinfo_Mixin> for data format)
102 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
106 obsolete text field for tracking card processing or other batch grouping
110 Optional unique identifer to prevent duplicate transactions.
114 books closed flag, empty or `Y'
118 Desired pkgnum when using experimental package balances.
122 The bank where the payment was deposited.
126 The name of the depositor.
130 The deposit account number.
138 The number of the batch this payment came from (see L<FS::pay_batch>),
139 or null if it was processed through a realtime gateway or entered manually.
143 The number of the realtime or batch gateway L<FS::payment_gateway>) this
144 payment was processed through. Null if it was entered manually or processed
145 by the "system default" gateway, which doesn't have a number.
149 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
150 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
151 redundant with C<gatewaynum>.
155 The authorization number returned by the credit card network.
159 The transaction ID returned by the gateway, if any. This is usually what
160 you would use to initiate a void or refund of the payment.
170 Creates a new payment. To add the payment to the databse, see L<"insert">.
174 sub table { 'cust_pay'; }
175 sub cust_linked { $_[0]->cust_main_custnum; }
176 sub cust_unlinked_msg {
178 "WARNING: can't find cust_main.custnum ". $self->custnum.
179 ' (cust_pay.paynum '. $self->paynum. ')';
182 =item insert [ OPTION => VALUE ... ]
184 Adds this payment to the database.
186 For backwards-compatibility and convenience, if the additional field invnum
187 is defined, an FS::cust_bill_pay record for the full amount of the payment
188 will be created. In this case, custnum is optional.
190 If the additional field discount_term is defined then a prepayment discount
191 is taken for that length of time. It is an error for the customer to owe
192 after this payment is made.
194 A hash of optional arguments may be passed. Currently "manual" is supported.
195 If true, a payment receipt is sent instead of a statement when
196 'payment_receipt_email' configuration option is set.
198 About the "manual" flag: Normally, if the 'payment_receipt' config option
199 is set, and the customer has an invoice email address, inserting a payment
200 causes a I<statement> to be emailed to the customer. If the payment is
201 considered "manual" (or if the customer has no invoices), then it will
202 instead send a I<payment receipt>. "manual" should be true whenever a
203 payment is created directly from the web interface, from a user-initiated
204 realtime payment, or from a third-party payment via self-service. It should
205 be I<false> when creating a payment from a billing event or from a batch.
210 my($self, %options) = @_;
212 local $SIG{HUP} = 'IGNORE';
213 local $SIG{INT} = 'IGNORE';
214 local $SIG{QUIT} = 'IGNORE';
215 local $SIG{TERM} = 'IGNORE';
216 local $SIG{TSTP} = 'IGNORE';
217 local $SIG{PIPE} = 'IGNORE';
219 my $oldAutoCommit = $FS::UID::AutoCommit;
220 local $FS::UID::AutoCommit = 0;
224 if ( $self->invnum ) {
225 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
227 $dbh->rollback if $oldAutoCommit;
228 return "Unknown cust_bill.invnum: ". $self->invnum;
230 $self->custnum($cust_bill->custnum );
233 my $error = $self->check;
234 return $error if $error;
236 my $cust_main = $self->cust_main;
237 my $old_balance = $cust_main->balance;
239 $error = $self->SUPER::insert;
241 $dbh->rollback if $oldAutoCommit;
242 return "error inserting cust_pay: $error";
245 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
246 if ( my $months = $self->discount_term ) {
247 # XXX this should be moved out somewhere, but discount_term_values
249 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
250 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
252 # %billing_pkgs contains this customer's active monthly packages.
253 # Recurring fees for those packages will be credited and then rebilled
254 # for the full discount term. Other packages on the last invoice
255 # (canceled, non-monthly recurring, or one-time charges) will be
257 my %billing_pkgs = map { $_->pkgnum => $_ }
258 grep { $_->part_pkg->freq eq '1' }
259 $cust_main->billing_pkgs;
260 my $credit = 0; # sum of recurring charges from that invoice
261 my $last_bill_date = 0; # the real bill date
262 foreach my $item ( $cust_bill->cust_bill_pkg ) {
263 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
264 $credit += $item->recur;
265 $last_bill_date = $item->cust_pkg->last_bill
266 if defined($item->cust_pkg)
267 and $item->cust_pkg->last_bill > $last_bill_date
270 my $cust_credit = new FS::cust_credit {
271 'custnum' => $self->custnum,
272 'amount' => sprintf('%.2f', $credit),
273 'reason' => 'customer chose to prepay for discount',
275 $error = $cust_credit->insert('reason_type' => $credit_type);
277 $dbh->rollback if $oldAutoCommit;
278 return "error inserting prepayment credit: $error";
282 # bill for the entire term
283 $_->bill($_->last_bill) foreach (values %billing_pkgs);
284 $error = $cust_main->bill(
285 # no recurring_only, we want unbilled packages with start dates to
287 'no_usage_reset' => 1,
288 'time' => $last_bill_date, # not $cust_bill->_date
289 'pkg_list' => [ values %billing_pkgs ],
290 'freq_override' => $months,
293 $dbh->rollback if $oldAutoCommit;
294 return "error inserting cust_pay: $error";
296 $error = $cust_main->apply_payments_and_credits;
298 $dbh->rollback if $oldAutoCommit;
299 return "error inserting cust_pay: $error";
301 my $new_balance = $cust_main->balance;
302 if ($new_balance > 0) {
303 $dbh->rollback if $oldAutoCommit;
304 return "balance after prepay discount attempt: $new_balance";
306 # user friendly: override the "apply only to this invoice" mode
313 if ( $self->invnum ) {
314 my $cust_bill_pay = new FS::cust_bill_pay {
315 'invnum' => $self->invnum,
316 'paynum' => $self->paynum,
317 'amount' => $self->paid,
318 '_date' => $self->_date,
320 $error = $cust_bill_pay->insert(%options);
322 if ( $ignore_noapply ) {
323 warn "warning: error inserting cust_bill_pay: $error ".
324 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
326 $dbh->rollback if $oldAutoCommit;
327 return "error inserting cust_bill_pay: $error";
332 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
334 #false laziness w/ cust_credit::insert
335 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
336 my @errors = $cust_main->unsuspend;
338 # side-fx with nested transactions? upstack rolls back?
339 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
345 #bill setup fees for voip_cdr bill_every_call packages
346 #some false laziness w/search in freeside-cdrd
348 'LEFT JOIN part_pkg USING ( pkgpart ) '.
349 "LEFT JOIN part_pkg_option
350 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
351 AND part_pkg_option.optionname = 'bill_every_call' )";
353 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
354 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
356 my @cust_pkg = qsearch({
357 'table' => 'cust_pkg',
358 'addl_from' => $addl_from,
359 'hashref' => { 'custnum' => $self->custnum,
363 'extra_sql' => $extra_sql,
367 warn "voip_cdr bill_every_call packages found; billing customer\n";
368 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
370 warn "WARNING: Error billing customer: $bill_error\n";
373 #end of billing setup fees for voip_cdr bill_every_call packages
375 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
378 my $trigger = $conf->config('payment_receipt-trigger',
379 $self->cust_main->agentnum) || 'cust_pay';
380 if ( $trigger eq 'cust_pay' ) {
381 my $error = $self->send_receipt(
382 'manual' => $options{'manual'},
383 'cust_bill' => $cust_bill,
384 'cust_main' => $cust_main,
386 warn "can't send payment receipt/statement: $error" if $error;
393 =item void [ REASON ]
395 Voids this payment: deletes the payment and all associated applications and
396 adds a record of the voided payment to the FS::cust_pay_void table.
403 local $SIG{HUP} = 'IGNORE';
404 local $SIG{INT} = 'IGNORE';
405 local $SIG{QUIT} = 'IGNORE';
406 local $SIG{TERM} = 'IGNORE';
407 local $SIG{TSTP} = 'IGNORE';
408 local $SIG{PIPE} = 'IGNORE';
410 my $oldAutoCommit = $FS::UID::AutoCommit;
411 local $FS::UID::AutoCommit = 0;
414 my $cust_pay_void = new FS::cust_pay_void ( {
415 map { $_ => $self->get($_) } $self->fields
417 $cust_pay_void->reason(shift) if scalar(@_);
418 my $error = $cust_pay_void->insert;
420 my $cust_pay_pending =
421 qsearchs('cust_pay_pending', { paynum => $self->paynum });
422 if ( $cust_pay_pending ) {
423 $cust_pay_pending->set('void_paynum', $self->paynum);
424 $cust_pay_pending->set('paynum', '');
425 $error ||= $cust_pay_pending->replace;
428 $error ||= $self->delete;
431 $dbh->rollback if $oldAutoCommit;
435 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
443 Unless the closed flag is set, deletes this payment and all associated
444 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
445 cases, you want to use the void method instead to leave a record of the
450 # very similar to FS::cust_credit::delete
453 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
455 local $SIG{HUP} = 'IGNORE';
456 local $SIG{INT} = 'IGNORE';
457 local $SIG{QUIT} = 'IGNORE';
458 local $SIG{TERM} = 'IGNORE';
459 local $SIG{TSTP} = 'IGNORE';
460 local $SIG{PIPE} = 'IGNORE';
462 my $oldAutoCommit = $FS::UID::AutoCommit;
463 local $FS::UID::AutoCommit = 0;
466 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
467 my $error = $app->delete;
469 $dbh->rollback if $oldAutoCommit;
474 my $error = $self->SUPER::delete(@_);
476 $dbh->rollback if $oldAutoCommit;
480 if ( $conf->exists('deletepayments')
481 && $conf->config('deletepayments') ne '' ) {
483 my $cust_main = $self->cust_main;
485 my $error = send_email(
486 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
487 #invoice_from??? well as good as any
488 'to' => $conf->config('deletepayments'),
489 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
491 "This is an automatic message from your Freeside installation\n",
492 "informing you that the following payment has been deleted:\n",
494 'paynum: '. $self->paynum. "\n",
495 'custnum: '. $self->custnum.
496 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
497 'paid: $'. sprintf("%.2f", $self->paid). "\n",
498 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
499 'payby: '. $self->payby. "\n",
500 'payinfo: '. $self->paymask. "\n",
501 'paybatch: '. $self->paybatch. "\n",
506 $dbh->rollback if $oldAutoCommit;
507 return "can't send payment deletion notification: $error";
512 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
518 =item replace [ OLD_RECORD ]
520 You can, but probably shouldn't modify payments...
522 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
523 supplied, replaces this record. If there is an error, returns the error,
524 otherwise returns false.
530 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
531 $self->SUPER::replace(@_);
536 Checks all fields to make sure this is a valid payment. If there is an error,
537 returns the error, otherwise returns false. Called by the insert method.
544 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
547 $self->ut_numbern('paynum')
548 || $self->ut_numbern('custnum')
549 || $self->ut_numbern('_date')
550 || $self->ut_money('paid')
551 || $self->ut_alphan('otaker')
552 || $self->ut_textn('paybatch')
553 || $self->ut_textn('payunique')
554 || $self->ut_enum('closed', [ '', 'Y' ])
555 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
556 || $self->ut_textn('bank')
557 || $self->ut_alphan('depositor')
558 || $self->ut_numbern('account')
559 || $self->ut_numbern('teller')
560 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
561 || $self->payinfo_check()
563 return $error if $error;
565 return "paid must be > 0 " if $self->paid <= 0;
567 return "unknown cust_main.custnum: ". $self->custnum
569 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
571 $self->_date(time) unless $self->_date;
573 return "invalid discount_term"
574 if ($self->discount_term && $self->discount_term < 2);
576 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
577 foreach (qw(bank depositor account teller)) {
578 return "$_ required" if $self->get($_) eq '';
582 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
583 # # UNIQUE index should catch this too, without race conditions, but this
584 # # should give a better error message the other 99.9% of the time...
585 # if ( length($self->payunique)
586 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
587 # #well, it *could* be a better error message
588 # return "duplicate transaction".
589 # " - a payment with unique identifer ". $self->payunique.
596 =item send_receipt HASHREF | OPTION => VALUE ...
598 Sends a payment receipt for this payment..
606 Flag indicating the payment is being made manually.
610 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
615 Customer (FS::cust_main) object (for efficiency).
623 my $opt = ref($_[0]) ? shift : { @_ };
625 my $cust_bill = $opt->{'cust_bill'};
626 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
628 my $conf = new FS::Conf;
630 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
632 my @invoicing_list = $cust_main->invoicing_list_emailonly;
633 return '' unless @invoicing_list;
635 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
639 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
640 #|| ! $conf->exists('invoice_html_statement')
644 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
647 my %substitutions = ();
648 $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
650 my $queue = new FS::queue {
651 'job' => 'FS::Misc::process_send_email',
652 'paynum' => $self->paynum,
653 'custnum' => $cust_main->custnum,
655 $error = $queue->insert(
656 FS::msg_template->by_key($msgnum)->prepare(
657 'cust_main' => $cust_main,
659 'from_config' => 'payment_receipt_from',
660 'substitutions' => \%substitutions,
662 'msgtype' => 'receipt', # override msg_template's default
665 } elsif ( $conf->exists('payment_receipt_email') ) {
667 my $receipt_template = new Text::Template (
669 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
671 warn "can't create payment receipt template: $Text::Template::ERROR";
675 my $payby = $self->payby;
676 my $payinfo = $self->payinfo;
677 $payby =~ s/^BILL$/Check/ if $payinfo;
678 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
679 $payinfo = $self->paymask
681 $payinfo = $self->decrypt($payinfo);
683 $payby =~ s/^CHEK$/Electronic check/;
686 'date' => time2str("%a %B %o, %Y", $self->_date),
687 'name' => $cust_main->name,
688 'paynum' => $self->paynum,
689 'paid' => sprintf("%.2f", $self->paid),
690 'payby' => ucfirst(lc($payby)),
691 'payinfo' => $payinfo,
692 'balance' => $cust_main->balance,
693 'company_name' => $conf->config('company_name', $cust_main->agentnum),
696 $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
698 if ( $opt->{'cust_pkg'} ) {
699 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
700 #setup date, other things?
703 my $queue = new FS::queue {
704 'job' => 'FS::Misc::process_send_generated_email',
705 'paynum' => $self->paynum,
706 'custnum' => $cust_main->custnum,
707 'msgtype' => 'receipt',
709 $error = $queue->insert(
710 'from' => $conf->config('invoice_from_name', $cust_main->agentnum ) ?
711 $conf->config('invoice_from_name', $cust_main->agentnum ) . ' <' .
712 $conf->config('invoice_from', $cust_main->agentnum ) . '>' :
713 $conf->config('invoice_from', $cust_main->agentnum ),
714 #invoice_from??? well as good as any
715 'to' => \@invoicing_list,
716 'subject' => 'Payment receipt',
717 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
722 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
726 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
728 my $queue = new FS::queue {
729 'job' => 'FS::cust_bill::queueable_email',
730 'paynum' => $self->paynum,
731 'custnum' => $cust_main->custnum,
734 $error = $queue->insert(
735 'invnum' => $cust_bill->invnum,
736 'template' => 'statement',
737 'notice_name' => 'Statement',
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 # Used by FS::Upgrade to migrate to a new database.
938 sub _upgrade_data { #class method
939 my ($class, %opt) = @_;
941 warn "$me upgrading $class\n" if $DEBUG;
943 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
946 # otaker/ivan upgrade
949 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
951 #not the most efficient, but hey, it only has to run once
953 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
954 " AND usernum IS NULL ".
955 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
956 " WHERE cust_main.custnum = cust_pay.custnum ) ";
958 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
960 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
961 $sth->execute or die $sth->errstr;
962 my $total = $sth->fetchrow_arrayref->[0];
963 #warn "$total cust_pay records to update\n"
965 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
970 my @cust_pay = qsearch( {
971 'table' => 'cust_pay',
973 'extra_sql' => $where,
974 'order_by' => 'ORDER BY paynum',
977 foreach my $cust_pay (@cust_pay) {
979 my $h_cust_pay = $cust_pay->h_search('insert');
981 next if $cust_pay->otaker eq $h_cust_pay->history_user;
982 #$cust_pay->otaker($h_cust_pay->history_user);
983 $cust_pay->set('otaker', $h_cust_pay->history_user);
985 $cust_pay->set('otaker', 'legacy');
988 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
989 my $error = $cust_pay->replace;
992 warn " *** WARNING: Error updating order taker for payment paynum ".
993 $cust_pay->paynun. ": $error\n";
997 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1000 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1001 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1007 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1011 # payinfo N/A upgrade
1014 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1016 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1018 my @na_cust_pay = qsearch( {
1019 'table' => 'cust_pay',
1020 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1021 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1024 foreach my $na ( @na_cust_pay ) {
1026 next unless $na->payinfo eq 'N/A';
1028 my $cust_pay_pending =
1029 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1030 unless ( $cust_pay_pending ) {
1031 warn " *** WARNING: not-yet recoverable N/A card for payment ".
1032 $na->paynum. " (no cust_pay_pending)\n";
1035 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1036 my $error = $na->replace;
1038 warn " *** WARNING: Error updating payinfo for payment paynum ".
1039 $na->paynun. ": $error\n";
1045 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1049 # otaker->usernum upgrade
1052 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1053 $class->_upgrade_otaker(%opt);
1054 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1056 # if we do this anywhere else, it should become an FS::Upgrade method
1057 my $num_to_upgrade = $class->count('paybatch is not null');
1058 my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1059 if ( $num_to_upgrade > 0 ) {
1060 warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1061 if ( $opt{queue} ) {
1062 if ( $num_jobs > 0 ) {
1063 warn "Upgrade already queued.\n";
1065 warn "Scheduling upgrade.\n";
1066 my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1070 process_upgrade_paybatch();
1075 sub process_upgrade_paybatch {
1077 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1078 local $FS::UID::AutoCommit = 1;
1081 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1083 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1084 my $search = FS::Cursor->new( {
1085 'table' => 'cust_pay',
1086 'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1088 while (my $cust_pay = $search->fetch) {
1089 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1090 $cust_pay->set('paybatch' => '');
1091 my $error = $cust_pay->replace;
1092 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1097 # migrate gateway info from the misused 'paybatch' field
1100 # not only cust_pay, but also voided and refunded payments
1101 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1102 local $FS::Record::nowarn_classload=1;
1103 # really inefficient, but again, only has to run once
1104 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1105 my $and_batchnum_is_null =
1106 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1107 my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1108 my $search = FS::Cursor->new({
1110 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1111 "AND (paybatch IS NOT NULL ".
1112 "OR (paybatch IS NULL AND auth IS NULL
1113 $and_batchnum_is_null ) )
1114 ORDER BY $pkey DESC"
1116 while ( my $object = $search->fetch ) {
1117 if ( $object->paybatch eq '' ) {
1118 # repair for a previous upgrade that didn't save 'auth'
1119 my $pkey = $object->primary_key;
1120 # find the last history record that had a paybatch value
1122 table => "h_$table",
1124 $pkey => $object->$pkey,
1125 paybatch => { op=>'!=', value=>''},
1126 history_action => 'replace_old',
1128 order_by => 'ORDER BY history_date DESC LIMIT 1',
1131 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1134 # if the paybatch didn't have an auth string, then it's fine
1135 $h->paybatch =~ /:(\w+):/ or next;
1136 # set paybatch to what it was in that record
1137 $object->set('paybatch', $h->paybatch)
1138 # and then upgrade it like the old records
1141 my $parsed = $object->_parse_paybatch;
1142 if (keys %$parsed) {
1143 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1144 $object->set('auth' => $parsed->{authorization});
1145 $object->set('paybatch', '');
1146 my $error = $object->replace;
1147 warn "error parsing CARD/CHEK paybatch fields on $object #".
1148 $object->get($object->primary_key).":\n $error\n"
1153 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1163 =item batch_import HASHREF
1165 Inserts new payments.
1172 my $fh = $param->{filehandle};
1173 my $format = $param->{'format'};
1175 my $agentnum = $param->{agentnum};
1176 my $_date = $param->{_date};
1177 $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1178 my $paybatch = $param->{'paybatch'};
1180 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1181 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1183 # here is the agent virtualization
1184 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1188 if ( $format eq 'simple' ) {
1189 @fields = qw( custnum agent_custid paid payinfo invnum );
1191 } elsif ( $format eq 'extended' ) {
1192 die "unimplemented\n";
1196 die "unknown format $format";
1199 eval "use Text::CSV_XS;";
1202 my $csv = new Text::CSV_XS;
1206 local $SIG{HUP} = 'IGNORE';
1207 local $SIG{INT} = 'IGNORE';
1208 local $SIG{QUIT} = 'IGNORE';
1209 local $SIG{TERM} = 'IGNORE';
1210 local $SIG{TSTP} = 'IGNORE';
1211 local $SIG{PIPE} = 'IGNORE';
1213 my $oldAutoCommit = $FS::UID::AutoCommit;
1214 local $FS::UID::AutoCommit = 0;
1218 while ( defined($line=<$fh>) ) {
1220 $csv->parse($line) or do {
1221 $dbh->rollback if $oldAutoCommit;
1222 return "can't parse: ". $csv->error_input();
1225 my @columns = $csv->fields();
1229 paybatch => $paybatch,
1231 $cust_pay{_date} = $_date if $_date;
1234 foreach my $field ( @fields ) {
1236 if ( $field eq 'agent_custid'
1238 && $columns[0] =~ /\S+/ )
1241 my $agent_custid = $columns[0];
1242 my %hash = ( 'agent_custid' => $agent_custid,
1243 'agentnum' => $agentnum,
1246 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1247 $dbh->rollback if $oldAutoCommit;
1248 return "can't specify custnum with agent_custid $agent_custid";
1251 $cust_main = qsearchs({
1252 'table' => 'cust_main',
1253 'hashref' => \%hash,
1254 'extra_sql' => $extra_sql,
1257 unless ( $cust_main ) {
1258 $dbh->rollback if $oldAutoCommit;
1259 return "can't find customer with agent_custid $agent_custid";
1263 $columns[0] = $cust_main->custnum;
1266 $cust_pay{$field} = shift @columns;
1269 if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1270 && length($1) == $custnum_length ) {
1271 $cust_pay{custnum} = $2;
1274 my $custnum = $cust_pay{custnum};
1276 my $cust_pay = new FS::cust_pay( \%cust_pay );
1277 my $error = $cust_pay->insert;
1279 if ( ! $error && $cust_pay->custnum != $custnum ) {
1280 #invnum was defined, and ->insert set custnum to the customer for that
1281 #invoice, but it wasn't the one the import specified.
1282 $dbh->rollback if $oldAutoCommit;
1283 $error = "specified invoice #". $cust_pay{invnum}.
1284 " is for custnum ". $cust_pay->custnum.
1285 ", not specified custnum $custnum";
1289 $dbh->rollback if $oldAutoCommit;
1290 return "can't insert payment for $line: $error";
1293 if ( $format eq 'simple' ) {
1294 # include agentnum for less surprise?
1295 $cust_main = qsearchs({
1296 'table' => 'cust_main',
1297 'hashref' => { 'custnum' => $cust_pay->custnum },
1298 'extra_sql' => $extra_sql,
1302 unless ( $cust_main ) {
1303 $dbh->rollback if $oldAutoCommit;
1304 return "can't find customer to which payments apply at line: $line";
1307 $error = $cust_main->apply_payments_and_credits;
1309 $dbh->rollback if $oldAutoCommit;
1310 return "can't apply payments to customer for $line: $error";
1318 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1320 return "Empty file!" unless $imported;
1330 Delete and replace methods.
1334 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1335 schema.html from the base documentation.