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;
28 $me = '[FS::cust_pay]';
32 #ask FS::UID to run this stuff for us later
33 FS::UID->install_callback( sub {
35 $unsuspendauto = $conf->exists('unsuspendauto');
38 @encrypted_fields = ('payinfo');
42 FS::cust_pay - Object methods for cust_pay objects
48 $record = new FS::cust_pay \%hash;
49 $record = new FS::cust_pay { 'column' => 'value' };
51 $error = $record->insert;
53 $error = $new_record->replace($old_record);
55 $error = $record->delete;
57 $error = $record->check;
61 An FS::cust_pay object represents a payment; the transfer of money from a
62 customer. FS::cust_pay inherits from FS::Record. The following fields are
69 primary key (assigned automatically for new payments)
73 customer (see L<FS::cust_main>)
77 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
78 L<Time::Local> and L<Date::Parse> for conversion functions.
82 Amount of this payment
86 order taker (see L<FS::access_user>)
90 Payment Type (See L<FS::payinfo_Mixin> for valid values)
94 Payment Information (See L<FS::payinfo_Mixin> for data format)
98 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
102 obsolete text field for tracking card processing or other batch grouping
106 Optional unique identifer to prevent duplicate transactions.
110 books closed flag, empty or `Y'
114 Desired pkgnum when using experimental package balances.
118 The bank where the payment was deposited.
122 The name of the depositor.
126 The deposit account number.
134 The number of the batch this payment came from (see L<FS::pay_batch>),
135 or null if it was processed through a realtime gateway or entered manually.
139 The number of the realtime or batch gateway L<FS::payment_gateway>) this
140 payment was processed through. Null if it was entered manually or processed
141 by the "system default" gateway, which doesn't have a number.
145 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
146 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
147 redundant with C<gatewaynum>.
151 The authorization number returned by the credit card network.
155 The transaction ID returned by the gateway, if any. This is usually what
156 you would use to initiate a void or refund of the payment.
166 Creates a new payment. To add the payment to the databse, see L<"insert">.
170 sub table { 'cust_pay'; }
171 sub cust_linked { $_[0]->cust_main_custnum; }
172 sub cust_unlinked_msg {
174 "WARNING: can't find cust_main.custnum ". $self->custnum.
175 ' (cust_pay.paynum '. $self->paynum. ')';
178 =item insert [ OPTION => VALUE ... ]
180 Adds this payment to the database.
182 For backwards-compatibility and convenience, if the additional field invnum
183 is defined, an FS::cust_bill_pay record for the full amount of the payment
184 will be created. In this case, custnum is optional.
186 If the additional field discount_term is defined then a prepayment discount
187 is taken for that length of time. It is an error for the customer to owe
188 after this payment is made.
190 A hash of optional arguments may be passed. Currently "manual" is supported.
191 If true, a payment receipt is sent instead of a statement when
192 'payment_receipt_email' configuration option is set.
194 About the "manual" flag: Normally, if the 'payment_receipt' config option
195 is set, and the customer has an invoice email address, inserting a payment
196 causes a I<statement> to be emailed to the customer. If the payment is
197 considered "manual" (or if the customer has no invoices), then it will
198 instead send a I<payment receipt>. "manual" should be true whenever a
199 payment is created directly from the web interface, from a user-initiated
200 realtime payment, or from a third-party payment via self-service. It should
201 be I<false> when creating a payment from a billing event or from a batch.
206 my($self, %options) = @_;
208 local $SIG{HUP} = 'IGNORE';
209 local $SIG{INT} = 'IGNORE';
210 local $SIG{QUIT} = 'IGNORE';
211 local $SIG{TERM} = 'IGNORE';
212 local $SIG{TSTP} = 'IGNORE';
213 local $SIG{PIPE} = 'IGNORE';
215 my $oldAutoCommit = $FS::UID::AutoCommit;
216 local $FS::UID::AutoCommit = 0;
220 if ( $self->invnum ) {
221 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
223 $dbh->rollback if $oldAutoCommit;
224 return "Unknown cust_bill.invnum: ". $self->invnum;
226 $self->custnum($cust_bill->custnum );
229 my $error = $self->check;
230 return $error if $error;
232 my $cust_main = $self->cust_main;
233 my $old_balance = $cust_main->balance;
235 $error = $self->SUPER::insert;
237 $dbh->rollback if $oldAutoCommit;
238 return "error inserting cust_pay: $error";
241 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
242 if ( my $months = $self->discount_term ) {
243 # XXX this should be moved out somewhere, but discount_term_values
245 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
246 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
248 # %billing_pkgs contains this customer's active monthly packages.
249 # Recurring fees for those packages will be credited and then rebilled
250 # for the full discount term. Other packages on the last invoice
251 # (canceled, non-monthly recurring, or one-time charges) will be
253 my %billing_pkgs = map { $_->pkgnum => $_ }
254 grep { $_->part_pkg->freq eq '1' }
255 $cust_main->billing_pkgs;
256 my $credit = 0; # sum of recurring charges from that invoice
257 my $last_bill_date = 0; # the real bill date
258 foreach my $item ( $cust_bill->cust_bill_pkg ) {
259 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
260 $credit += $item->recur;
261 $last_bill_date = $item->cust_pkg->last_bill
262 if defined($item->cust_pkg)
263 and $item->cust_pkg->last_bill > $last_bill_date
266 my $cust_credit = new FS::cust_credit {
267 'custnum' => $self->custnum,
268 'amount' => sprintf('%.2f', $credit),
269 'reason' => 'customer chose to prepay for discount',
271 $error = $cust_credit->insert('reason_type' => $credit_type);
273 $dbh->rollback if $oldAutoCommit;
274 return "error inserting prepayment credit: $error";
278 # bill for the entire term
279 $_->bill($_->last_bill) foreach (values %billing_pkgs);
280 $error = $cust_main->bill(
281 # no recurring_only, we want unbilled packages with start dates to
283 'no_usage_reset' => 1,
284 'time' => $last_bill_date, # not $cust_bill->_date
285 'pkg_list' => [ values %billing_pkgs ],
286 'freq_override' => $months,
289 $dbh->rollback if $oldAutoCommit;
290 return "error inserting cust_pay: $error";
292 $error = $cust_main->apply_payments_and_credits;
294 $dbh->rollback if $oldAutoCommit;
295 return "error inserting cust_pay: $error";
297 my $new_balance = $cust_main->balance;
298 if ($new_balance > 0) {
299 $dbh->rollback if $oldAutoCommit;
300 return "balance after prepay discount attempt: $new_balance";
302 # user friendly: override the "apply only to this invoice" mode
309 if ( $self->invnum ) {
310 my $cust_bill_pay = new FS::cust_bill_pay {
311 'invnum' => $self->invnum,
312 'paynum' => $self->paynum,
313 'amount' => $self->paid,
314 '_date' => $self->_date,
316 $error = $cust_bill_pay->insert(%options);
318 if ( $ignore_noapply ) {
319 warn "warning: error inserting cust_bill_pay: $error ".
320 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
322 $dbh->rollback if $oldAutoCommit;
323 return "error inserting cust_bill_pay: $error";
328 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
330 #false laziness w/ cust_credit::insert
331 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
332 my @errors = $cust_main->unsuspend;
334 # side-fx with nested transactions? upstack rolls back?
335 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
341 #bill setup fees for voip_cdr bill_every_call packages
342 #some false laziness w/search in freeside-cdrd
344 'LEFT JOIN part_pkg USING ( pkgpart ) '.
345 "LEFT JOIN part_pkg_option
346 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
347 AND part_pkg_option.optionname = 'bill_every_call' )";
349 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
350 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
352 my @cust_pkg = qsearch({
353 'table' => 'cust_pkg',
354 'addl_from' => $addl_from,
355 'hashref' => { 'custnum' => $self->custnum,
359 'extra_sql' => $extra_sql,
363 warn "voip_cdr bill_every_call packages found; billing customer\n";
364 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
366 warn "WARNING: Error billing customer: $bill_error\n";
369 #end of billing setup fees for voip_cdr bill_every_call packages
371 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
374 my $trigger = $conf->config('payment_receipt-trigger',
375 $self->cust_main->agentnum) || 'cust_pay';
376 if ( $trigger eq 'cust_pay' ) {
377 my $error = $self->send_receipt(
378 'manual' => $options{'manual'},
379 'cust_bill' => $cust_bill,
380 'cust_main' => $cust_main,
382 warn "can't send payment receipt/statement: $error" if $error;
389 =item void [ REASON ]
391 Voids this payment: deletes the payment and all associated applications and
392 adds a record of the voided payment to the FS::cust_pay_void table.
399 local $SIG{HUP} = 'IGNORE';
400 local $SIG{INT} = 'IGNORE';
401 local $SIG{QUIT} = 'IGNORE';
402 local $SIG{TERM} = 'IGNORE';
403 local $SIG{TSTP} = 'IGNORE';
404 local $SIG{PIPE} = 'IGNORE';
406 my $oldAutoCommit = $FS::UID::AutoCommit;
407 local $FS::UID::AutoCommit = 0;
410 my $cust_pay_void = new FS::cust_pay_void ( {
411 map { $_ => $self->get($_) } $self->fields
413 $cust_pay_void->reason(shift) if scalar(@_);
414 my $error = $cust_pay_void->insert;
416 $dbh->rollback if $oldAutoCommit;
420 $error = $self->delete;
422 $dbh->rollback if $oldAutoCommit;
426 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
434 Unless the closed flag is set, deletes this payment and all associated
435 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
436 cases, you want to use the void method instead to leave a record of the
441 # very similar to FS::cust_credit::delete
444 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
446 local $SIG{HUP} = 'IGNORE';
447 local $SIG{INT} = 'IGNORE';
448 local $SIG{QUIT} = 'IGNORE';
449 local $SIG{TERM} = 'IGNORE';
450 local $SIG{TSTP} = 'IGNORE';
451 local $SIG{PIPE} = 'IGNORE';
453 my $oldAutoCommit = $FS::UID::AutoCommit;
454 local $FS::UID::AutoCommit = 0;
457 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
458 my $error = $app->delete;
460 $dbh->rollback if $oldAutoCommit;
465 my $error = $self->SUPER::delete(@_);
467 $dbh->rollback if $oldAutoCommit;
471 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
477 =item replace [ OLD_RECORD ]
479 You can, but probably shouldn't modify payments...
481 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
482 supplied, replaces this record. If there is an error, returns the error,
483 otherwise returns false.
489 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
490 $self->SUPER::replace(@_);
495 Checks all fields to make sure this is a valid payment. If there is an error,
496 returns the error, otherwise returns false. Called by the insert method.
503 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
506 $self->ut_numbern('paynum')
507 || $self->ut_numbern('custnum')
508 || $self->ut_numbern('_date')
509 || $self->ut_money('paid')
510 || $self->ut_alphan('otaker')
511 || $self->ut_textn('paybatch')
512 || $self->ut_textn('payunique')
513 || $self->ut_enum('closed', [ '', 'Y' ])
514 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
515 || $self->ut_textn('bank')
516 || $self->ut_alphan('depositor')
517 || $self->ut_numbern('account')
518 || $self->ut_numbern('teller')
519 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
520 || $self->payinfo_check()
522 return $error if $error;
524 return "paid must be > 0 " if $self->paid <= 0;
526 return "unknown cust_main.custnum: ". $self->custnum
528 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
530 $self->_date(time) unless $self->_date;
532 return "invalid discount_term"
533 if ($self->discount_term && $self->discount_term < 2);
535 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
536 foreach (qw(bank depositor account teller)) {
537 return "$_ required" if $self->get($_) eq '';
541 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
542 # # UNIQUE index should catch this too, without race conditions, but this
543 # # should give a better error message the other 99.9% of the time...
544 # if ( length($self->payunique)
545 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
546 # #well, it *could* be a better error message
547 # return "duplicate transaction".
548 # " - a payment with unique identifer ". $self->payunique.
555 =item send_receipt HASHREF | OPTION => VALUE ...
557 Sends a payment receipt for this payment..
565 Flag indicating the payment is being made manually.
569 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
574 Customer (FS::cust_main) object (for efficiency).
582 my $opt = ref($_[0]) ? shift : { @_ };
584 my $cust_bill = $opt->{'cust_bill'};
585 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
587 my $conf = new FS::Conf;
589 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
591 my @invoicing_list = $cust_main->invoicing_list_emailonly;
592 return '' unless @invoicing_list;
594 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
598 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
599 #|| ! $conf->exists('invoice_html_statement')
603 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
606 my $queue = new FS::queue {
607 'job' => 'FS::Misc::process_send_email',
608 'paynum' => $self->paynum,
609 'custnum' => $cust_main->custnum,
611 $error = $queue->insert(
612 FS::msg_template->by_key($msgnum)->prepare(
613 'cust_main' => $cust_main,
615 'from_config' => 'payment_receipt_from',
619 } elsif ( $conf->exists('payment_receipt_email') ) {
621 my $receipt_template = new Text::Template (
623 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
625 warn "can't create payment receipt template: $Text::Template::ERROR";
629 my $payby = $self->payby;
630 my $payinfo = $self->payinfo;
631 $payby =~ s/^BILL$/Check/ if $payinfo;
632 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
633 $payinfo = $self->paymask
635 $payinfo = $self->decrypt($payinfo);
637 $payby =~ s/^CHEK$/Electronic check/;
640 'date' => time2str("%a %B %o, %Y", $self->_date),
641 'name' => $cust_main->name,
642 'paynum' => $self->paynum,
643 'paid' => sprintf("%.2f", $self->paid),
644 'payby' => ucfirst(lc($payby)),
645 'payinfo' => $payinfo,
646 'balance' => $cust_main->balance,
647 'company_name' => $conf->config('company_name', $cust_main->agentnum),
650 if ( $opt->{'cust_pkg'} ) {
651 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
652 #setup date, other things?
655 my $queue = new FS::queue {
656 'job' => 'FS::Misc::process_send_generated_email',
657 'paynum' => $self->paynum,
658 'custnum' => $cust_main->custnum,
660 $error = $queue->insert(
661 'from' => $conf->config('invoice_from', $cust_main->agentnum),
662 #invoice_from??? well as good as any
663 'to' => \@invoicing_list,
664 'subject' => 'Payment receipt',
665 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
670 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
674 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
676 my $queue = new FS::queue {
677 'job' => 'FS::cust_bill::queueable_email',
678 'paynum' => $self->paynum,
679 'custnum' => $cust_main->custnum,
682 $error = $queue->insert(
683 'invnum' => $cust_bill->invnum,
684 'template' => 'statement',
685 'notice_name' => 'Statement',
691 warn "send_receipt: $error\n" if $error;
696 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
703 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
704 sort { $a->_date <=> $b->_date
705 || $a->invnum <=> $b->invnum }
706 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
710 =item cust_pay_refund
712 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
717 sub cust_pay_refund {
719 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
720 sort { $a->_date <=> $b->_date }
721 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
728 Returns the amount of this payment that is still unapplied; which is
729 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
730 applications (see L<FS::cust_pay_refund>).
736 my $amount = $self->paid;
737 $amount -= $_->amount foreach ( $self->cust_bill_pay );
738 $amount -= $_->amount foreach ( $self->cust_pay_refund );
739 sprintf("%.2f", $amount );
744 Returns the amount of this payment that has not been refuned; which is
745 paid minus all refund applications (see L<FS::cust_pay_refund>).
751 my $amount = $self->paid;
752 $amount -= $_->amount foreach ( $self->cust_pay_refund );
753 sprintf("%.2f", $amount );
758 Returns the "paid" field.
773 =item batch_insert CUST_PAY_OBJECT, ...
775 Class method which inserts multiple payments. Takes a list of FS::cust_pay
776 objects. Returns a list, each element representing the status of inserting the
777 corresponding payment - empty. If there is an error inserting any payment, the
778 entire transaction is rolled back, i.e. all payments are inserted or none are.
780 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
781 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
782 those objects will be inserted with the paynum of the payment, and for
783 each one, an error message or an empty string will be inserted into the
788 my @errors = FS::cust_pay->batch_insert(@cust_pay);
789 my $num_errors = scalar(grep $_, @errors);
790 if ( $num_errors == 0 ) {
791 #success; all payments were inserted
793 #failure; no payments were inserted.
799 my $self = shift; #class method
801 local $SIG{HUP} = 'IGNORE';
802 local $SIG{INT} = 'IGNORE';
803 local $SIG{QUIT} = 'IGNORE';
804 local $SIG{TERM} = 'IGNORE';
805 local $SIG{TSTP} = 'IGNORE';
806 local $SIG{PIPE} = 'IGNORE';
808 my $oldAutoCommit = $FS::UID::AutoCommit;
809 local $FS::UID::AutoCommit = 0;
815 foreach my $cust_pay (@_) {
816 my $error = $cust_pay->insert( 'manual' => 1 );
817 push @errors, $error;
818 $num_errors++ if $error;
820 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
822 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
823 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
827 $cust_bill_pay->set('paynum', $cust_pay->paynum);
828 my $apply_error = $cust_bill_pay->insert;
829 push @errors, $apply_error || '';
830 $num_errors++ if $apply_error;
834 } elsif ( !$error ) { #normal case: apply payments as usual
835 $cust_pay->cust_main->apply_payments;
841 $dbh->rollback if $oldAutoCommit;
843 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
852 Returns an SQL fragment to retreive the unapplied amount.
857 my ($class, $start, $end) = @_;
858 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
859 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
860 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
861 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
865 ( SELECT SUM(amount) FROM cust_bill_pay
866 WHERE cust_pay.paynum = cust_bill_pay.paynum
867 $bill_start $bill_end )
871 ( SELECT SUM(amount) FROM cust_pay_refund
872 WHERE cust_pay.paynum = cust_pay_refund.paynum
873 $refund_start $refund_end )
882 # Used by FS::Upgrade to migrate to a new database.
886 sub _upgrade_data { #class method
887 my ($class, %opts) = @_;
889 warn "$me upgrading $class\n" if $DEBUG;
891 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
894 # otaker/ivan upgrade
897 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
899 #not the most efficient, but hey, it only has to run once
901 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
902 " AND usernum IS NULL ".
903 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
904 " WHERE cust_main.custnum = cust_pay.custnum ) ";
906 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
908 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
909 $sth->execute or die $sth->errstr;
910 my $total = $sth->fetchrow_arrayref->[0];
911 #warn "$total cust_pay records to update\n"
913 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
918 my @cust_pay = qsearch( {
919 'table' => 'cust_pay',
921 'extra_sql' => $where,
922 'order_by' => 'ORDER BY paynum',
925 foreach my $cust_pay (@cust_pay) {
927 my $h_cust_pay = $cust_pay->h_search('insert');
929 next if $cust_pay->otaker eq $h_cust_pay->history_user;
930 #$cust_pay->otaker($h_cust_pay->history_user);
931 $cust_pay->set('otaker', $h_cust_pay->history_user);
933 $cust_pay->set('otaker', 'legacy');
936 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
937 my $error = $cust_pay->replace;
940 warn " *** WARNING: Error updating order taker for payment paynum ".
941 $cust_pay->paynun. ": $error\n";
945 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
948 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
949 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
955 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
959 # payinfo N/A upgrade
962 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
964 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
966 my @na_cust_pay = qsearch( {
967 'table' => 'cust_pay',
968 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
969 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
972 foreach my $na ( @na_cust_pay ) {
974 next unless $na->payinfo eq 'N/A';
976 my $cust_pay_pending =
977 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
978 unless ( $cust_pay_pending ) {
979 warn " *** WARNING: not-yet recoverable N/A card for payment ".
980 $na->paynum. " (no cust_pay_pending)\n";
983 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
984 my $error = $na->replace;
986 warn " *** WARNING: Error updating payinfo for payment paynum ".
987 $na->paynun. ": $error\n";
993 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
997 # otaker->usernum upgrade
1000 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1001 $class->_upgrade_otaker(%opts);
1002 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1005 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1007 my $search = FS::Cursor->new( {
1008 'table' => 'cust_pay',
1009 'addl_from' => ' JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS text) ',
1011 while (my $cust_pay = $search->fetch) {
1012 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1013 $cust_pay->set('paybatch' => '');
1014 my $error = $cust_pay->replace;
1015 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1020 # migrate gateway info from the misused 'paybatch' field
1023 # not only cust_pay, but also voided and refunded payments
1024 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1025 local $FS::Record::nowarn_classload=1;
1026 # really inefficient, but again, only has to run once
1027 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1028 my $and_batchnum_is_null =
1029 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1030 my $search = FS::Cursor->new({
1032 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1033 "AND (paybatch IS NOT NULL ".
1034 "OR (paybatch IS NULL AND auth IS NULL
1035 $and_batchnum_is_null ) )",
1037 while ( my $object = $search->fetch ) {
1038 if ( $object->paybatch eq '' ) {
1039 # repair for a previous upgrade that didn't save 'auth'
1040 my $pkey = $object->primary_key;
1041 # find the last history record that had a paybatch value
1043 table => "h_$table",
1045 $pkey => $object->$pkey,
1046 paybatch => { op=>'!=', value=>''},
1047 history_action => 'replace_old',
1049 order_by => 'ORDER BY history_date DESC LIMIT 1',
1052 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1055 # if the paybatch didn't have an auth string, then it's fine
1056 $h->paybatch =~ /:(\w+):/ or next;
1057 # set paybatch to what it was in that record
1058 $object->set('paybatch', $h->paybatch)
1059 # and then upgrade it like the old records
1062 my $parsed = $object->_parse_paybatch;
1063 if (keys %$parsed) {
1064 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1065 $object->set('auth' => $parsed->{authorization});
1066 $object->set('paybatch', '');
1067 my $error = $object->replace;
1068 warn "error parsing CARD/CHEK paybatch fields on $object #".
1069 $object->get($object->primary_key).":\n $error\n"
1074 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1084 =item batch_import HASHREF
1086 Inserts new payments.
1093 my $fh = $param->{filehandle};
1094 my $agentnum = $param->{agentnum};
1095 my $format = $param->{'format'};
1096 my $paybatch = $param->{'paybatch'};
1098 # here is the agent virtualization
1099 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1103 if ( $format eq 'simple' ) {
1104 @fields = qw( custnum agent_custid paid payinfo );
1106 } elsif ( $format eq 'extended' ) {
1107 die "unimplemented\n";
1111 die "unknown format $format";
1114 eval "use Text::CSV_XS;";
1117 my $csv = new Text::CSV_XS;
1121 local $SIG{HUP} = 'IGNORE';
1122 local $SIG{INT} = 'IGNORE';
1123 local $SIG{QUIT} = 'IGNORE';
1124 local $SIG{TERM} = 'IGNORE';
1125 local $SIG{TSTP} = 'IGNORE';
1126 local $SIG{PIPE} = 'IGNORE';
1128 my $oldAutoCommit = $FS::UID::AutoCommit;
1129 local $FS::UID::AutoCommit = 0;
1133 while ( defined($line=<$fh>) ) {
1135 $csv->parse($line) or do {
1136 $dbh->rollback if $oldAutoCommit;
1137 return "can't parse: ". $csv->error_input();
1140 my @columns = $csv->fields();
1144 paybatch => $paybatch,
1148 foreach my $field ( @fields ) {
1150 if ( $field eq 'agent_custid'
1152 && $columns[0] =~ /\S+/ )
1155 my $agent_custid = $columns[0];
1156 my %hash = ( 'agent_custid' => $agent_custid,
1157 'agentnum' => $agentnum,
1160 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1161 $dbh->rollback if $oldAutoCommit;
1162 return "can't specify custnum with agent_custid $agent_custid";
1165 $cust_main = qsearchs({
1166 'table' => 'cust_main',
1167 'hashref' => \%hash,
1168 'extra_sql' => $extra_sql,
1171 unless ( $cust_main ) {
1172 $dbh->rollback if $oldAutoCommit;
1173 return "can't find customer with agent_custid $agent_custid";
1177 $columns[0] = $cust_main->custnum;
1180 $cust_pay{$field} = shift @columns;
1183 my $cust_pay = new FS::cust_pay( \%cust_pay );
1184 my $error = $cust_pay->insert;
1187 $dbh->rollback if $oldAutoCommit;
1188 return "can't insert payment for $line: $error";
1191 if ( $format eq 'simple' ) {
1192 # include agentnum for less surprise?
1193 $cust_main = qsearchs({
1194 'table' => 'cust_main',
1195 'hashref' => { 'custnum' => $cust_pay->custnum },
1196 'extra_sql' => $extra_sql,
1200 unless ( $cust_main ) {
1201 $dbh->rollback if $oldAutoCommit;
1202 return "can't find customer to which payments apply at line: $line";
1205 $error = $cust_main->apply_payments_and_credits;
1207 $dbh->rollback if $oldAutoCommit;
1208 return "can't apply payments to customer for $line: $error";
1216 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1218 return "Empty file!" unless $imported;
1228 Delete and replace methods.
1232 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1233 schema.html from the base documentation.