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::Record qw( dbh qsearch qsearchs );
15 use FS::cust_main_Mixin;
16 use FS::payinfo_transaction_Mixin;
18 use FS::cust_bill_pay;
19 use FS::cust_pay_refund;
22 use FS::cust_pay_void;
23 use FS::upgrade_journal;
27 $me = '[FS::cust_pay]';
31 #ask FS::UID to run this stuff for us later
32 FS::UID->install_callback( sub {
34 $unsuspendauto = $conf->exists('unsuspendauto');
37 @encrypted_fields = ('payinfo');
41 FS::cust_pay - Object methods for cust_pay objects
47 $record = new FS::cust_pay \%hash;
48 $record = new FS::cust_pay { 'column' => 'value' };
50 $error = $record->insert;
52 $error = $new_record->replace($old_record);
54 $error = $record->delete;
56 $error = $record->check;
60 An FS::cust_pay object represents a payment; the transfer of money from a
61 customer. FS::cust_pay inherits from FS::Record. The following fields are
68 primary key (assigned automatically for new payments)
72 customer (see L<FS::cust_main>)
76 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
77 L<Time::Local> and L<Date::Parse> for conversion functions.
81 Amount of this payment
85 order taker (see L<FS::access_user>)
89 Payment Type (See L<FS::payinfo_Mixin> for valid values)
93 Payment Information (See L<FS::payinfo_Mixin> for data format)
97 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
101 obsolete text field for tracking card processing or other batch grouping
105 Optional unique identifer to prevent duplicate transactions.
109 books closed flag, empty or `Y'
113 Desired pkgnum when using experimental package balances.
117 The bank where the payment was deposited.
121 The name of the depositor.
125 The deposit account number.
133 The number of the batch this payment came from (see L<FS::pay_batch>),
134 or null if it was processed through a realtime gateway or entered manually.
138 The number of the realtime or batch gateway L<FS::payment_gateway>) this
139 payment was processed through. Null if it was entered manually or processed
140 by the "system default" gateway, which doesn't have a number.
144 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
145 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
146 redundant with C<gatewaynum>.
150 The authorization number returned by the credit card network.
154 The transaction ID returned by the gateway, if any. This is usually what
155 you would use to initiate a void or refund of the payment.
165 Creates a new payment. To add the payment to the databse, see L<"insert">.
169 sub table { 'cust_pay'; }
170 sub cust_linked { $_[0]->cust_main_custnum; }
171 sub cust_unlinked_msg {
173 "WARNING: can't find cust_main.custnum ". $self->custnum.
174 ' (cust_pay.paynum '. $self->paynum. ')';
177 =item insert [ OPTION => VALUE ... ]
179 Adds this payment to the database.
181 For backwards-compatibility and convenience, if the additional field invnum
182 is defined, an FS::cust_bill_pay record for the full amount of the payment
183 will be created. In this case, custnum is optional.
185 If the additional field discount_term is defined then a prepayment discount
186 is taken for that length of time. It is an error for the customer to owe
187 after this payment is made.
189 A hash of optional arguments may be passed. Currently "manual" is supported.
190 If true, a payment receipt is sent instead of a statement when
191 'payment_receipt_email' configuration option is set.
196 my($self, %options) = @_;
198 local $SIG{HUP} = 'IGNORE';
199 local $SIG{INT} = 'IGNORE';
200 local $SIG{QUIT} = 'IGNORE';
201 local $SIG{TERM} = 'IGNORE';
202 local $SIG{TSTP} = 'IGNORE';
203 local $SIG{PIPE} = 'IGNORE';
205 my $oldAutoCommit = $FS::UID::AutoCommit;
206 local $FS::UID::AutoCommit = 0;
210 if ( $self->invnum ) {
211 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
213 $dbh->rollback if $oldAutoCommit;
214 return "Unknown cust_bill.invnum: ". $self->invnum;
216 $self->custnum($cust_bill->custnum );
219 my $error = $self->check;
220 return $error if $error;
222 my $cust_main = $self->cust_main;
223 my $old_balance = $cust_main->balance;
225 $error = $self->SUPER::insert;
227 $dbh->rollback if $oldAutoCommit;
228 return "error inserting cust_pay: $error";
231 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
232 if ( my $months = $self->discount_term ) {
233 # XXX this should be moved out somewhere, but discount_term_values
235 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
236 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
238 # %billing_pkgs contains this customer's active monthly packages.
239 # Recurring fees for those packages will be credited and then rebilled
240 # for the full discount term. Other packages on the last invoice
241 # (canceled, non-monthly recurring, or one-time charges) will be
243 my %billing_pkgs = map { $_->pkgnum => $_ }
244 grep { $_->part_pkg->freq eq '1' }
245 $cust_main->billing_pkgs;
246 my $credit = 0; # sum of recurring charges from that invoice
247 my $last_bill_date = 0; # the real bill date
248 foreach my $item ( $cust_bill->cust_bill_pkg ) {
249 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
250 $credit += $item->recur;
251 $last_bill_date = $item->cust_pkg->last_bill
252 if defined($item->cust_pkg)
253 and $item->cust_pkg->last_bill > $last_bill_date
256 my $cust_credit = new FS::cust_credit {
257 'custnum' => $self->custnum,
258 'amount' => sprintf('%.2f', $credit),
259 'reason' => 'customer chose to prepay for discount',
261 $error = $cust_credit->insert('reason_type' => $credit_type);
263 $dbh->rollback if $oldAutoCommit;
264 return "error inserting prepayment credit: $error";
268 # bill for the entire term
269 $_->bill($_->last_bill) foreach (values %billing_pkgs);
270 $error = $cust_main->bill(
271 # no recurring_only, we want unbilled packages with start dates to
273 'no_usage_reset' => 1,
274 'time' => $last_bill_date, # not $cust_bill->_date
275 'pkg_list' => [ values %billing_pkgs ],
276 'freq_override' => $months,
279 $dbh->rollback if $oldAutoCommit;
280 return "error inserting cust_pay: $error";
282 $error = $cust_main->apply_payments_and_credits;
284 $dbh->rollback if $oldAutoCommit;
285 return "error inserting cust_pay: $error";
287 my $new_balance = $cust_main->balance;
288 if ($new_balance > 0) {
289 $dbh->rollback if $oldAutoCommit;
290 return "balance after prepay discount attempt: $new_balance";
292 # user friendly: override the "apply only to this invoice" mode
299 if ( $self->invnum ) {
300 my $cust_bill_pay = new FS::cust_bill_pay {
301 'invnum' => $self->invnum,
302 'paynum' => $self->paynum,
303 'amount' => $self->paid,
304 '_date' => $self->_date,
306 $error = $cust_bill_pay->insert(%options);
308 if ( $ignore_noapply ) {
309 warn "warning: error inserting cust_bill_pay: $error ".
310 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
312 $dbh->rollback if $oldAutoCommit;
313 return "error inserting cust_bill_pay: $error";
318 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
320 #false laziness w/ cust_credit::insert
321 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
322 my @errors = $cust_main->unsuspend;
324 # side-fx with nested transactions? upstack rolls back?
325 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
331 #bill setup fees for voip_cdr bill_every_call packages
332 #some false laziness w/search in freeside-cdrd
334 'LEFT JOIN part_pkg USING ( pkgpart ) '.
335 "LEFT JOIN part_pkg_option
336 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
337 AND part_pkg_option.optionname = 'bill_every_call' )";
339 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
340 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
342 my @cust_pkg = qsearch({
343 'table' => 'cust_pkg',
344 'addl_from' => $addl_from,
345 'hashref' => { 'custnum' => $self->custnum,
349 'extra_sql' => $extra_sql,
353 warn "voip_cdr bill_every_call packages found; billing customer\n";
354 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
356 warn "WARNING: Error billing customer: $bill_error\n";
359 #end of billing setup fees for voip_cdr bill_every_call packages
361 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
364 my $trigger = $conf->config('payment_receipt-trigger',
365 $self->cust_main->agentnum) || 'cust_pay';
366 if ( $trigger eq 'cust_pay' ) {
367 my $error = $self->send_receipt(
368 'manual' => $options{'manual'},
369 'cust_bill' => $cust_bill,
370 'cust_main' => $cust_main,
372 warn "can't send payment receipt/statement: $error" if $error;
379 =item void [ REASON ]
381 Voids this payment: deletes the payment and all associated applications and
382 adds a record of the voided payment to the FS::cust_pay_void table.
389 local $SIG{HUP} = 'IGNORE';
390 local $SIG{INT} = 'IGNORE';
391 local $SIG{QUIT} = 'IGNORE';
392 local $SIG{TERM} = 'IGNORE';
393 local $SIG{TSTP} = 'IGNORE';
394 local $SIG{PIPE} = 'IGNORE';
396 my $oldAutoCommit = $FS::UID::AutoCommit;
397 local $FS::UID::AutoCommit = 0;
400 my $cust_pay_void = new FS::cust_pay_void ( {
401 map { $_ => $self->get($_) } $self->fields
403 $cust_pay_void->reason(shift) if scalar(@_);
404 my $error = $cust_pay_void->insert;
406 $dbh->rollback if $oldAutoCommit;
410 $error = $self->delete;
412 $dbh->rollback if $oldAutoCommit;
416 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
424 Unless the closed flag is set, deletes this payment and all associated
425 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
426 cases, you want to use the void method instead to leave a record of the
431 # very similar to FS::cust_credit::delete
434 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
436 local $SIG{HUP} = 'IGNORE';
437 local $SIG{INT} = 'IGNORE';
438 local $SIG{QUIT} = 'IGNORE';
439 local $SIG{TERM} = 'IGNORE';
440 local $SIG{TSTP} = 'IGNORE';
441 local $SIG{PIPE} = 'IGNORE';
443 my $oldAutoCommit = $FS::UID::AutoCommit;
444 local $FS::UID::AutoCommit = 0;
447 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
448 my $error = $app->delete;
450 $dbh->rollback if $oldAutoCommit;
455 my $error = $self->SUPER::delete(@_);
457 $dbh->rollback if $oldAutoCommit;
461 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
467 =item replace [ OLD_RECORD ]
469 You can, but probably shouldn't modify payments...
471 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
472 supplied, replaces this record. If there is an error, returns the error,
473 otherwise returns false.
479 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
480 $self->SUPER::replace(@_);
485 Checks all fields to make sure this is a valid payment. If there is an error,
486 returns the error, otherwise returns false. Called by the insert method.
493 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
496 $self->ut_numbern('paynum')
497 || $self->ut_numbern('custnum')
498 || $self->ut_numbern('_date')
499 || $self->ut_money('paid')
500 || $self->ut_alphan('otaker')
501 || $self->ut_textn('paybatch')
502 || $self->ut_textn('payunique')
503 || $self->ut_enum('closed', [ '', 'Y' ])
504 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
505 || $self->ut_textn('bank')
506 || $self->ut_alphan('depositor')
507 || $self->ut_numbern('account')
508 || $self->ut_numbern('teller')
509 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
510 || $self->payinfo_check()
512 return $error if $error;
514 return "paid must be > 0 " if $self->paid <= 0;
516 return "unknown cust_main.custnum: ". $self->custnum
518 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
520 $self->_date(time) unless $self->_date;
522 return "invalid discount_term"
523 if ($self->discount_term && $self->discount_term < 2);
525 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
526 foreach (qw(bank depositor account teller)) {
527 return "$_ required" if $self->get($_) eq '';
531 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
532 # # UNIQUE index should catch this too, without race conditions, but this
533 # # should give a better error message the other 99.9% of the time...
534 # if ( length($self->payunique)
535 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
536 # #well, it *could* be a better error message
537 # return "duplicate transaction".
538 # " - a payment with unique identifer ". $self->payunique.
545 =item send_receipt HASHREF | OPTION => VALUE ...
547 Sends a payment receipt for this payment..
555 Flag indicating the payment is being made manually.
559 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
564 Customer (FS::cust_main) object (for efficiency).
572 my $opt = ref($_[0]) ? shift : { @_ };
574 my $cust_bill = $opt->{'cust_bill'};
575 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
577 my $conf = new FS::Conf;
579 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
581 my @invoicing_list = $cust_main->invoicing_list_emailonly;
582 return '' unless @invoicing_list;
584 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
588 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
589 #|| ! $conf->exists('invoice_html_statement')
593 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
596 my $queue = new FS::queue {
597 'job' => 'FS::Misc::process_send_email',
598 'paynum' => $self->paynum,
599 'custnum' => $cust_main->custnum,
601 $error = $queue->insert(
602 FS::msg_template->by_key($msgnum)->prepare(
603 'cust_main' => $cust_main,
605 'from_config' => 'payment_receipt_from',
609 } elsif ( $conf->exists('payment_receipt_email') ) {
611 my $receipt_template = new Text::Template (
613 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
615 warn "can't create payment receipt template: $Text::Template::ERROR";
619 my $payby = $self->payby;
620 my $payinfo = $self->payinfo;
621 $payby =~ s/^BILL$/Check/ if $payinfo;
622 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
623 $payinfo = $self->paymask
625 $payinfo = $self->decrypt($payinfo);
627 $payby =~ s/^CHEK$/Electronic check/;
630 'date' => time2str("%a %B %o, %Y", $self->_date),
631 'name' => $cust_main->name,
632 'paynum' => $self->paynum,
633 'paid' => sprintf("%.2f", $self->paid),
634 'payby' => ucfirst(lc($payby)),
635 'payinfo' => $payinfo,
636 'balance' => $cust_main->balance,
637 'company_name' => $conf->config('company_name', $cust_main->agentnum),
640 if ( $opt->{'cust_pkg'} ) {
641 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
642 #setup date, other things?
645 my $queue = new FS::queue {
646 'job' => 'FS::Misc::process_send_generated_email',
647 'paynum' => $self->paynum,
648 'custnum' => $cust_main->custnum,
650 $error = $queue->insert(
651 'from' => $conf->config('invoice_from', $cust_main->agentnum),
652 #invoice_from??? well as good as any
653 'to' => \@invoicing_list,
654 'subject' => 'Payment receipt',
655 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
660 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
664 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
666 my $queue = new FS::queue {
667 'job' => 'FS::cust_bill::queueable_email',
668 'paynum' => $self->paynum,
669 'custnum' => $cust_main->custnum,
672 $error = $queue->insert(
673 'invnum' => $cust_bill->invnum,
674 'template' => 'statement',
675 'notice_name' => 'Statement',
681 warn "send_receipt: $error\n" if $error;
686 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
693 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
694 sort { $a->_date <=> $b->_date
695 || $a->invnum <=> $b->invnum }
696 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
700 =item cust_pay_refund
702 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
707 sub cust_pay_refund {
709 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
710 sort { $a->_date <=> $b->_date }
711 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
718 Returns the amount of this payment that is still unapplied; which is
719 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
720 applications (see L<FS::cust_pay_refund>).
726 my $amount = $self->paid;
727 $amount -= $_->amount foreach ( $self->cust_bill_pay );
728 $amount -= $_->amount foreach ( $self->cust_pay_refund );
729 sprintf("%.2f", $amount );
734 Returns the amount of this payment that has not been refuned; which is
735 paid minus all refund applications (see L<FS::cust_pay_refund>).
741 my $amount = $self->paid;
742 $amount -= $_->amount foreach ( $self->cust_pay_refund );
743 sprintf("%.2f", $amount );
748 Returns the "paid" field.
763 =item batch_insert CUST_PAY_OBJECT, ...
765 Class method which inserts multiple payments. Takes a list of FS::cust_pay
766 objects. Returns a list, each element representing the status of inserting the
767 corresponding payment - empty. If there is an error inserting any payment, the
768 entire transaction is rolled back, i.e. all payments are inserted or none are.
770 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
771 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
772 those objects will be inserted with the paynum of the payment, and for
773 each one, an error message or an empty string will be inserted into the
778 my @errors = FS::cust_pay->batch_insert(@cust_pay);
779 my $num_errors = scalar(grep $_, @errors);
780 if ( $num_errors == 0 ) {
781 #success; all payments were inserted
783 #failure; no payments were inserted.
789 my $self = shift; #class method
791 local $SIG{HUP} = 'IGNORE';
792 local $SIG{INT} = 'IGNORE';
793 local $SIG{QUIT} = 'IGNORE';
794 local $SIG{TERM} = 'IGNORE';
795 local $SIG{TSTP} = 'IGNORE';
796 local $SIG{PIPE} = 'IGNORE';
798 my $oldAutoCommit = $FS::UID::AutoCommit;
799 local $FS::UID::AutoCommit = 0;
805 foreach my $cust_pay (@_) {
806 my $error = $cust_pay->insert( 'manual' => 1 );
807 push @errors, $error;
808 $num_errors++ if $error;
810 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
812 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
813 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
817 $cust_bill_pay->set('paynum', $cust_pay->paynum);
818 my $apply_error = $cust_bill_pay->insert;
819 push @errors, $apply_error || '';
820 $num_errors++ if $apply_error;
824 } elsif ( !$error ) { #normal case: apply payments as usual
825 $cust_pay->cust_main->apply_payments;
831 $dbh->rollback if $oldAutoCommit;
833 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
842 Returns an SQL fragment to retreive the unapplied amount.
847 my ($class, $start, $end) = @_;
848 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
849 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
850 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
851 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
855 ( SELECT SUM(amount) FROM cust_bill_pay
856 WHERE cust_pay.paynum = cust_bill_pay.paynum
857 $bill_start $bill_end )
861 ( SELECT SUM(amount) FROM cust_pay_refund
862 WHERE cust_pay.paynum = cust_pay_refund.paynum
863 $refund_start $refund_end )
872 # Used by FS::Upgrade to migrate to a new database.
876 sub _upgrade_data { #class method
877 my ($class, %opts) = @_;
879 warn "$me upgrading $class\n" if $DEBUG;
881 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
884 # otaker/ivan upgrade
887 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
889 #not the most efficient, but hey, it only has to run once
891 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
892 " AND usernum IS NULL ".
893 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
894 " WHERE cust_main.custnum = cust_pay.custnum ) ";
896 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
898 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
899 $sth->execute or die $sth->errstr;
900 my $total = $sth->fetchrow_arrayref->[0];
901 #warn "$total cust_pay records to update\n"
903 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
908 my @cust_pay = qsearch( {
909 'table' => 'cust_pay',
911 'extra_sql' => $where,
912 'order_by' => 'ORDER BY paynum',
915 foreach my $cust_pay (@cust_pay) {
917 my $h_cust_pay = $cust_pay->h_search('insert');
919 next if $cust_pay->otaker eq $h_cust_pay->history_user;
920 #$cust_pay->otaker($h_cust_pay->history_user);
921 $cust_pay->set('otaker', $h_cust_pay->history_user);
923 $cust_pay->set('otaker', 'legacy');
926 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
927 my $error = $cust_pay->replace;
930 warn " *** WARNING: Error updating order taker for payment paynum ".
931 $cust_pay->paynun. ": $error\n";
935 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
938 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
939 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
945 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
949 # payinfo N/A upgrade
952 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
954 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
956 my @na_cust_pay = qsearch( {
957 'table' => 'cust_pay',
958 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
959 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
962 foreach my $na ( @na_cust_pay ) {
964 next unless $na->payinfo eq 'N/A';
966 my $cust_pay_pending =
967 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
968 unless ( $cust_pay_pending ) {
969 warn " *** WARNING: not-yet recoverable N/A card for payment ".
970 $na->paynum. " (no cust_pay_pending)\n";
973 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
974 my $error = $na->replace;
976 warn " *** WARNING: Error updating payinfo for payment paynum ".
977 $na->paynun. ": $error\n";
983 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
987 # otaker->usernum upgrade
990 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
991 $class->_upgrade_otaker(%opts);
992 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
995 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
997 my @cust_pay = qsearch( {
998 'table' => 'cust_pay',
999 'addl_from' => ' JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS text) ',
1001 foreach my $cust_pay (@cust_pay) {
1002 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1003 $cust_pay->set('paybatch' => '');
1004 my $error = $cust_pay->replace;
1005 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1010 # migrate gateway info from the misused 'paybatch' field
1013 # not only cust_pay, but also voided and refunded payments
1014 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1015 local $FS::Record::nowarn_classload=1;
1016 # really inefficient, but again, only has to run once
1017 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1018 my $and_batchnum_is_null =
1019 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1020 foreach my $object ( qsearch({
1022 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1023 "AND (paybatch IS NOT NULL ".
1024 "OR (paybatch IS NULL AND auth IS NULL
1025 $and_batchnum_is_null ) )",
1028 if ( $object->paybatch eq '' ) {
1029 # repair for a previous upgrade that didn't save 'auth'
1030 my $pkey = $object->primary_key;
1031 # find the last history record that had a paybatch value
1033 table => "h_$table",
1035 $pkey => $object->$pkey,
1036 paybatch => { op=>'!=', value=>''},
1037 history_action => 'replace_old',
1039 order_by => 'ORDER BY history_date DESC LIMIT 1',
1042 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1045 # if the paybatch didn't have an auth string, then it's fine
1046 $h->paybatch =~ /:(\w+):/ or next;
1047 # set paybatch to what it was in that record
1048 $object->set('paybatch', $h->paybatch)
1049 # and then upgrade it like the old records
1052 my $parsed = $object->_parse_paybatch;
1053 if (keys %$parsed) {
1054 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1055 $object->set('auth' => $parsed->{authorization});
1056 $object->set('paybatch', '');
1057 my $error = $object->replace;
1058 warn "error parsing CARD/CHEK paybatch fields on $object #".
1059 $object->get($object->primary_key).":\n $error\n"
1064 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1074 =item batch_import HASHREF
1076 Inserts new payments.
1083 my $fh = $param->{filehandle};
1084 my $agentnum = $param->{agentnum};
1085 my $format = $param->{'format'};
1086 my $paybatch = $param->{'paybatch'};
1088 # here is the agent virtualization
1089 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1093 if ( $format eq 'simple' ) {
1094 @fields = qw( custnum agent_custid paid payinfo );
1096 } elsif ( $format eq 'extended' ) {
1097 die "unimplemented\n";
1101 die "unknown format $format";
1104 eval "use Text::CSV_XS;";
1107 my $csv = new Text::CSV_XS;
1111 local $SIG{HUP} = 'IGNORE';
1112 local $SIG{INT} = 'IGNORE';
1113 local $SIG{QUIT} = 'IGNORE';
1114 local $SIG{TERM} = 'IGNORE';
1115 local $SIG{TSTP} = 'IGNORE';
1116 local $SIG{PIPE} = 'IGNORE';
1118 my $oldAutoCommit = $FS::UID::AutoCommit;
1119 local $FS::UID::AutoCommit = 0;
1123 while ( defined($line=<$fh>) ) {
1125 $csv->parse($line) or do {
1126 $dbh->rollback if $oldAutoCommit;
1127 return "can't parse: ". $csv->error_input();
1130 my @columns = $csv->fields();
1134 paybatch => $paybatch,
1138 foreach my $field ( @fields ) {
1140 if ( $field eq 'agent_custid'
1142 && $columns[0] =~ /\S+/ )
1145 my $agent_custid = $columns[0];
1146 my %hash = ( 'agent_custid' => $agent_custid,
1147 'agentnum' => $agentnum,
1150 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1151 $dbh->rollback if $oldAutoCommit;
1152 return "can't specify custnum with agent_custid $agent_custid";
1155 $cust_main = qsearchs({
1156 'table' => 'cust_main',
1157 'hashref' => \%hash,
1158 'extra_sql' => $extra_sql,
1161 unless ( $cust_main ) {
1162 $dbh->rollback if $oldAutoCommit;
1163 return "can't find customer with agent_custid $agent_custid";
1167 $columns[0] = $cust_main->custnum;
1170 $cust_pay{$field} = shift @columns;
1173 my $cust_pay = new FS::cust_pay( \%cust_pay );
1174 my $error = $cust_pay->insert;
1177 $dbh->rollback if $oldAutoCommit;
1178 return "can't insert payment for $line: $error";
1181 if ( $format eq 'simple' ) {
1182 # include agentnum for less surprise?
1183 $cust_main = qsearchs({
1184 'table' => 'cust_main',
1185 'hashref' => { 'custnum' => $cust_pay->custnum },
1186 'extra_sql' => $extra_sql,
1190 unless ( $cust_main ) {
1191 $dbh->rollback if $oldAutoCommit;
1192 return "can't find customer to which payments apply at line: $line";
1195 $error = $cust_main->apply_payments_and_credits;
1197 $dbh->rollback if $oldAutoCommit;
1198 return "can't apply payments to customer for $line: $error";
1206 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1208 return "Empty file!" unless $imported;
1218 Delete and replace methods.
1222 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1223 schema.html from the base documentation.