4 use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin
6 use vars qw( $DEBUG $me $conf @encrypted_fields
7 $unsuspendauto $ignore_noapply
10 use Business::CreditCard;
12 use FS::Misc::DateTime qw( parse_datetime ); #for batch_import
13 use FS::Record qw( dbh qsearch qsearchs );
16 use FS::cust_main_Mixin;
17 use FS::payinfo_transaction_Mixin;
19 use FS::cust_bill_pay;
20 use FS::cust_pay_refund;
23 use FS::cust_pay_void;
24 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');
40 sub nohistory_fields { ('payinfo'); }
44 FS::cust_pay - Object methods for cust_pay objects
50 $record = new FS::cust_pay \%hash;
51 $record = new FS::cust_pay { 'column' => 'value' };
53 $error = $record->insert;
55 $error = $new_record->replace($old_record);
57 $error = $record->delete;
59 $error = $record->check;
63 An FS::cust_pay object represents a payment; the transfer of money from a
64 customer. FS::cust_pay inherits from FS::Record. The following fields are
71 primary key (assigned automatically for new payments)
75 customer (see L<FS::cust_main>)
79 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
80 L<Time::Local> and L<Date::Parse> for conversion functions.
84 Amount of this payment
88 order taker (see L<FS::access_user>)
92 Payment Type (See L<FS::payinfo_Mixin> for valid values)
96 Payment Information (See L<FS::payinfo_Mixin> for data format)
100 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
104 obsolete text field for tracking card processing or other batch grouping
108 Optional unique identifer to prevent duplicate transactions.
112 books closed flag, empty or `Y'
116 Desired pkgnum when using experimental package balances.
120 The bank where the payment was deposited.
124 The name of the depositor.
128 The deposit account number.
136 The number of the batch this payment came from (see L<FS::pay_batch>),
137 or null if it was processed through a realtime gateway or entered manually.
141 The number of the realtime or batch gateway L<FS::payment_gateway>) this
142 payment was processed through. Null if it was entered manually or processed
143 by the "system default" gateway, which doesn't have a number.
147 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
148 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
149 redundant with C<gatewaynum>.
153 The authorization number returned by the credit card network.
157 The transaction ID returned by the gateway, if any. This is usually what
158 you would use to initiate a void or refund of the payment.
168 Creates a new payment. To add the payment to the databse, see L<"insert">.
172 sub table { 'cust_pay'; }
173 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum; }
174 sub cust_unlinked_msg {
176 "WARNING: can't find cust_main.custnum ". $self->custnum.
177 ' (cust_pay.paynum '. $self->paynum. ')';
180 =item insert [ OPTION => VALUE ... ]
182 Adds this payment to the database.
184 For backwards-compatibility and convenience, if the additional field invnum
185 is defined, an FS::cust_bill_pay record for the full amount of the payment
186 will be created. In this case, custnum is optional.
188 If the additional field discount_term is defined then a prepayment discount
189 is taken for that length of time. It is an error for the customer to owe
190 after this payment is made.
192 A hash of optional arguments may be passed. Currently "manual" is supported.
193 If true, a payment receipt is sent instead of a statement when
194 'payment_receipt_email' configuration option is set.
196 About the "manual" flag: Normally, if the 'payment_receipt' config option
197 is set, and the customer has an invoice email address, inserting a payment
198 causes a I<statement> to be emailed to the customer. If the payment is
199 considered "manual" (or if the customer has no invoices), then it will
200 instead send a I<payment receipt>. "manual" should be true whenever a
201 payment is created directly from the web interface, from a user-initiated
202 realtime payment, or from a third-party payment via self-service. It should
203 be I<false> when creating a payment from a billing event or from a batch.
208 my($self, %options) = @_;
210 local $SIG{HUP} = 'IGNORE';
211 local $SIG{INT} = 'IGNORE';
212 local $SIG{QUIT} = 'IGNORE';
213 local $SIG{TERM} = 'IGNORE';
214 local $SIG{TSTP} = 'IGNORE';
215 local $SIG{PIPE} = 'IGNORE';
217 my $oldAutoCommit = $FS::UID::AutoCommit;
218 local $FS::UID::AutoCommit = 0;
222 if ( $self->invnum ) {
223 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
225 $dbh->rollback if $oldAutoCommit;
226 return "Unknown cust_bill.invnum: ". $self->invnum;
228 $self->custnum($cust_bill->custnum );
231 my $error = $self->check;
232 return $error if $error;
234 my $cust_main = $self->cust_main;
235 my $old_balance = $cust_main->balance;
237 $error = $self->SUPER::insert;
239 $dbh->rollback if $oldAutoCommit;
240 return "error inserting cust_pay: $error";
243 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
244 if ( my $months = $self->discount_term ) {
245 # XXX this should be moved out somewhere, but discount_term_values
247 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
248 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
250 # %billing_pkgs contains this customer's active monthly packages.
251 # Recurring fees for those packages will be credited and then rebilled
252 # for the full discount term. Other packages on the last invoice
253 # (canceled, non-monthly recurring, or one-time charges) will be
255 my %billing_pkgs = map { $_->pkgnum => $_ }
256 grep { $_->part_pkg->freq eq '1' }
257 $cust_main->billing_pkgs;
258 my $credit = 0; # sum of recurring charges from that invoice
259 my $last_bill_date = 0; # the real bill date
260 foreach my $item ( $cust_bill->cust_bill_pkg ) {
261 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
262 $credit += $item->recur;
263 $last_bill_date = $item->cust_pkg->last_bill
264 if defined($item->cust_pkg)
265 and $item->cust_pkg->last_bill > $last_bill_date
268 my $cust_credit = new FS::cust_credit {
269 'custnum' => $self->custnum,
270 'amount' => sprintf('%.2f', $credit),
271 'reason' => 'customer chose to prepay for discount',
273 $error = $cust_credit->insert('reason_type' => $credit_type);
275 $dbh->rollback if $oldAutoCommit;
276 return "error inserting prepayment credit: $error";
280 # bill for the entire term
281 $_->bill($_->last_bill) foreach (values %billing_pkgs);
282 $error = $cust_main->bill(
283 # no recurring_only, we want unbilled packages with start dates to
285 'no_usage_reset' => 1,
286 'time' => $last_bill_date, # not $cust_bill->_date
287 'pkg_list' => [ values %billing_pkgs ],
288 'freq_override' => $months,
291 $dbh->rollback if $oldAutoCommit;
292 return "error inserting cust_pay: $error";
294 $error = $cust_main->apply_payments_and_credits;
296 $dbh->rollback if $oldAutoCommit;
297 return "error inserting cust_pay: $error";
299 my $new_balance = $cust_main->balance;
300 if ($new_balance > 0) {
301 $dbh->rollback if $oldAutoCommit;
302 return "balance after prepay discount attempt: $new_balance";
304 # user friendly: override the "apply only to this invoice" mode
311 if ( $self->invnum ) {
312 my $cust_bill_pay = new FS::cust_bill_pay {
313 'invnum' => $self->invnum,
314 'paynum' => $self->paynum,
315 'amount' => $self->paid,
316 '_date' => $self->_date,
318 $error = $cust_bill_pay->insert(%options);
320 if ( $ignore_noapply ) {
321 warn "warning: error inserting cust_bill_pay: $error ".
322 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
324 $dbh->rollback if $oldAutoCommit;
325 return "error inserting cust_bill_pay: $error";
330 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
332 #false laziness w/ cust_credit::insert
333 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
334 my @errors = $cust_main->unsuspend;
336 # side-fx with nested transactions? upstack rolls back?
337 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
343 #bill setup fees for voip_cdr bill_every_call packages
344 #some false laziness w/search in freeside-cdrd
346 'LEFT JOIN part_pkg USING ( pkgpart ) '.
347 "LEFT JOIN part_pkg_option
348 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
349 AND part_pkg_option.optionname = 'bill_every_call' )";
351 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
352 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
354 my @cust_pkg = qsearch({
355 'table' => 'cust_pkg',
356 'addl_from' => $addl_from,
357 'hashref' => { 'custnum' => $self->custnum,
361 'extra_sql' => $extra_sql,
365 warn "voip_cdr bill_every_call packages found; billing customer\n";
366 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
368 warn "WARNING: Error billing customer: $bill_error\n";
371 #end of billing setup fees for voip_cdr bill_every_call packages
373 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
376 my $trigger = $conf->config('payment_receipt-trigger',
377 $self->cust_main->agentnum) || 'cust_pay';
378 if ( $trigger eq 'cust_pay' ) {
379 my $error = $self->send_receipt(
380 'manual' => $options{'manual'},
381 'cust_bill' => $cust_bill,
382 'cust_main' => $cust_main,
384 warn "can't send payment receipt/statement: $error" if $error;
391 =item void [ REASON ]
393 Voids this payment: deletes the payment and all associated applications and
394 adds a record of the voided payment to the FS::cust_pay_void table.
401 local $SIG{HUP} = 'IGNORE';
402 local $SIG{INT} = 'IGNORE';
403 local $SIG{QUIT} = 'IGNORE';
404 local $SIG{TERM} = 'IGNORE';
405 local $SIG{TSTP} = 'IGNORE';
406 local $SIG{PIPE} = 'IGNORE';
408 my $oldAutoCommit = $FS::UID::AutoCommit;
409 local $FS::UID::AutoCommit = 0;
412 my $cust_pay_void = new FS::cust_pay_void ( {
413 map { $_ => $self->get($_) } $self->fields
415 $cust_pay_void->reason(shift) if scalar(@_);
416 my $error = $cust_pay_void->insert;
418 my $cust_pay_pending =
419 qsearchs('cust_pay_pending', { paynum => $self->paynum });
420 if ( $cust_pay_pending ) {
421 $cust_pay_pending->set('void_paynum', $self->paynum);
422 $cust_pay_pending->set('paynum', '');
423 $error ||= $cust_pay_pending->replace;
426 $error ||= $self->delete;
429 $dbh->rollback if $oldAutoCommit;
433 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
441 Unless the closed flag is set, deletes this payment and all associated
442 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
443 cases, you want to use the void method instead to leave a record of the
448 # very similar to FS::cust_credit::delete
451 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
453 local $SIG{HUP} = 'IGNORE';
454 local $SIG{INT} = 'IGNORE';
455 local $SIG{QUIT} = 'IGNORE';
456 local $SIG{TERM} = 'IGNORE';
457 local $SIG{TSTP} = 'IGNORE';
458 local $SIG{PIPE} = 'IGNORE';
460 my $oldAutoCommit = $FS::UID::AutoCommit;
461 local $FS::UID::AutoCommit = 0;
464 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
465 my $error = $app->delete;
467 $dbh->rollback if $oldAutoCommit;
472 my $error = $self->SUPER::delete(@_);
474 $dbh->rollback if $oldAutoCommit;
478 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
484 =item replace [ OLD_RECORD ]
486 You can, but probably shouldn't modify payments...
488 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
489 supplied, replaces this record. If there is an error, returns the error,
490 otherwise returns false.
496 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
497 $self->SUPER::replace(@_);
502 Checks all fields to make sure this is a valid payment. If there is an error,
503 returns the error, otherwise returns false. Called by the insert method.
510 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
513 $self->ut_numbern('paynum')
514 || $self->ut_numbern('custnum')
515 || $self->ut_numbern('_date')
516 || $self->ut_money('paid')
517 || $self->ut_alphan('otaker')
518 || $self->ut_textn('paybatch')
519 || $self->ut_textn('payunique')
520 || $self->ut_enum('closed', [ '', 'Y' ])
521 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
522 || $self->ut_textn('bank')
523 || $self->ut_alphan('depositor')
524 || $self->ut_numbern('account')
525 || $self->ut_numbern('teller')
526 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
527 || $self->payinfo_check()
529 return $error if $error;
531 return "paid must be > 0 " if $self->paid <= 0;
533 return "unknown cust_main.custnum: ". $self->custnum
535 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
537 $self->_date(time) unless $self->_date;
539 return "invalid discount_term"
540 if ($self->discount_term && $self->discount_term < 2);
542 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
543 foreach (qw(bank depositor account teller)) {
544 return "$_ required" if $self->get($_) eq '';
548 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
549 # # UNIQUE index should catch this too, without race conditions, but this
550 # # should give a better error message the other 99.9% of the time...
551 # if ( length($self->payunique)
552 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
553 # #well, it *could* be a better error message
554 # return "duplicate transaction".
555 # " - a payment with unique identifer ". $self->payunique.
562 =item send_receipt HASHREF | OPTION => VALUE ...
564 Sends a payment receipt for this payment..
572 Flag indicating the payment is being made manually.
576 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
581 Customer (FS::cust_main) object (for efficiency).
589 my $opt = ref($_[0]) ? shift : { @_ };
591 my $cust_bill = $opt->{'cust_bill'};
592 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
594 my $conf = new FS::Conf;
596 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
598 my @invoicing_list = $cust_main->invoicing_list_emailonly;
599 return '' unless @invoicing_list;
601 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
605 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
606 #|| ! $conf->exists('invoice_html_statement')
610 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
613 my %substitutions = ();
614 $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
616 my $queue = new FS::queue {
617 'job' => 'FS::Misc::process_send_email',
618 'paynum' => $self->paynum,
619 'custnum' => $cust_main->custnum,
621 $error = $queue->insert(
622 FS::msg_template->by_key($msgnum)->prepare(
623 'cust_main' => $cust_main,
625 'from_config' => 'payment_receipt_from',
626 'substitutions' => \%substitutions,
628 'msgtype' => 'receipt', # override msg_template's default
631 } elsif ( $conf->exists('payment_receipt_email') ) {
633 my $receipt_template = new Text::Template (
635 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
637 warn "can't create payment receipt template: $Text::Template::ERROR";
641 my $payby = $self->payby;
642 my $payinfo = $self->payinfo;
643 $payby =~ s/^BILL$/Check/ if $payinfo;
644 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
645 $payinfo = $self->paymask
647 $payinfo = $self->decrypt($payinfo);
649 $payby =~ s/^CHEK$/Electronic check/;
652 'date' => time2str("%a %B %o, %Y", $self->_date),
653 'name' => $cust_main->name,
654 'paynum' => $self->paynum,
655 'paid' => sprintf("%.2f", $self->paid),
656 'payby' => ucfirst(lc($payby)),
657 'payinfo' => $payinfo,
658 'balance' => $cust_main->balance,
659 'company_name' => $conf->config('company_name', $cust_main->agentnum),
662 $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
664 if ( $opt->{'cust_pkg'} ) {
665 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
666 #setup date, other things?
669 my $queue = new FS::queue {
670 'job' => 'FS::Misc::process_send_generated_email',
671 'paynum' => $self->paynum,
672 'custnum' => $cust_main->custnum,
673 'msgtype' => 'receipt',
675 $error = $queue->insert(
676 'from' => $conf->config('invoice_from', $cust_main->agentnum),
677 #invoice_from??? well as good as any
678 'to' => \@invoicing_list,
679 'subject' => 'Payment receipt',
680 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
685 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
689 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
691 my $queue = new FS::queue {
692 'job' => 'FS::cust_bill::queueable_email',
693 'paynum' => $self->paynum,
694 'custnum' => $cust_main->custnum,
697 $error = $queue->insert(
698 'invnum' => $cust_bill->invnum,
699 'template' => 'statement',
700 'notice_name' => 'Statement',
706 warn "send_receipt: $error\n" if $error;
711 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
718 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
719 sort { $a->_date <=> $b->_date
720 || $a->invnum <=> $b->invnum }
721 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
725 =item cust_pay_refund
727 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
732 sub cust_pay_refund {
734 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
735 sort { $a->_date <=> $b->_date }
736 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
743 Returns the amount of this payment that is still unapplied; which is
744 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
745 applications (see L<FS::cust_pay_refund>).
751 my $amount = $self->paid;
752 $amount -= $_->amount foreach ( $self->cust_bill_pay );
753 $amount -= $_->amount foreach ( $self->cust_pay_refund );
754 sprintf("%.2f", $amount );
759 Returns the amount of this payment that has not been refuned; which is
760 paid minus all refund applications (see L<FS::cust_pay_refund>).
766 my $amount = $self->paid;
767 $amount -= $_->amount foreach ( $self->cust_pay_refund );
768 sprintf("%.2f", $amount );
773 Returns the "paid" field.
788 =item batch_insert CUST_PAY_OBJECT, ...
790 Class method which inserts multiple payments. Takes a list of FS::cust_pay
791 objects. Returns a list, each element representing the status of inserting the
792 corresponding payment - empty. If there is an error inserting any payment, the
793 entire transaction is rolled back, i.e. all payments are inserted or none are.
795 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
796 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
797 those objects will be inserted with the paynum of the payment, and for
798 each one, an error message or an empty string will be inserted into the
803 my @errors = FS::cust_pay->batch_insert(@cust_pay);
804 my $num_errors = scalar(grep $_, @errors);
805 if ( $num_errors == 0 ) {
806 #success; all payments were inserted
808 #failure; no payments were inserted.
814 my $self = shift; #class method
816 local $SIG{HUP} = 'IGNORE';
817 local $SIG{INT} = 'IGNORE';
818 local $SIG{QUIT} = 'IGNORE';
819 local $SIG{TERM} = 'IGNORE';
820 local $SIG{TSTP} = 'IGNORE';
821 local $SIG{PIPE} = 'IGNORE';
823 my $oldAutoCommit = $FS::UID::AutoCommit;
824 local $FS::UID::AutoCommit = 0;
830 foreach my $cust_pay (@_) {
831 my $error = $cust_pay->insert( 'manual' => 1 );
832 push @errors, $error;
833 $num_errors++ if $error;
835 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
837 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
838 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
842 $cust_bill_pay->set('paynum', $cust_pay->paynum);
843 my $apply_error = $cust_bill_pay->insert;
844 push @errors, $apply_error || '';
845 $num_errors++ if $apply_error;
849 } elsif ( !$error ) { #normal case: apply payments as usual
850 $cust_pay->cust_main->apply_payments;
856 $dbh->rollback if $oldAutoCommit;
858 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
867 Returns an SQL fragment to retreive the unapplied amount.
872 my ($class, $start, $end) = @_;
873 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
874 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
875 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
876 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
880 ( SELECT SUM(amount) FROM cust_bill_pay
881 WHERE cust_pay.paynum = cust_bill_pay.paynum
882 $bill_start $bill_end )
886 ( SELECT SUM(amount) FROM cust_pay_refund
887 WHERE cust_pay.paynum = cust_pay_refund.paynum
888 $refund_start $refund_end )
897 # Used by FS::Upgrade to migrate to a new database.
901 sub _upgrade_data { #class method
902 my ($class, %opt) = @_;
904 warn "$me upgrading $class\n" if $DEBUG;
906 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
909 # otaker/ivan upgrade
912 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
914 #not the most efficient, but hey, it only has to run once
916 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
917 " AND usernum IS NULL ".
918 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
919 " WHERE cust_main.custnum = cust_pay.custnum ) ";
921 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
923 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
924 $sth->execute or die $sth->errstr;
925 my $total = $sth->fetchrow_arrayref->[0];
926 #warn "$total cust_pay records to update\n"
928 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
933 my @cust_pay = qsearch( {
934 'table' => 'cust_pay',
936 'extra_sql' => $where,
937 'order_by' => 'ORDER BY paynum',
940 foreach my $cust_pay (@cust_pay) {
942 my $h_cust_pay = $cust_pay->h_search('insert');
944 next if $cust_pay->otaker eq $h_cust_pay->history_user;
945 #$cust_pay->otaker($h_cust_pay->history_user);
946 $cust_pay->set('otaker', $h_cust_pay->history_user);
948 $cust_pay->set('otaker', 'legacy');
951 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
952 my $error = $cust_pay->replace;
955 warn " *** WARNING: Error updating order taker for payment paynum ".
956 $cust_pay->paynun. ": $error\n";
960 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
963 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
964 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
970 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
974 # payinfo N/A upgrade
977 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
979 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
981 my @na_cust_pay = qsearch( {
982 'table' => 'cust_pay',
983 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
984 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
987 foreach my $na ( @na_cust_pay ) {
989 next unless $na->payinfo eq 'N/A';
991 my $cust_pay_pending =
992 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
993 unless ( $cust_pay_pending ) {
994 warn " *** WARNING: not-yet recoverable N/A card for payment ".
995 $na->paynum. " (no cust_pay_pending)\n";
998 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
999 my $error = $na->replace;
1001 warn " *** WARNING: Error updating payinfo for payment paynum ".
1002 $na->paynun. ": $error\n";
1008 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1012 # otaker->usernum upgrade
1015 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1016 $class->_upgrade_otaker(%opt);
1017 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1019 # if we do this anywhere else, it should become an FS::Upgrade method
1020 my $num_to_upgrade = $class->count('paybatch is not null');
1021 my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1022 if ( $num_to_upgrade > 0 ) {
1023 warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1024 if ( $opt{queue} ) {
1025 if ( $num_jobs > 0 ) {
1026 warn "Upgrade already queued.\n";
1028 warn "Scheduling upgrade.\n";
1029 my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1033 process_upgrade_paybatch();
1038 sub process_upgrade_paybatch {
1040 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1041 local $FS::UID::AutoCommit = 1;
1044 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1046 my $search = FS::Cursor->new( {
1047 'table' => 'cust_pay',
1048 'addl_from' => ' JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS text) ',
1050 while (my $cust_pay = $search->fetch) {
1051 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1052 $cust_pay->set('paybatch' => '');
1053 my $error = $cust_pay->replace;
1054 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1059 # migrate gateway info from the misused 'paybatch' field
1062 # not only cust_pay, but also voided and refunded payments
1063 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1064 local $FS::Record::nowarn_classload=1;
1065 # really inefficient, but again, only has to run once
1066 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1067 my $and_batchnum_is_null =
1068 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1069 my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1070 my $search = FS::Cursor->new({
1072 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1073 "AND (paybatch IS NOT NULL ".
1074 "OR (paybatch IS NULL AND auth IS NULL
1075 $and_batchnum_is_null ) )
1076 ORDER BY $pkey DESC"
1078 while ( my $object = $search->fetch ) {
1079 if ( $object->paybatch eq '' ) {
1080 # repair for a previous upgrade that didn't save 'auth'
1081 my $pkey = $object->primary_key;
1082 # find the last history record that had a paybatch value
1084 table => "h_$table",
1086 $pkey => $object->$pkey,
1087 paybatch => { op=>'!=', value=>''},
1088 history_action => 'replace_old',
1090 order_by => 'ORDER BY history_date DESC LIMIT 1',
1093 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1096 # if the paybatch didn't have an auth string, then it's fine
1097 $h->paybatch =~ /:(\w+):/ or next;
1098 # set paybatch to what it was in that record
1099 $object->set('paybatch', $h->paybatch)
1100 # and then upgrade it like the old records
1103 my $parsed = $object->_parse_paybatch;
1104 if (keys %$parsed) {
1105 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1106 $object->set('auth' => $parsed->{authorization});
1107 $object->set('paybatch', '');
1108 my $error = $object->replace;
1109 warn "error parsing CARD/CHEK paybatch fields on $object #".
1110 $object->get($object->primary_key).":\n $error\n"
1115 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1125 =item batch_import HASHREF
1127 Inserts new payments.
1134 my $fh = $param->{filehandle};
1135 my $format = $param->{'format'};
1137 my $agentnum = $param->{agentnum};
1138 my $_date = $param->{_date};
1139 $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1140 my $paybatch = $param->{'paybatch'};
1142 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1143 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1145 # here is the agent virtualization
1146 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1150 if ( $format eq 'simple' ) {
1151 @fields = qw( custnum agent_custid paid payinfo );
1153 } elsif ( $format eq 'extended' ) {
1154 die "unimplemented\n";
1158 die "unknown format $format";
1161 eval "use Text::CSV_XS;";
1164 my $csv = new Text::CSV_XS;
1168 local $SIG{HUP} = 'IGNORE';
1169 local $SIG{INT} = 'IGNORE';
1170 local $SIG{QUIT} = 'IGNORE';
1171 local $SIG{TERM} = 'IGNORE';
1172 local $SIG{TSTP} = 'IGNORE';
1173 local $SIG{PIPE} = 'IGNORE';
1175 my $oldAutoCommit = $FS::UID::AutoCommit;
1176 local $FS::UID::AutoCommit = 0;
1180 while ( defined($line=<$fh>) ) {
1182 $csv->parse($line) or do {
1183 $dbh->rollback if $oldAutoCommit;
1184 return "can't parse: ". $csv->error_input();
1187 my @columns = $csv->fields();
1191 paybatch => $paybatch,
1193 $cust_pay{_date} = $_date if $_date;
1196 foreach my $field ( @fields ) {
1198 if ( $field eq 'agent_custid'
1200 && $columns[0] =~ /\S+/ )
1203 my $agent_custid = $columns[0];
1204 my %hash = ( 'agent_custid' => $agent_custid,
1205 'agentnum' => $agentnum,
1208 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1209 $dbh->rollback if $oldAutoCommit;
1210 return "can't specify custnum with agent_custid $agent_custid";
1213 $cust_main = qsearchs({
1214 'table' => 'cust_main',
1215 'hashref' => \%hash,
1216 'extra_sql' => $extra_sql,
1219 unless ( $cust_main ) {
1220 $dbh->rollback if $oldAutoCommit;
1221 return "can't find customer with agent_custid $agent_custid";
1225 $columns[0] = $cust_main->custnum;
1228 $cust_pay{$field} = shift @columns;
1231 if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1232 && length($1) == $custnum_length ) {
1233 $cust_pay{custnum} = $2;
1236 my $cust_pay = new FS::cust_pay( \%cust_pay );
1237 my $error = $cust_pay->insert;
1240 $dbh->rollback if $oldAutoCommit;
1241 return "can't insert payment for $line: $error";
1244 if ( $format eq 'simple' ) {
1245 # include agentnum for less surprise?
1246 $cust_main = qsearchs({
1247 'table' => 'cust_main',
1248 'hashref' => { 'custnum' => $cust_pay->custnum },
1249 'extra_sql' => $extra_sql,
1253 unless ( $cust_main ) {
1254 $dbh->rollback if $oldAutoCommit;
1255 return "can't find customer to which payments apply at line: $line";
1258 $error = $cust_main->apply_payments_and_credits;
1260 $dbh->rollback if $oldAutoCommit;
1261 return "can't apply payments to customer for $line: $error";
1269 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1271 return "Empty file!" unless $imported;
1281 Delete and replace methods.
1285 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1286 schema.html from the base documentation.