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 $self->custnum($cust_bill->custnum );
245 my $error = $self->check;
246 return $error if $error;
248 my $cust_main = $self->cust_main;
249 my $old_balance = $cust_main->balance;
251 $error = $self->SUPER::insert;
253 $dbh->rollback if $oldAutoCommit;
254 return "error inserting cust_pay: $error";
257 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
258 if ( my $months = $self->discount_term ) {
259 # XXX this should be moved out somewhere, but discount_term_values
261 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
262 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
264 # %billing_pkgs contains this customer's active monthly packages.
265 # Recurring fees for those packages will be credited and then rebilled
266 # for the full discount term. Other packages on the last invoice
267 # (canceled, non-monthly recurring, or one-time charges) will be
269 my %billing_pkgs = map { $_->pkgnum => $_ }
270 grep { $_->part_pkg->freq eq '1' }
271 $cust_main->billing_pkgs;
272 my $credit = 0; # sum of recurring charges from that invoice
273 my $last_bill_date = 0; # the real bill date
274 foreach my $item ( $cust_bill->cust_bill_pkg ) {
275 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
276 $credit += $item->recur;
277 $last_bill_date = $item->cust_pkg->last_bill
278 if defined($item->cust_pkg)
279 and $item->cust_pkg->last_bill > $last_bill_date
282 my $cust_credit = new FS::cust_credit {
283 'custnum' => $self->custnum,
284 'amount' => sprintf('%.2f', $credit),
285 'reason' => 'customer chose to prepay for discount',
287 $error = $cust_credit->insert('reason_type' => $credit_type);
289 $dbh->rollback if $oldAutoCommit;
290 return "error inserting prepayment credit: $error";
294 # bill for the entire term
295 $_->bill($_->last_bill) foreach (values %billing_pkgs);
296 $error = $cust_main->bill(
297 # no recurring_only, we want unbilled packages with start dates to
299 'no_usage_reset' => 1,
300 'time' => $last_bill_date, # not $cust_bill->_date
301 'pkg_list' => [ values %billing_pkgs ],
302 'freq_override' => $months,
305 $dbh->rollback if $oldAutoCommit;
306 return "error inserting cust_pay: $error";
308 $error = $cust_main->apply_payments_and_credits;
310 $dbh->rollback if $oldAutoCommit;
311 return "error inserting cust_pay: $error";
313 my $new_balance = $cust_main->balance;
314 if ($new_balance > 0) {
315 $dbh->rollback if $oldAutoCommit;
316 return "balance after prepay discount attempt: $new_balance";
318 # user friendly: override the "apply only to this invoice" mode
325 if ( $self->invnum ) {
326 my $cust_bill_pay = new FS::cust_bill_pay {
327 'invnum' => $self->invnum,
328 'paynum' => $self->paynum,
329 'amount' => $self->paid,
330 '_date' => $self->_date,
332 $error = $cust_bill_pay->insert(%options);
334 if ( $ignore_noapply ) {
335 warn "warning: error inserting cust_bill_pay: $error ".
336 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
338 $dbh->rollback if $oldAutoCommit;
339 return "error inserting cust_bill_pay: $error";
344 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
346 #false laziness w/ cust_credit::insert
347 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
348 my @errors = $cust_main->unsuspend;
350 # side-fx with nested transactions? upstack rolls back?
351 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
357 #bill setup fees for voip_cdr bill_every_call packages
358 #some false laziness w/search in freeside-cdrd
360 'LEFT JOIN part_pkg USING ( pkgpart ) '.
361 "LEFT JOIN part_pkg_option
362 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
363 AND part_pkg_option.optionname = 'bill_every_call' )";
365 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
366 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
368 my @cust_pkg = qsearch({
369 'table' => 'cust_pkg',
370 'addl_from' => $addl_from,
371 'hashref' => { 'custnum' => $self->custnum,
375 'extra_sql' => $extra_sql,
379 warn "voip_cdr bill_every_call packages found; billing customer\n";
380 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
382 warn "WARNING: Error billing customer: $bill_error\n";
385 #end of billing setup fees for voip_cdr bill_every_call packages
387 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
390 my $trigger = $conf->config('payment_receipt-trigger',
391 $self->cust_main->agentnum) || 'cust_pay';
392 if ( $trigger eq 'cust_pay' ) {
393 my $error = $self->send_receipt(
394 'manual' => $options{'manual'},
395 'noemail' => $options{'noemail'},
396 'cust_bill' => $cust_bill,
397 'cust_main' => $cust_main,
399 warn "can't send payment receipt/statement: $error" if $error;
406 =item void [ REASON ]
408 Voids this payment: deletes the payment and all associated applications and
409 adds a record of the voided payment to the FS::cust_pay_void table.
416 local $SIG{HUP} = 'IGNORE';
417 local $SIG{INT} = 'IGNORE';
418 local $SIG{QUIT} = 'IGNORE';
419 local $SIG{TERM} = 'IGNORE';
420 local $SIG{TSTP} = 'IGNORE';
421 local $SIG{PIPE} = 'IGNORE';
423 my $oldAutoCommit = $FS::UID::AutoCommit;
424 local $FS::UID::AutoCommit = 0;
427 my $cust_pay_void = new FS::cust_pay_void ( {
428 map { $_ => $self->get($_) } $self->fields
430 $cust_pay_void->reason(shift) if scalar(@_);
431 my $error = $cust_pay_void->insert;
433 my $cust_pay_pending =
434 qsearchs('cust_pay_pending', { paynum => $self->paynum });
435 if ( $cust_pay_pending ) {
436 $cust_pay_pending->set('void_paynum', $self->paynum);
437 $cust_pay_pending->set('paynum', '');
438 $error ||= $cust_pay_pending->replace;
441 $error ||= $self->delete;
444 $dbh->rollback if $oldAutoCommit;
448 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
456 Unless the closed flag is set, deletes this payment and all associated
457 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
458 cases, you want to use the void method instead to leave a record of the
463 # very similar to FS::cust_credit::delete
466 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
468 local $SIG{HUP} = 'IGNORE';
469 local $SIG{INT} = 'IGNORE';
470 local $SIG{QUIT} = 'IGNORE';
471 local $SIG{TERM} = 'IGNORE';
472 local $SIG{TSTP} = 'IGNORE';
473 local $SIG{PIPE} = 'IGNORE';
475 my $oldAutoCommit = $FS::UID::AutoCommit;
476 local $FS::UID::AutoCommit = 0;
479 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
480 my $error = $app->delete;
482 $dbh->rollback if $oldAutoCommit;
487 my $error = $self->SUPER::delete(@_);
489 $dbh->rollback if $oldAutoCommit;
493 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
499 =item replace [ OLD_RECORD ]
501 You can, but probably shouldn't modify payments...
503 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
504 supplied, replaces this record. If there is an error, returns the error,
505 otherwise returns false.
511 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
512 $self->SUPER::replace(@_);
517 Checks all fields to make sure this is a valid payment. If there is an error,
518 returns the error, otherwise returns false. Called by the insert method.
525 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
528 $self->ut_numbern('paynum')
529 || $self->ut_numbern('custnum')
530 || $self->ut_numbern('_date')
531 || $self->ut_money('paid')
532 || $self->ut_alphan('otaker')
533 || $self->ut_textn('paybatch')
534 || $self->ut_textn('payunique')
535 || $self->ut_enum('closed', [ '', 'Y' ])
536 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
537 || $self->ut_textn('bank')
538 || $self->ut_alphan('depositor')
539 || $self->ut_numbern('account')
540 || $self->ut_numbern('teller')
541 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
542 || $self->payinfo_check()
544 return $error if $error;
546 return "paid must be > 0 " if $self->paid <= 0;
548 return "unknown cust_main.custnum: ". $self->custnum
550 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
552 $self->_date(time) unless $self->_date;
554 return "invalid discount_term"
555 if ($self->discount_term && $self->discount_term < 2);
557 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
558 foreach (qw(bank depositor account teller)) {
559 return "$_ required" if $self->get($_) eq '';
563 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
564 # # UNIQUE index should catch this too, without race conditions, but this
565 # # should give a better error message the other 99.9% of the time...
566 # if ( length($self->payunique)
567 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
568 # #well, it *could* be a better error message
569 # return "duplicate transaction".
570 # " - a payment with unique identifer ". $self->payunique.
577 =item send_receipt HASHREF | OPTION => VALUE ...
579 Sends a payment receipt for this payment..
587 Flag indicating the payment is being made manually.
591 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
596 Customer (FS::cust_main) object (for efficiency).
600 Don't send an email receipt.
610 my $opt = ref($_[0]) ? shift : { @_ };
612 my $cust_bill = $opt->{'cust_bill'};
613 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
615 my $conf = new FS::Conf;
617 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
619 my @invoicing_list = $cust_main->invoicing_list_emailonly;
620 return '' unless @invoicing_list;
622 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
626 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
627 #|| ! $conf->exists('invoice_html_statement')
631 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
634 my %substitutions = ();
635 $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
637 my $queue = new FS::queue {
638 'job' => 'FS::Misc::process_send_email',
639 'paynum' => $self->paynum,
640 'custnum' => $cust_main->custnum,
642 $error = $queue->insert(
643 FS::msg_template->by_key($msgnum)->prepare(
644 'cust_main' => $cust_main,
646 'from_config' => 'payment_receipt_from',
647 'substitutions' => \%substitutions,
649 'msgtype' => 'receipt', # override msg_template's default
652 } elsif ( $conf->exists('payment_receipt_email') ) {
654 my $receipt_template = new Text::Template (
656 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
658 warn "can't create payment receipt template: $Text::Template::ERROR";
662 my $payby = $self->payby;
663 my $payinfo = $self->payinfo;
664 $payby =~ s/^BILL$/Check/ if $payinfo;
665 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
666 $payinfo = $self->paymask
668 $payinfo = $self->decrypt($payinfo);
670 $payby =~ s/^CHEK$/Electronic check/;
673 'date' => time2str("%a %B %o, %Y", $self->_date),
674 'name' => $cust_main->name,
675 'paynum' => $self->paynum,
676 'paid' => sprintf("%.2f", $self->paid),
677 'payby' => ucfirst(lc($payby)),
678 'payinfo' => $payinfo,
679 'balance' => $cust_main->balance,
680 'company_name' => $conf->config('company_name', $cust_main->agentnum),
683 $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
685 if ( $opt->{'cust_pkg'} ) {
686 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
687 #setup date, other things?
690 my $queue = new FS::queue {
691 'job' => 'FS::Misc::process_send_generated_email',
692 'paynum' => $self->paynum,
693 'custnum' => $cust_main->custnum,
694 'msgtype' => 'receipt',
696 $error = $queue->insert(
697 'from' => $conf->invoice_from_full( $cust_main->agentnum ),
698 #invoice_from??? well as good as any
699 'to' => \@invoicing_list,
700 'subject' => 'Payment receipt',
701 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
706 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
710 #not manual and no noemail flag (here or on the customer)
711 } elsif ( ! $opt->{'noemail'} && ! $cust_main->invoice_noemail ) {
713 my $queue = new FS::queue {
714 'job' => 'FS::cust_bill::queueable_email',
715 'paynum' => $self->paynum,
716 'custnum' => $cust_main->custnum,
720 'invnum' => $cust_bill->invnum,
724 if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
725 $opt{'mode'} = $mode;
727 # backward compatibility, no good fix for this yet as some people may
728 # still have "invoice_latex_statement" and such options
729 $opt{'template'} = 'statement';
730 $opt{'notice_name'} = 'Statement';
733 $error = $queue->insert(%opt);
737 warn "send_receipt: $error\n" if $error;
742 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
749 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
750 sort { $a->_date <=> $b->_date
751 || $a->invnum <=> $b->invnum }
752 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
756 =item cust_pay_refund
758 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
763 sub cust_pay_refund {
765 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
766 sort { $a->_date <=> $b->_date }
767 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
774 Returns the amount of this payment that is still unapplied; which is
775 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
776 applications (see L<FS::cust_pay_refund>).
782 my $amount = $self->paid;
783 $amount -= $_->amount foreach ( $self->cust_bill_pay );
784 $amount -= $_->amount foreach ( $self->cust_pay_refund );
785 sprintf("%.2f", $amount );
790 Returns the amount of this payment that has not been refuned; which is
791 paid minus all refund applications (see L<FS::cust_pay_refund>).
797 my $amount = $self->paid;
798 $amount -= $_->amount foreach ( $self->cust_pay_refund );
799 sprintf("%.2f", $amount );
804 Returns the "paid" field.
819 =item batch_insert CUST_PAY_OBJECT, ...
821 Class method which inserts multiple payments. Takes a list of FS::cust_pay
822 objects. Returns a list, each element representing the status of inserting the
823 corresponding payment - empty. If there is an error inserting any payment, the
824 entire transaction is rolled back, i.e. all payments are inserted or none are.
826 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
827 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
828 those objects will be inserted with the paynum of the payment, and for
829 each one, an error message or an empty string will be inserted into the
834 my @errors = FS::cust_pay->batch_insert(@cust_pay);
835 my $num_errors = scalar(grep $_, @errors);
836 if ( $num_errors == 0 ) {
837 #success; all payments were inserted
839 #failure; no payments were inserted.
845 my $self = shift; #class method
847 local $SIG{HUP} = 'IGNORE';
848 local $SIG{INT} = 'IGNORE';
849 local $SIG{QUIT} = 'IGNORE';
850 local $SIG{TERM} = 'IGNORE';
851 local $SIG{TSTP} = 'IGNORE';
852 local $SIG{PIPE} = 'IGNORE';
854 my $oldAutoCommit = $FS::UID::AutoCommit;
855 local $FS::UID::AutoCommit = 0;
861 foreach my $cust_pay (@_) {
862 my $error = $cust_pay->insert( 'manual' => 1 );
863 push @errors, $error;
864 $num_errors++ if $error;
866 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
868 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
869 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
873 $cust_bill_pay->set('paynum', $cust_pay->paynum);
874 my $apply_error = $cust_bill_pay->insert;
875 push @errors, $apply_error || '';
876 $num_errors++ if $apply_error;
880 } elsif ( !$error ) { #normal case: apply payments as usual
881 $cust_pay->cust_main->apply_payments;
887 $dbh->rollback if $oldAutoCommit;
889 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
898 Returns an SQL fragment to retreive the unapplied amount.
903 my ($class, $start, $end) = @_;
904 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
905 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
906 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
907 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
911 ( SELECT SUM(amount) FROM cust_bill_pay
912 WHERE cust_pay.paynum = cust_bill_pay.paynum
913 $bill_start $bill_end )
917 ( SELECT SUM(amount) FROM cust_pay_refund
918 WHERE cust_pay.paynum = cust_pay_refund.paynum
919 $refund_start $refund_end )
928 my @fields = grep { $_ ne 'payinfo' } $self->fields;
929 +{ ( map { $_=>$self->$_ } @fields ),
935 # Used by FS::Upgrade to migrate to a new database.
939 sub _upgrade_data { #class method
940 my ($class, %opt) = @_;
942 warn "$me upgrading $class\n" if $DEBUG;
944 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
947 # otaker/ivan upgrade
950 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
952 #not the most efficient, but hey, it only has to run once
954 my $where = " WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' )
956 AND EXISTS ( SELECT 1 FROM cust_main
957 WHERE cust_main.custnum = cust_pay.custnum )
960 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
962 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
963 $sth->execute or die $sth->errstr;
964 my $total = $sth->fetchrow_arrayref->[0];
965 #warn "$total cust_pay records to update\n"
967 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
972 my @cust_pay = qsearch( {
973 'table' => 'cust_pay',
975 'extra_sql' => $where,
976 'order_by' => 'ORDER BY paynum',
979 foreach my $cust_pay (@cust_pay) {
981 my $h_cust_pay = $cust_pay->h_search('insert');
983 next if $cust_pay->otaker eq $h_cust_pay->history_user;
984 #$cust_pay->otaker($h_cust_pay->history_user);
985 $cust_pay->set('otaker', $h_cust_pay->history_user);
987 $cust_pay->set('otaker', 'legacy');
990 my $error = $cust_pay->replace;
993 warn " *** WARNING: Error updating order taker for payment paynum ".
994 $cust_pay->paynun. ": $error\n";
999 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1000 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1006 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1010 # payinfo N/A upgrade
1013 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1015 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1017 my @na_cust_pay = qsearch( {
1018 'table' => 'cust_pay',
1019 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1020 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1023 foreach my $na ( @na_cust_pay ) {
1025 next unless $na->payinfo eq 'N/A';
1027 my $cust_pay_pending =
1028 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1029 unless ( $cust_pay_pending ) {
1030 warn " *** WARNING: not-yet recoverable N/A card for payment ".
1031 $na->paynum. " (no cust_pay_pending)\n";
1034 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1035 my $error = $na->replace;
1037 warn " *** WARNING: Error updating payinfo for payment paynum ".
1038 $na->paynun. ": $error\n";
1044 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1048 # otaker->usernum upgrade
1051 $class->_upgrade_otaker(%opt);
1053 # if we do this anywhere else, it should become an FS::Upgrade method
1054 my $num_to_upgrade = $class->count('paybatch is not null');
1055 my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1056 if ( $num_to_upgrade > 0 ) {
1057 warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1058 if ( $opt{queue} ) {
1059 if ( $num_jobs > 0 ) {
1060 warn "Upgrade already queued.\n";
1062 warn "Scheduling upgrade.\n";
1063 my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1067 process_upgrade_paybatch();
1072 sub process_upgrade_paybatch {
1074 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1075 local $FS::UID::AutoCommit = 1;
1078 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1080 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1081 my $search = FS::Cursor->new( {
1082 'table' => 'cust_pay',
1083 'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1085 while (my $cust_pay = $search->fetch) {
1086 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1087 $cust_pay->set('paybatch' => '');
1088 my $error = $cust_pay->replace;
1089 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1094 # migrate gateway info from the misused 'paybatch' field
1097 # not only cust_pay, but also voided and refunded payments
1098 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1099 local $FS::Record::nowarn_classload=1;
1100 # really inefficient, but again, only has to run once
1101 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1102 my $and_batchnum_is_null =
1103 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1104 my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1105 my $search = FS::Cursor->new({
1107 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1108 "AND (paybatch IS NOT NULL ".
1109 "OR (paybatch IS NULL AND auth IS NULL
1110 $and_batchnum_is_null ) )
1111 ORDER BY $pkey DESC"
1113 while ( my $object = $search->fetch ) {
1114 if ( $object->paybatch eq '' ) {
1115 # repair for a previous upgrade that didn't save 'auth'
1116 my $pkey = $object->primary_key;
1117 # find the last history record that had a paybatch value
1119 table => "h_$table",
1121 $pkey => $object->$pkey,
1122 paybatch => { op=>'!=', value=>''},
1123 history_action => 'replace_old',
1125 order_by => 'ORDER BY history_date DESC LIMIT 1',
1128 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1131 # if the paybatch didn't have an auth string, then it's fine
1132 $h->paybatch =~ /:(\w+):/ or next;
1133 # set paybatch to what it was in that record
1134 $object->set('paybatch', $h->paybatch)
1135 # and then upgrade it like the old records
1138 my $parsed = $object->_parse_paybatch;
1139 if (keys %$parsed) {
1140 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1141 $object->set('auth' => $parsed->{authorization});
1142 $object->set('paybatch', '');
1143 my $error = $object->replace;
1144 warn "error parsing CARD/CHEK paybatch fields on $object #".
1145 $object->get($object->primary_key).":\n $error\n"
1150 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1160 =item batch_import HASHREF
1162 Inserts new payments.
1169 my $fh = $param->{filehandle};
1170 my $format = $param->{'format'};
1172 my $agentnum = $param->{agentnum};
1173 my $_date = $param->{_date};
1174 $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1175 my $paybatch = $param->{'paybatch'};
1177 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1178 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1180 # here is the agent virtualization
1181 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1185 if ( $format eq 'simple' ) {
1186 @fields = qw( custnum agent_custid paid payinfo invnum );
1188 } elsif ( $format eq 'extended' ) {
1189 die "unimplemented\n";
1193 die "unknown format $format";
1196 eval "use Text::CSV_XS;";
1199 my $csv = new Text::CSV_XS;
1203 local $SIG{HUP} = 'IGNORE';
1204 local $SIG{INT} = 'IGNORE';
1205 local $SIG{QUIT} = 'IGNORE';
1206 local $SIG{TERM} = 'IGNORE';
1207 local $SIG{TSTP} = 'IGNORE';
1208 local $SIG{PIPE} = 'IGNORE';
1210 my $oldAutoCommit = $FS::UID::AutoCommit;
1211 local $FS::UID::AutoCommit = 0;
1215 while ( defined($line=<$fh>) ) {
1217 $csv->parse($line) or do {
1218 $dbh->rollback if $oldAutoCommit;
1219 return "can't parse: ". $csv->error_input();
1222 my @columns = $csv->fields();
1226 paybatch => $paybatch,
1228 $cust_pay{_date} = $_date if $_date;
1231 foreach my $field ( @fields ) {
1233 if ( $field eq 'agent_custid'
1235 && $columns[0] =~ /\S+/ )
1238 my $agent_custid = $columns[0];
1239 my %hash = ( 'agent_custid' => $agent_custid,
1240 'agentnum' => $agentnum,
1243 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1244 $dbh->rollback if $oldAutoCommit;
1245 return "can't specify custnum with agent_custid $agent_custid";
1248 $cust_main = qsearchs({
1249 'table' => 'cust_main',
1250 'hashref' => \%hash,
1251 'extra_sql' => $extra_sql,
1254 unless ( $cust_main ) {
1255 $dbh->rollback if $oldAutoCommit;
1256 return "can't find customer with agent_custid $agent_custid";
1260 $columns[0] = $cust_main->custnum;
1263 $cust_pay{$field} = shift @columns;
1266 if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1267 && length($1) == $custnum_length ) {
1268 $cust_pay{custnum} = $2;
1271 my $custnum = $cust_pay{custnum};
1273 my $cust_pay = new FS::cust_pay( \%cust_pay );
1274 my $error = $cust_pay->insert;
1276 if ( ! $error && $cust_pay->custnum != $custnum ) {
1277 #invnum was defined, and ->insert set custnum to the customer for that
1278 #invoice, but it wasn't the one the import specified.
1279 $dbh->rollback if $oldAutoCommit;
1280 $error = "specified invoice #". $cust_pay{invnum}.
1281 " is for custnum ". $cust_pay->custnum.
1282 ", not specified custnum $custnum";
1286 $dbh->rollback if $oldAutoCommit;
1287 return "can't insert payment for $line: $error";
1290 if ( $format eq 'simple' ) {
1291 # include agentnum for less surprise?
1292 $cust_main = qsearchs({
1293 'table' => 'cust_main',
1294 'hashref' => { 'custnum' => $cust_pay->custnum },
1295 'extra_sql' => $extra_sql,
1299 unless ( $cust_main ) {
1300 $dbh->rollback if $oldAutoCommit;
1301 return "can't find customer to which payments apply at line: $line";
1304 $error = $cust_main->apply_payments_and_credits;
1306 $dbh->rollback if $oldAutoCommit;
1307 return "can't apply payments to customer for $line: $error";
1315 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1317 return "Empty file!" unless $imported;
1327 Delete and replace methods.
1331 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1332 schema.html from the base documentation.