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');
39 sub nohistory_fields { ('payinfo'); }
43 FS::cust_pay - Object methods for cust_pay objects
49 $record = new FS::cust_pay \%hash;
50 $record = new FS::cust_pay { 'column' => 'value' };
52 $error = $record->insert;
54 $error = $new_record->replace($old_record);
56 $error = $record->delete;
58 $error = $record->check;
62 An FS::cust_pay object represents a payment; the transfer of money from a
63 customer. FS::cust_pay inherits from FS::Record. The following fields are
70 primary key (assigned automatically for new payments)
74 customer (see L<FS::cust_main>)
78 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
79 L<Time::Local> and L<Date::Parse> for conversion functions.
83 Amount of this payment
87 order taker (see L<FS::access_user>)
91 Payment Type (See L<FS::payinfo_Mixin> for valid values)
95 Payment Information (See L<FS::payinfo_Mixin> for data format)
99 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
103 obsolete text field for tracking card processing or other batch grouping
107 Optional unique identifer to prevent duplicate transactions.
111 books closed flag, empty or `Y'
115 Desired pkgnum when using experimental package balances.
119 The bank where the payment was deposited.
123 The name of the depositor.
127 The deposit account number.
135 The number of the batch this payment came from (see L<FS::pay_batch>),
136 or null if it was processed through a realtime gateway or entered manually.
140 The number of the realtime or batch gateway L<FS::payment_gateway>) this
141 payment was processed through. Null if it was entered manually or processed
142 by the "system default" gateway, which doesn't have a number.
146 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
147 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
148 redundant with C<gatewaynum>.
152 The authorization number returned by the credit card network.
156 The transaction ID returned by the gateway, if any. This is usually what
157 you would use to initiate a void or refund of the payment.
167 Creates a new payment. To add the payment to the databse, see L<"insert">.
171 sub table { 'cust_pay'; }
172 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum; }
173 sub cust_unlinked_msg {
175 "WARNING: can't find cust_main.custnum ". $self->custnum.
176 ' (cust_pay.paynum '. $self->paynum. ')';
179 =item insert [ OPTION => VALUE ... ]
181 Adds this payment to the database.
183 For backwards-compatibility and convenience, if the additional field invnum
184 is defined, an FS::cust_bill_pay record for the full amount of the payment
185 will be created. In this case, custnum is optional.
187 If the additional field discount_term is defined then a prepayment discount
188 is taken for that length of time. It is an error for the customer to owe
189 after this payment is made.
191 A hash of optional arguments may be passed. Currently "manual" is supported.
192 If true, a payment receipt is sent instead of a statement when
193 'payment_receipt_email' configuration option is set.
195 About the "manual" flag: Normally, if the 'payment_receipt' config option
196 is set, and the customer has an invoice email address, inserting a payment
197 causes a I<statement> to be emailed to the customer. If the payment is
198 considered "manual" (or if the customer has no invoices), then it will
199 instead send a I<payment receipt>. "manual" should be true whenever a
200 payment is created directly from the web interface, from a user-initiated
201 realtime payment, or from a third-party payment via self-service. It should
202 be I<false> when creating a payment from a billing event or from a batch.
207 my($self, %options) = @_;
209 local $SIG{HUP} = 'IGNORE';
210 local $SIG{INT} = 'IGNORE';
211 local $SIG{QUIT} = 'IGNORE';
212 local $SIG{TERM} = 'IGNORE';
213 local $SIG{TSTP} = 'IGNORE';
214 local $SIG{PIPE} = 'IGNORE';
216 my $oldAutoCommit = $FS::UID::AutoCommit;
217 local $FS::UID::AutoCommit = 0;
221 if ( $self->invnum ) {
222 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
224 $dbh->rollback if $oldAutoCommit;
225 return "Unknown cust_bill.invnum: ". $self->invnum;
227 $self->custnum($cust_bill->custnum );
230 my $error = $self->check;
231 return $error if $error;
233 my $cust_main = $self->cust_main;
234 my $old_balance = $cust_main->balance;
236 $error = $self->SUPER::insert;
238 $dbh->rollback if $oldAutoCommit;
239 return "error inserting cust_pay: $error";
242 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
243 if ( my $months = $self->discount_term ) {
244 # XXX this should be moved out somewhere, but discount_term_values
246 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
247 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
249 # %billing_pkgs contains this customer's active monthly packages.
250 # Recurring fees for those packages will be credited and then rebilled
251 # for the full discount term. Other packages on the last invoice
252 # (canceled, non-monthly recurring, or one-time charges) will be
254 my %billing_pkgs = map { $_->pkgnum => $_ }
255 grep { $_->part_pkg->freq eq '1' }
256 $cust_main->billing_pkgs;
257 my $credit = 0; # sum of recurring charges from that invoice
258 my $last_bill_date = 0; # the real bill date
259 foreach my $item ( $cust_bill->cust_bill_pkg ) {
260 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
261 $credit += $item->recur;
262 $last_bill_date = $item->cust_pkg->last_bill
263 if defined($item->cust_pkg)
264 and $item->cust_pkg->last_bill > $last_bill_date
267 my $cust_credit = new FS::cust_credit {
268 'custnum' => $self->custnum,
269 'amount' => sprintf('%.2f', $credit),
270 'reason' => 'customer chose to prepay for discount',
272 $error = $cust_credit->insert('reason_type' => $credit_type);
274 $dbh->rollback if $oldAutoCommit;
275 return "error inserting prepayment credit: $error";
279 # bill for the entire term
280 $_->bill($_->last_bill) foreach (values %billing_pkgs);
281 $error = $cust_main->bill(
282 # no recurring_only, we want unbilled packages with start dates to
284 'no_usage_reset' => 1,
285 'time' => $last_bill_date, # not $cust_bill->_date
286 'pkg_list' => [ values %billing_pkgs ],
287 'freq_override' => $months,
290 $dbh->rollback if $oldAutoCommit;
291 return "error inserting cust_pay: $error";
293 $error = $cust_main->apply_payments_and_credits;
295 $dbh->rollback if $oldAutoCommit;
296 return "error inserting cust_pay: $error";
298 my $new_balance = $cust_main->balance;
299 if ($new_balance > 0) {
300 $dbh->rollback if $oldAutoCommit;
301 return "balance after prepay discount attempt: $new_balance";
303 # user friendly: override the "apply only to this invoice" mode
310 if ( $self->invnum ) {
311 my $cust_bill_pay = new FS::cust_bill_pay {
312 'invnum' => $self->invnum,
313 'paynum' => $self->paynum,
314 'amount' => $self->paid,
315 '_date' => $self->_date,
317 $error = $cust_bill_pay->insert(%options);
319 if ( $ignore_noapply ) {
320 warn "warning: error inserting cust_bill_pay: $error ".
321 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
323 $dbh->rollback if $oldAutoCommit;
324 return "error inserting cust_bill_pay: $error";
329 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
331 #false laziness w/ cust_credit::insert
332 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
333 my @errors = $cust_main->unsuspend;
335 # side-fx with nested transactions? upstack rolls back?
336 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
342 #bill setup fees for voip_cdr bill_every_call packages
343 #some false laziness w/search in freeside-cdrd
345 'LEFT JOIN part_pkg USING ( pkgpart ) '.
346 "LEFT JOIN part_pkg_option
347 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
348 AND part_pkg_option.optionname = 'bill_every_call' )";
350 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
351 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
353 my @cust_pkg = qsearch({
354 'table' => 'cust_pkg',
355 'addl_from' => $addl_from,
356 'hashref' => { 'custnum' => $self->custnum,
360 'extra_sql' => $extra_sql,
364 warn "voip_cdr bill_every_call packages found; billing customer\n";
365 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
367 warn "WARNING: Error billing customer: $bill_error\n";
370 #end of billing setup fees for voip_cdr bill_every_call packages
372 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
375 my $trigger = $conf->config('payment_receipt-trigger',
376 $self->cust_main->agentnum) || 'cust_pay';
377 if ( $trigger eq 'cust_pay' ) {
378 my $error = $self->send_receipt(
379 'manual' => $options{'manual'},
380 'cust_bill' => $cust_bill,
381 'cust_main' => $cust_main,
383 warn "can't send payment receipt/statement: $error" if $error;
390 =item void [ REASON ]
392 Voids this payment: deletes the payment and all associated applications and
393 adds a record of the voided payment to the FS::cust_pay_void table.
400 local $SIG{HUP} = 'IGNORE';
401 local $SIG{INT} = 'IGNORE';
402 local $SIG{QUIT} = 'IGNORE';
403 local $SIG{TERM} = 'IGNORE';
404 local $SIG{TSTP} = 'IGNORE';
405 local $SIG{PIPE} = 'IGNORE';
407 my $oldAutoCommit = $FS::UID::AutoCommit;
408 local $FS::UID::AutoCommit = 0;
411 my $cust_pay_void = new FS::cust_pay_void ( {
412 map { $_ => $self->get($_) } $self->fields
414 $cust_pay_void->reason(shift) if scalar(@_);
415 my $error = $cust_pay_void->insert;
417 $dbh->rollback if $oldAutoCommit;
421 $error = $self->delete;
423 $dbh->rollback if $oldAutoCommit;
427 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
435 Unless the closed flag is set, deletes this payment and all associated
436 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
437 cases, you want to use the void method instead to leave a record of the
442 # very similar to FS::cust_credit::delete
445 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
447 local $SIG{HUP} = 'IGNORE';
448 local $SIG{INT} = 'IGNORE';
449 local $SIG{QUIT} = 'IGNORE';
450 local $SIG{TERM} = 'IGNORE';
451 local $SIG{TSTP} = 'IGNORE';
452 local $SIG{PIPE} = 'IGNORE';
454 my $oldAutoCommit = $FS::UID::AutoCommit;
455 local $FS::UID::AutoCommit = 0;
458 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
459 my $error = $app->delete;
461 $dbh->rollback if $oldAutoCommit;
466 my $error = $self->SUPER::delete(@_);
468 $dbh->rollback if $oldAutoCommit;
472 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
478 =item replace [ OLD_RECORD ]
480 You can, but probably shouldn't modify payments...
482 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
483 supplied, replaces this record. If there is an error, returns the error,
484 otherwise returns false.
490 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
491 $self->SUPER::replace(@_);
496 Checks all fields to make sure this is a valid payment. If there is an error,
497 returns the error, otherwise returns false. Called by the insert method.
504 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
507 $self->ut_numbern('paynum')
508 || $self->ut_numbern('custnum')
509 || $self->ut_numbern('_date')
510 || $self->ut_money('paid')
511 || $self->ut_alphan('otaker')
512 || $self->ut_textn('paybatch')
513 || $self->ut_textn('payunique')
514 || $self->ut_enum('closed', [ '', 'Y' ])
515 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
516 || $self->ut_textn('bank')
517 || $self->ut_alphan('depositor')
518 || $self->ut_numbern('account')
519 || $self->ut_numbern('teller')
520 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
521 || $self->payinfo_check()
523 return $error if $error;
525 return "paid must be > 0 " if $self->paid <= 0;
527 return "unknown cust_main.custnum: ". $self->custnum
529 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
531 $self->_date(time) unless $self->_date;
533 return "invalid discount_term"
534 if ($self->discount_term && $self->discount_term < 2);
536 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
537 foreach (qw(bank depositor account teller)) {
538 return "$_ required" if $self->get($_) eq '';
542 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
543 # # UNIQUE index should catch this too, without race conditions, but this
544 # # should give a better error message the other 99.9% of the time...
545 # if ( length($self->payunique)
546 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
547 # #well, it *could* be a better error message
548 # return "duplicate transaction".
549 # " - a payment with unique identifer ". $self->payunique.
556 =item send_receipt HASHREF | OPTION => VALUE ...
558 Sends a payment receipt for this payment..
566 Flag indicating the payment is being made manually.
570 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
575 Customer (FS::cust_main) object (for efficiency).
583 my $opt = ref($_[0]) ? shift : { @_ };
585 my $cust_bill = $opt->{'cust_bill'};
586 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
588 my $conf = new FS::Conf;
590 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
592 my @invoicing_list = $cust_main->invoicing_list_emailonly;
593 return '' unless @invoicing_list;
595 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
599 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
600 #|| ! $conf->exists('invoice_html_statement')
604 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
607 my $queue = new FS::queue {
608 'job' => 'FS::Misc::process_send_email',
609 'paynum' => $self->paynum,
610 'custnum' => $cust_main->custnum,
612 $error = $queue->insert(
613 FS::msg_template->by_key($msgnum)->prepare(
614 'cust_main' => $cust_main,
616 'from_config' => 'payment_receipt_from',
620 } elsif ( $conf->exists('payment_receipt_email') ) {
622 my $receipt_template = new Text::Template (
624 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
626 warn "can't create payment receipt template: $Text::Template::ERROR";
630 my $payby = $self->payby;
631 my $payinfo = $self->payinfo;
632 $payby =~ s/^BILL$/Check/ if $payinfo;
633 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
634 $payinfo = $self->paymask
636 $payinfo = $self->decrypt($payinfo);
638 $payby =~ s/^CHEK$/Electronic check/;
641 'date' => time2str("%a %B %o, %Y", $self->_date),
642 'name' => $cust_main->name,
643 'paynum' => $self->paynum,
644 'paid' => sprintf("%.2f", $self->paid),
645 'payby' => ucfirst(lc($payby)),
646 'payinfo' => $payinfo,
647 'balance' => $cust_main->balance,
648 'company_name' => $conf->config('company_name', $cust_main->agentnum),
651 if ( $opt->{'cust_pkg'} ) {
652 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
653 #setup date, other things?
656 my $queue = new FS::queue {
657 'job' => 'FS::Misc::process_send_generated_email',
658 'paynum' => $self->paynum,
659 'custnum' => $cust_main->custnum,
661 $error = $queue->insert(
662 'from' => $conf->config('invoice_from', $cust_main->agentnum),
663 #invoice_from??? well as good as any
664 'to' => \@invoicing_list,
665 'subject' => 'Payment receipt',
666 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
671 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
675 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
677 my $queue = new FS::queue {
678 'job' => 'FS::cust_bill::queueable_email',
679 'paynum' => $self->paynum,
680 'custnum' => $cust_main->custnum,
683 $error = $queue->insert(
684 'invnum' => $cust_bill->invnum,
685 'template' => 'statement',
686 'notice_name' => 'Statement',
692 warn "send_receipt: $error\n" if $error;
697 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
704 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
705 sort { $a->_date <=> $b->_date
706 || $a->invnum <=> $b->invnum }
707 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
711 =item cust_pay_refund
713 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
718 sub cust_pay_refund {
720 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
721 sort { $a->_date <=> $b->_date }
722 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
729 Returns the amount of this payment that is still unapplied; which is
730 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
731 applications (see L<FS::cust_pay_refund>).
737 my $amount = $self->paid;
738 $amount -= $_->amount foreach ( $self->cust_bill_pay );
739 $amount -= $_->amount foreach ( $self->cust_pay_refund );
740 sprintf("%.2f", $amount );
745 Returns the amount of this payment that has not been refuned; which is
746 paid minus all refund applications (see L<FS::cust_pay_refund>).
752 my $amount = $self->paid;
753 $amount -= $_->amount foreach ( $self->cust_pay_refund );
754 sprintf("%.2f", $amount );
759 Returns the "paid" field.
774 =item batch_insert CUST_PAY_OBJECT, ...
776 Class method which inserts multiple payments. Takes a list of FS::cust_pay
777 objects. Returns a list, each element representing the status of inserting the
778 corresponding payment - empty. If there is an error inserting any payment, the
779 entire transaction is rolled back, i.e. all payments are inserted or none are.
781 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
782 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
783 those objects will be inserted with the paynum of the payment, and for
784 each one, an error message or an empty string will be inserted into the
789 my @errors = FS::cust_pay->batch_insert(@cust_pay);
790 my $num_errors = scalar(grep $_, @errors);
791 if ( $num_errors == 0 ) {
792 #success; all payments were inserted
794 #failure; no payments were inserted.
800 my $self = shift; #class method
802 local $SIG{HUP} = 'IGNORE';
803 local $SIG{INT} = 'IGNORE';
804 local $SIG{QUIT} = 'IGNORE';
805 local $SIG{TERM} = 'IGNORE';
806 local $SIG{TSTP} = 'IGNORE';
807 local $SIG{PIPE} = 'IGNORE';
809 my $oldAutoCommit = $FS::UID::AutoCommit;
810 local $FS::UID::AutoCommit = 0;
816 foreach my $cust_pay (@_) {
817 my $error = $cust_pay->insert( 'manual' => 1 );
818 push @errors, $error;
819 $num_errors++ if $error;
821 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
823 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
824 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
828 $cust_bill_pay->set('paynum', $cust_pay->paynum);
829 my $apply_error = $cust_bill_pay->insert;
830 push @errors, $apply_error || '';
831 $num_errors++ if $apply_error;
835 } elsif ( !$error ) { #normal case: apply payments as usual
836 $cust_pay->cust_main->apply_payments;
842 $dbh->rollback if $oldAutoCommit;
844 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
853 Returns an SQL fragment to retreive the unapplied amount.
858 my ($class, $start, $end) = @_;
859 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
860 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
861 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
862 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
866 ( SELECT SUM(amount) FROM cust_bill_pay
867 WHERE cust_pay.paynum = cust_bill_pay.paynum
868 $bill_start $bill_end )
872 ( SELECT SUM(amount) FROM cust_pay_refund
873 WHERE cust_pay.paynum = cust_pay_refund.paynum
874 $refund_start $refund_end )
883 # Used by FS::Upgrade to migrate to a new database.
887 sub _upgrade_data { #class method
888 my ($class, %opts) = @_;
890 warn "$me upgrading $class\n" if $DEBUG;
892 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
895 # otaker/ivan upgrade
898 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
900 #not the most efficient, but hey, it only has to run once
902 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
903 " AND usernum IS NULL ".
904 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
905 " WHERE cust_main.custnum = cust_pay.custnum ) ";
907 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
909 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
910 $sth->execute or die $sth->errstr;
911 my $total = $sth->fetchrow_arrayref->[0];
912 #warn "$total cust_pay records to update\n"
914 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
919 my @cust_pay = qsearch( {
920 'table' => 'cust_pay',
922 'extra_sql' => $where,
923 'order_by' => 'ORDER BY paynum',
926 foreach my $cust_pay (@cust_pay) {
928 my $h_cust_pay = $cust_pay->h_search('insert');
930 next if $cust_pay->otaker eq $h_cust_pay->history_user;
931 #$cust_pay->otaker($h_cust_pay->history_user);
932 $cust_pay->set('otaker', $h_cust_pay->history_user);
934 $cust_pay->set('otaker', 'legacy');
937 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
938 my $error = $cust_pay->replace;
941 warn " *** WARNING: Error updating order taker for payment paynum ".
942 $cust_pay->paynun. ": $error\n";
946 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
949 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
950 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
956 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
960 # payinfo N/A upgrade
963 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
965 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
967 my @na_cust_pay = qsearch( {
968 'table' => 'cust_pay',
969 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
970 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
973 foreach my $na ( @na_cust_pay ) {
975 next unless $na->payinfo eq 'N/A';
977 my $cust_pay_pending =
978 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
979 unless ( $cust_pay_pending ) {
980 warn " *** WARNING: not-yet recoverable N/A card for payment ".
981 $na->paynum. " (no cust_pay_pending)\n";
984 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
985 my $error = $na->replace;
987 warn " *** WARNING: Error updating payinfo for payment paynum ".
988 $na->paynun. ": $error\n";
994 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
998 # otaker->usernum upgrade
1001 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1002 $class->_upgrade_otaker(%opts);
1003 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1006 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1008 my $search = FS::Cursor->new( {
1009 'table' => 'cust_pay',
1010 'addl_from' => ' JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS text) ',
1012 while (my $cust_pay = $search->fetch) {
1013 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1014 $cust_pay->set('paybatch' => '');
1015 my $error = $cust_pay->replace;
1016 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1021 # migrate gateway info from the misused 'paybatch' field
1024 # not only cust_pay, but also voided and refunded payments
1025 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1026 local $FS::Record::nowarn_classload=1;
1027 # really inefficient, but again, only has to run once
1028 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1029 my $and_batchnum_is_null =
1030 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1031 my $search = FS::Cursor->new({
1033 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1034 "AND (paybatch IS NOT NULL ".
1035 "OR (paybatch IS NULL AND auth IS NULL
1036 $and_batchnum_is_null ) )",
1038 while ( my $object = $search->fetch ) {
1039 if ( $object->paybatch eq '' ) {
1040 # repair for a previous upgrade that didn't save 'auth'
1041 my $pkey = $object->primary_key;
1042 # find the last history record that had a paybatch value
1044 table => "h_$table",
1046 $pkey => $object->$pkey,
1047 paybatch => { op=>'!=', value=>''},
1048 history_action => 'replace_old',
1050 order_by => 'ORDER BY history_date DESC LIMIT 1',
1053 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1056 # if the paybatch didn't have an auth string, then it's fine
1057 $h->paybatch =~ /:(\w+):/ or next;
1058 # set paybatch to what it was in that record
1059 $object->set('paybatch', $h->paybatch)
1060 # and then upgrade it like the old records
1063 my $parsed = $object->_parse_paybatch;
1064 if (keys %$parsed) {
1065 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1066 $object->set('auth' => $parsed->{authorization});
1067 $object->set('paybatch', '');
1068 my $error = $object->replace;
1069 warn "error parsing CARD/CHEK paybatch fields on $object #".
1070 $object->get($object->primary_key).":\n $error\n"
1075 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1085 =item batch_import HASHREF
1087 Inserts new payments.
1094 my $fh = $param->{filehandle};
1095 my $agentnum = $param->{agentnum};
1096 my $format = $param->{'format'};
1097 my $paybatch = $param->{'paybatch'};
1099 # here is the agent virtualization
1100 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1104 if ( $format eq 'simple' ) {
1105 @fields = qw( custnum agent_custid paid payinfo );
1107 } elsif ( $format eq 'extended' ) {
1108 die "unimplemented\n";
1112 die "unknown format $format";
1115 eval "use Text::CSV_XS;";
1118 my $csv = new Text::CSV_XS;
1122 local $SIG{HUP} = 'IGNORE';
1123 local $SIG{INT} = 'IGNORE';
1124 local $SIG{QUIT} = 'IGNORE';
1125 local $SIG{TERM} = 'IGNORE';
1126 local $SIG{TSTP} = 'IGNORE';
1127 local $SIG{PIPE} = 'IGNORE';
1129 my $oldAutoCommit = $FS::UID::AutoCommit;
1130 local $FS::UID::AutoCommit = 0;
1134 while ( defined($line=<$fh>) ) {
1136 $csv->parse($line) or do {
1137 $dbh->rollback if $oldAutoCommit;
1138 return "can't parse: ". $csv->error_input();
1141 my @columns = $csv->fields();
1145 paybatch => $paybatch,
1149 foreach my $field ( @fields ) {
1151 if ( $field eq 'agent_custid'
1153 && $columns[0] =~ /\S+/ )
1156 my $agent_custid = $columns[0];
1157 my %hash = ( 'agent_custid' => $agent_custid,
1158 'agentnum' => $agentnum,
1161 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1162 $dbh->rollback if $oldAutoCommit;
1163 return "can't specify custnum with agent_custid $agent_custid";
1166 $cust_main = qsearchs({
1167 'table' => 'cust_main',
1168 'hashref' => \%hash,
1169 'extra_sql' => $extra_sql,
1172 unless ( $cust_main ) {
1173 $dbh->rollback if $oldAutoCommit;
1174 return "can't find customer with agent_custid $agent_custid";
1178 $columns[0] = $cust_main->custnum;
1181 $cust_pay{$field} = shift @columns;
1184 my $cust_pay = new FS::cust_pay( \%cust_pay );
1185 my $error = $cust_pay->insert;
1188 $dbh->rollback if $oldAutoCommit;
1189 return "can't insert payment for $line: $error";
1192 if ( $format eq 'simple' ) {
1193 # include agentnum for less surprise?
1194 $cust_main = qsearchs({
1195 'table' => 'cust_main',
1196 'hashref' => { 'custnum' => $cust_pay->custnum },
1197 'extra_sql' => $extra_sql,
1201 unless ( $cust_main ) {
1202 $dbh->rollback if $oldAutoCommit;
1203 return "can't find customer to which payments apply at line: $line";
1206 $error = $cust_main->apply_payments_and_credits;
1208 $dbh->rollback if $oldAutoCommit;
1209 return "can't apply payments to customer for $line: $error";
1217 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1219 return "Empty file!" unless $imported;
1229 Delete and replace methods.
1233 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1234 schema.html from the base documentation.