4 use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin
6 use vars qw( $DEBUG $me $conf @encrypted_fields
7 $unsuspendauto $ignore_noapply
10 use Business::CreditCard;
12 use FS::UID qw( getotaker );
13 use FS::Misc qw( send_email );
14 use FS::Record qw( dbh qsearch qsearchs );
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;
29 $me = '[FS::cust_pay]';
33 #ask FS::UID to run this stuff for us later
34 FS::UID->install_callback( sub {
36 $unsuspendauto = $conf->exists('unsuspendauto');
39 @encrypted_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; }
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.
198 my($self, %options) = @_;
200 local $SIG{HUP} = 'IGNORE';
201 local $SIG{INT} = 'IGNORE';
202 local $SIG{QUIT} = 'IGNORE';
203 local $SIG{TERM} = 'IGNORE';
204 local $SIG{TSTP} = 'IGNORE';
205 local $SIG{PIPE} = 'IGNORE';
207 my $oldAutoCommit = $FS::UID::AutoCommit;
208 local $FS::UID::AutoCommit = 0;
212 if ( $self->invnum ) {
213 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
215 $dbh->rollback if $oldAutoCommit;
216 return "Unknown cust_bill.invnum: ". $self->invnum;
218 $self->custnum($cust_bill->custnum );
221 my $error = $self->check;
222 return $error if $error;
224 my $cust_main = $self->cust_main;
225 my $old_balance = $cust_main->balance;
227 $error = $self->SUPER::insert;
229 $dbh->rollback if $oldAutoCommit;
230 return "error inserting cust_pay: $error";
233 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
234 if ( my $months = $self->discount_term ) {
235 # XXX this should be moved out somewhere, but discount_term_values
237 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
238 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
240 # %billing_pkgs contains this customer's active monthly packages.
241 # Recurring fees for those packages will be credited and then rebilled
242 # for the full discount term. Other packages on the last invoice
243 # (canceled, non-monthly recurring, or one-time charges) will be
245 my %billing_pkgs = map { $_->pkgnum => $_ }
246 grep { $_->part_pkg->freq eq '1' }
247 $cust_main->billing_pkgs;
248 my $credit = 0; # sum of recurring charges from that invoice
249 my $last_bill_date = 0; # the real bill date
250 foreach my $item ( $cust_bill->cust_bill_pkg ) {
251 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
252 $credit += $item->recur;
253 $last_bill_date = $item->cust_pkg->last_bill
254 if defined($item->cust_pkg)
255 and $item->cust_pkg->last_bill > $last_bill_date
258 my $cust_credit = new FS::cust_credit {
259 'custnum' => $self->custnum,
260 'amount' => sprintf('%.2f', $credit),
261 'reason' => 'customer chose to prepay for discount',
263 $error = $cust_credit->insert('reason_type' => $credit_type);
265 $dbh->rollback if $oldAutoCommit;
266 return "error inserting prepayment credit: $error";
270 # bill for the entire term
271 $_->bill($_->last_bill) foreach (values %billing_pkgs);
272 $error = $cust_main->bill(
273 # no recurring_only, we want unbilled packages with start dates to
275 'no_usage_reset' => 1,
276 'time' => $last_bill_date, # not $cust_bill->_date
277 'pkg_list' => [ values %billing_pkgs ],
278 'freq_override' => $months,
281 $dbh->rollback if $oldAutoCommit;
282 return "error inserting cust_pay: $error";
284 $error = $cust_main->apply_payments_and_credits;
286 $dbh->rollback if $oldAutoCommit;
287 return "error inserting cust_pay: $error";
289 my $new_balance = $cust_main->balance;
290 if ($new_balance > 0) {
291 $dbh->rollback if $oldAutoCommit;
292 return "balance after prepay discount attempt: $new_balance";
294 # user friendly: override the "apply only to this invoice" mode
301 if ( $self->invnum ) {
302 my $cust_bill_pay = new FS::cust_bill_pay {
303 'invnum' => $self->invnum,
304 'paynum' => $self->paynum,
305 'amount' => $self->paid,
306 '_date' => $self->_date,
308 $error = $cust_bill_pay->insert(%options);
310 if ( $ignore_noapply ) {
311 warn "warning: error inserting cust_bill_pay: $error ".
312 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
314 $dbh->rollback if $oldAutoCommit;
315 return "error inserting cust_bill_pay: $error";
320 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
322 #false laziness w/ cust_credit::insert
323 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
324 my @errors = $cust_main->unsuspend;
326 # side-fx with nested transactions? upstack rolls back?
327 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
333 #bill setup fees for voip_cdr bill_every_call packages
334 #some false laziness w/search in freeside-cdrd
336 'LEFT JOIN part_pkg USING ( pkgpart ) '.
337 "LEFT JOIN part_pkg_option
338 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
339 AND part_pkg_option.optionname = 'bill_every_call' )";
341 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
342 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
344 my @cust_pkg = qsearch({
345 'table' => 'cust_pkg',
346 'addl_from' => $addl_from,
347 'hashref' => { 'custnum' => $self->custnum,
351 'extra_sql' => $extra_sql,
355 warn "voip_cdr bill_every_call packages found; billing customer\n";
356 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
358 warn "WARNING: Error billing customer: $bill_error\n";
361 #end of billing setup fees for voip_cdr bill_every_call packages
363 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
366 my $trigger = $conf->config('payment_receipt-trigger',
367 $self->cust_main->agentnum) || 'cust_pay';
368 if ( $trigger eq 'cust_pay' ) {
369 my $error = $self->send_receipt(
370 'manual' => $options{'manual'},
371 'cust_bill' => $cust_bill,
372 'cust_main' => $cust_main,
374 warn "can't send payment receipt/statement: $error" if $error;
381 =item void [ REASON ]
383 Voids this payment: deletes the payment and all associated applications and
384 adds a record of the voided payment to the FS::cust_pay_void table.
391 local $SIG{HUP} = 'IGNORE';
392 local $SIG{INT} = 'IGNORE';
393 local $SIG{QUIT} = 'IGNORE';
394 local $SIG{TERM} = 'IGNORE';
395 local $SIG{TSTP} = 'IGNORE';
396 local $SIG{PIPE} = 'IGNORE';
398 my $oldAutoCommit = $FS::UID::AutoCommit;
399 local $FS::UID::AutoCommit = 0;
402 my $cust_pay_void = new FS::cust_pay_void ( {
403 map { $_ => $self->get($_) } $self->fields
405 $cust_pay_void->reason(shift) if scalar(@_);
406 my $error = $cust_pay_void->insert;
408 $dbh->rollback if $oldAutoCommit;
412 $error = $self->delete;
414 $dbh->rollback if $oldAutoCommit;
418 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
426 Unless the closed flag is set, deletes this payment and all associated
427 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
428 cases, you want to use the void method instead to leave a record of the
433 # very similar to FS::cust_credit::delete
436 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
438 local $SIG{HUP} = 'IGNORE';
439 local $SIG{INT} = 'IGNORE';
440 local $SIG{QUIT} = 'IGNORE';
441 local $SIG{TERM} = 'IGNORE';
442 local $SIG{TSTP} = 'IGNORE';
443 local $SIG{PIPE} = 'IGNORE';
445 my $oldAutoCommit = $FS::UID::AutoCommit;
446 local $FS::UID::AutoCommit = 0;
449 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
450 my $error = $app->delete;
452 $dbh->rollback if $oldAutoCommit;
457 my $error = $self->SUPER::delete(@_);
459 $dbh->rollback if $oldAutoCommit;
463 if ( $conf->exists('deletepayments')
464 && $conf->config('deletepayments') ne '' ) {
466 my $cust_main = $self->cust_main;
468 my $error = send_email(
469 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
470 #invoice_from??? well as good as any
471 'to' => $conf->config('deletepayments'),
472 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
474 "This is an automatic message from your Freeside installation\n",
475 "informing you that the following payment has been deleted:\n",
477 'paynum: '. $self->paynum. "\n",
478 'custnum: '. $self->custnum.
479 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
480 'paid: $'. sprintf("%.2f", $self->paid). "\n",
481 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
482 'payby: '. $self->payby. "\n",
483 'payinfo: '. $self->paymask. "\n",
484 'paybatch: '. $self->paybatch. "\n",
489 $dbh->rollback if $oldAutoCommit;
490 return "can't send payment deletion notification: $error";
495 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
501 =item replace [ OLD_RECORD ]
503 You can, but probably shouldn't modify payments...
505 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
506 supplied, replaces this record. If there is an error, returns the error,
507 otherwise returns false.
513 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
514 $self->SUPER::replace(@_);
519 Checks all fields to make sure this is a valid payment. If there is an error,
520 returns the error, otherwise returns false. Called by the insert method.
527 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
530 $self->ut_numbern('paynum')
531 || $self->ut_numbern('custnum')
532 || $self->ut_numbern('_date')
533 || $self->ut_money('paid')
534 || $self->ut_alphan('otaker')
535 || $self->ut_textn('paybatch')
536 || $self->ut_textn('payunique')
537 || $self->ut_enum('closed', [ '', 'Y' ])
538 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
539 || $self->ut_textn('bank')
540 || $self->ut_alphan('depositor')
541 || $self->ut_numbern('account')
542 || $self->ut_numbern('teller')
543 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
544 || $self->payinfo_check()
546 return $error if $error;
548 return "paid must be > 0 " if $self->paid <= 0;
550 return "unknown cust_main.custnum: ". $self->custnum
552 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
554 $self->_date(time) unless $self->_date;
556 return "invalid discount_term"
557 if ($self->discount_term && $self->discount_term < 2);
559 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
560 foreach (qw(bank depositor account teller)) {
561 return "$_ required" if $self->get($_) eq '';
565 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
566 # # UNIQUE index should catch this too, without race conditions, but this
567 # # should give a better error message the other 99.9% of the time...
568 # if ( length($self->payunique)
569 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
570 # #well, it *could* be a better error message
571 # return "duplicate transaction".
572 # " - a payment with unique identifer ". $self->payunique.
579 =item send_receipt HASHREF | OPTION => VALUE ...
581 Sends a payment receipt for this payment..
589 Flag indicating the payment is being made manually.
593 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
598 Customer (FS::cust_main) object (for efficiency).
606 my $opt = ref($_[0]) ? shift : { @_ };
608 my $cust_bill = $opt->{'cust_bill'};
609 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
611 my $conf = new FS::Conf;
613 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
615 my @invoicing_list = $cust_main->invoicing_list_emailonly;
616 return '' unless @invoicing_list;
618 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
622 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
623 #|| ! $conf->exists('invoice_html_statement')
627 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
630 my $queue = new FS::queue {
631 'job' => 'FS::Misc::process_send_email',
632 'paynum' => $self->paynum,
633 'custnum' => $cust_main->custnum,
635 $error = $queue->insert(
636 FS::msg_template->by_key($msgnum)->prepare(
637 'cust_main' => $cust_main,
639 'from_config' => 'payment_receipt_from',
643 } elsif ( $conf->exists('payment_receipt_email') ) {
645 my $receipt_template = new Text::Template (
647 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
649 warn "can't create payment receipt template: $Text::Template::ERROR";
653 my $payby = $self->payby;
654 my $payinfo = $self->payinfo;
655 $payby =~ s/^BILL$/Check/ if $payinfo;
656 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
657 $payinfo = $self->paymask
659 $payinfo = $self->decrypt($payinfo);
661 $payby =~ s/^CHEK$/Electronic check/;
664 'date' => time2str("%a %B %o, %Y", $self->_date),
665 'name' => $cust_main->name,
666 'paynum' => $self->paynum,
667 'paid' => sprintf("%.2f", $self->paid),
668 'payby' => ucfirst(lc($payby)),
669 'payinfo' => $payinfo,
670 'balance' => $cust_main->balance,
671 'company_name' => $conf->config('company_name', $cust_main->agentnum),
674 if ( $opt->{'cust_pkg'} ) {
675 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
676 #setup date, other things?
679 my $queue = new FS::queue {
680 'job' => 'FS::Misc::process_send_generated_email',
681 'paynum' => $self->paynum,
682 'custnum' => $cust_main->custnum,
684 $error = $queue->insert(
685 'from' => $conf->config('invoice_from', $cust_main->agentnum),
686 #invoice_from??? well as good as any
687 'to' => \@invoicing_list,
688 'subject' => 'Payment receipt',
689 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
694 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
698 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
700 my $queue = new FS::queue {
701 'job' => 'FS::cust_bill::queueable_email',
702 'paynum' => $self->paynum,
703 'custnum' => $cust_main->custnum,
706 $error = $queue->insert(
707 'invnum' => $cust_bill->invnum,
708 'template' => 'statement',
709 'notice_name' => 'Statement',
715 warn "send_receipt: $error\n" if $error;
720 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
727 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
728 sort { $a->_date <=> $b->_date
729 || $a->invnum <=> $b->invnum }
730 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
734 =item cust_pay_refund
736 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
741 sub cust_pay_refund {
743 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
744 sort { $a->_date <=> $b->_date }
745 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
752 Returns the amount of this payment that is still unapplied; which is
753 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
754 applications (see L<FS::cust_pay_refund>).
760 my $amount = $self->paid;
761 $amount -= $_->amount foreach ( $self->cust_bill_pay );
762 $amount -= $_->amount foreach ( $self->cust_pay_refund );
763 sprintf("%.2f", $amount );
768 Returns the amount of this payment that has not been refuned; which is
769 paid minus all refund applications (see L<FS::cust_pay_refund>).
775 my $amount = $self->paid;
776 $amount -= $_->amount foreach ( $self->cust_pay_refund );
777 sprintf("%.2f", $amount );
782 Returns the "paid" field.
797 =item batch_insert CUST_PAY_OBJECT, ...
799 Class method which inserts multiple payments. Takes a list of FS::cust_pay
800 objects. Returns a list, each element representing the status of inserting the
801 corresponding payment - empty. If there is an error inserting any payment, the
802 entire transaction is rolled back, i.e. all payments are inserted or none are.
804 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
805 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
806 those objects will be inserted with the paynum of the payment, and for
807 each one, an error message or an empty string will be inserted into the
812 my @errors = FS::cust_pay->batch_insert(@cust_pay);
813 my $num_errors = scalar(grep $_, @errors);
814 if ( $num_errors == 0 ) {
815 #success; all payments were inserted
817 #failure; no payments were inserted.
823 my $self = shift; #class method
825 local $SIG{HUP} = 'IGNORE';
826 local $SIG{INT} = 'IGNORE';
827 local $SIG{QUIT} = 'IGNORE';
828 local $SIG{TERM} = 'IGNORE';
829 local $SIG{TSTP} = 'IGNORE';
830 local $SIG{PIPE} = 'IGNORE';
832 my $oldAutoCommit = $FS::UID::AutoCommit;
833 local $FS::UID::AutoCommit = 0;
839 foreach my $cust_pay (@_) {
840 my $error = $cust_pay->insert( 'manual' => 1 );
841 push @errors, $error;
842 $num_errors++ if $error;
844 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
846 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
847 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
851 $cust_bill_pay->set('paynum', $cust_pay->paynum);
852 my $apply_error = $cust_bill_pay->insert;
853 push @errors, $apply_error || '';
854 $num_errors++ if $apply_error;
858 } elsif ( !$error ) { #normal case: apply payments as usual
859 $cust_pay->cust_main->apply_payments;
865 $dbh->rollback if $oldAutoCommit;
867 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
876 Returns an SQL fragment to retreive the unapplied amount.
881 my ($class, $start, $end) = @_;
882 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
883 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
884 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
885 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
889 ( SELECT SUM(amount) FROM cust_bill_pay
890 WHERE cust_pay.paynum = cust_bill_pay.paynum
891 $bill_start $bill_end )
895 ( SELECT SUM(amount) FROM cust_pay_refund
896 WHERE cust_pay.paynum = cust_pay_refund.paynum
897 $refund_start $refund_end )
906 # Used by FS::Upgrade to migrate to a new database.
910 sub _upgrade_data { #class method
911 my ($class, %opts) = @_;
913 warn "$me upgrading $class\n" if $DEBUG;
915 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
918 # otaker/ivan upgrade
921 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
923 #not the most efficient, but hey, it only has to run once
925 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
926 " AND usernum IS NULL ".
927 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
928 " WHERE cust_main.custnum = cust_pay.custnum ) ";
930 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
932 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
933 $sth->execute or die $sth->errstr;
934 my $total = $sth->fetchrow_arrayref->[0];
935 #warn "$total cust_pay records to update\n"
937 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
942 my @cust_pay = qsearch( {
943 'table' => 'cust_pay',
945 'extra_sql' => $where,
946 'order_by' => 'ORDER BY paynum',
949 foreach my $cust_pay (@cust_pay) {
951 my $h_cust_pay = $cust_pay->h_search('insert');
953 next if $cust_pay->otaker eq $h_cust_pay->history_user;
954 #$cust_pay->otaker($h_cust_pay->history_user);
955 $cust_pay->set('otaker', $h_cust_pay->history_user);
957 $cust_pay->set('otaker', 'legacy');
960 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
961 my $error = $cust_pay->replace;
964 warn " *** WARNING: Error updating order taker for payment paynum ".
965 $cust_pay->paynun. ": $error\n";
969 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
972 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
973 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
979 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
983 # payinfo N/A upgrade
986 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
988 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
990 my @na_cust_pay = qsearch( {
991 'table' => 'cust_pay',
992 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
993 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
996 foreach my $na ( @na_cust_pay ) {
998 next unless $na->payinfo eq 'N/A';
1000 my $cust_pay_pending =
1001 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1002 unless ( $cust_pay_pending ) {
1003 warn " *** WARNING: not-yet recoverable N/A card for payment ".
1004 $na->paynum. " (no cust_pay_pending)\n";
1007 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1008 my $error = $na->replace;
1010 warn " *** WARNING: Error updating payinfo for payment paynum ".
1011 $na->paynun. ": $error\n";
1017 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1021 # otaker->usernum upgrade
1024 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1025 $class->_upgrade_otaker(%opts);
1026 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1029 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1031 my @cust_pay = qsearch( {
1032 'table' => 'cust_pay',
1033 'addl_from' => ' JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS text) ',
1035 foreach my $cust_pay (@cust_pay) {
1036 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1037 $cust_pay->set('paybatch' => '');
1038 my $error = $cust_pay->replace;
1039 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1044 # migrate gateway info from the misused 'paybatch' field
1047 # not only cust_pay, but also voided and refunded payments
1048 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1049 local $FS::Record::nowarn_classload=1;
1050 # really inefficient, but again, only has to run once
1051 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1052 my $and_batchnum_is_null =
1053 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1054 foreach my $object ( qsearch({
1056 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1057 "AND (paybatch IS NOT NULL ".
1058 "OR (paybatch IS NULL AND auth IS NULL
1059 $and_batchnum_is_null ) )",
1062 if ( $object->paybatch eq '' ) {
1063 # repair for a previous upgrade that didn't save 'auth'
1064 my $pkey = $object->primary_key;
1065 # find the last history record that had a paybatch value
1067 table => "h_$table",
1069 $pkey => $object->$pkey,
1070 paybatch => { op=>'!=', value=>''},
1071 history_action => 'replace_old',
1073 order_by => 'ORDER BY history_date DESC LIMIT 1',
1076 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1079 # if the paybatch didn't have an auth string, then it's fine
1080 $h->paybatch =~ /:(\w+):/ or next;
1081 # set paybatch to what it was in that record
1082 $object->set('paybatch', $h->paybatch)
1083 # and then upgrade it like the old records
1086 my $parsed = $object->_parse_paybatch;
1087 if (keys %$parsed) {
1088 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1089 $object->set('auth' => $parsed->{authorization});
1090 $object->set('paybatch', '');
1091 my $error = $object->replace;
1092 warn "error parsing CARD/CHEK paybatch fields on $object #".
1093 $object->get($object->primary_key).":\n $error\n"
1098 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1108 =item batch_import HASHREF
1110 Inserts new payments.
1117 my $fh = $param->{filehandle};
1118 my $agentnum = $param->{agentnum};
1119 my $format = $param->{'format'};
1120 my $paybatch = $param->{'paybatch'};
1122 # here is the agent virtualization
1123 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1127 if ( $format eq 'simple' ) {
1128 @fields = qw( custnum agent_custid paid payinfo );
1130 } elsif ( $format eq 'extended' ) {
1131 die "unimplemented\n";
1135 die "unknown format $format";
1138 eval "use Text::CSV_XS;";
1141 my $csv = new Text::CSV_XS;
1145 local $SIG{HUP} = 'IGNORE';
1146 local $SIG{INT} = 'IGNORE';
1147 local $SIG{QUIT} = 'IGNORE';
1148 local $SIG{TERM} = 'IGNORE';
1149 local $SIG{TSTP} = 'IGNORE';
1150 local $SIG{PIPE} = 'IGNORE';
1152 my $oldAutoCommit = $FS::UID::AutoCommit;
1153 local $FS::UID::AutoCommit = 0;
1157 while ( defined($line=<$fh>) ) {
1159 $csv->parse($line) or do {
1160 $dbh->rollback if $oldAutoCommit;
1161 return "can't parse: ". $csv->error_input();
1164 my @columns = $csv->fields();
1168 paybatch => $paybatch,
1172 foreach my $field ( @fields ) {
1174 if ( $field eq 'agent_custid'
1176 && $columns[0] =~ /\S+/ )
1179 my $agent_custid = $columns[0];
1180 my %hash = ( 'agent_custid' => $agent_custid,
1181 'agentnum' => $agentnum,
1184 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1185 $dbh->rollback if $oldAutoCommit;
1186 return "can't specify custnum with agent_custid $agent_custid";
1189 $cust_main = qsearchs({
1190 'table' => 'cust_main',
1191 'hashref' => \%hash,
1192 'extra_sql' => $extra_sql,
1195 unless ( $cust_main ) {
1196 $dbh->rollback if $oldAutoCommit;
1197 return "can't find customer with agent_custid $agent_custid";
1201 $columns[0] = $cust_main->custnum;
1204 $cust_pay{$field} = shift @columns;
1207 my $cust_pay = new FS::cust_pay( \%cust_pay );
1208 my $error = $cust_pay->insert;
1211 $dbh->rollback if $oldAutoCommit;
1212 return "can't insert payment for $line: $error";
1215 if ( $format eq 'simple' ) {
1216 # include agentnum for less surprise?
1217 $cust_main = qsearchs({
1218 'table' => 'cust_main',
1219 'hashref' => { 'custnum' => $cust_pay->custnum },
1220 'extra_sql' => $extra_sql,
1224 unless ( $cust_main ) {
1225 $dbh->rollback if $oldAutoCommit;
1226 return "can't find customer to which payments apply at line: $line";
1229 $error = $cust_main->apply_payments_and_credits;
1231 $dbh->rollback if $oldAutoCommit;
1232 return "can't apply payments to customer for $line: $error";
1240 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1242 return "Empty file!" unless $imported;
1252 Delete and replace methods.
1256 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1257 schema.html from the base documentation.