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 );
14 use FS::UID qw( driver_name );
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;
30 $me = '[FS::cust_pay]';
34 #ask FS::UID to run this stuff for us later
35 FS::UID->install_callback( sub {
37 $unsuspendauto = $conf->exists('unsuspendauto');
40 @encrypted_fields = ('payinfo');
41 sub nohistory_fields { ('payinfo'); }
45 FS::cust_pay - Object methods for cust_pay objects
51 $record = new FS::cust_pay \%hash;
52 $record = new FS::cust_pay { 'column' => 'value' };
54 $error = $record->insert;
56 $error = $new_record->replace($old_record);
58 $error = $record->delete;
60 $error = $record->check;
64 An FS::cust_pay object represents a payment; the transfer of money from a
65 customer. FS::cust_pay inherits from FS::Record. The following fields are
72 primary key (assigned automatically for new payments)
76 customer (see L<FS::cust_main>)
80 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
81 L<Time::Local> and L<Date::Parse> for conversion functions.
85 Amount of this payment
89 order taker (see L<FS::access_user>)
93 Payment Type (See L<FS::payinfo_Mixin> for valid values)
97 Payment Information (See L<FS::payinfo_Mixin> for data format)
101 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
105 obsolete text field for tracking card processing or other batch grouping
109 Optional unique identifer to prevent duplicate transactions.
113 books closed flag, empty or `Y'
117 Desired pkgnum when using experimental package balances.
121 The bank where the payment was deposited.
125 The name of the depositor.
129 The deposit account number.
137 The number of the batch this payment came from (see L<FS::pay_batch>),
138 or null if it was processed through a realtime gateway or entered manually.
142 The number of the realtime or batch gateway L<FS::payment_gateway>) this
143 payment was processed through. Null if it was entered manually or processed
144 by the "system default" gateway, which doesn't have a number.
148 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
149 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
150 redundant with C<gatewaynum>.
154 The authorization number returned by the credit card network.
158 The transaction ID returned by the gateway, if any. This is usually what
159 you would use to initiate a void or refund of the payment.
169 Creates a new payment. To add the payment to the databse, see L<"insert">.
173 sub table { 'cust_pay'; }
174 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum; }
175 sub cust_unlinked_msg {
177 "WARNING: can't find cust_main.custnum ". $self->custnum.
178 ' (cust_pay.paynum '. $self->paynum. ')';
181 =item insert [ OPTION => VALUE ... ]
183 Adds this payment to the database.
185 For backwards-compatibility and convenience, if the additional field invnum
186 is defined, an FS::cust_bill_pay record for the full amount of the payment
187 will be created. In this case, custnum is optional.
189 If the additional field discount_term is defined then a prepayment discount
190 is taken for that length of time. It is an error for the customer to owe
191 after this payment is made.
193 A hash of optional arguments may be passed. Currently "manual" is supported.
194 If true, a payment receipt is sent instead of a statement when
195 'payment_receipt_email' configuration option is set.
197 About the "manual" flag: Normally, if the 'payment_receipt' config option
198 is set, and the customer has an invoice email address, inserting a payment
199 causes a I<statement> to be emailed to the customer. If the payment is
200 considered "manual" (or if the customer has no invoices), then it will
201 instead send a I<payment receipt>. "manual" should be true whenever a
202 payment is created directly from the web interface, from a user-initiated
203 realtime payment, or from a third-party payment via self-service. It should
204 be I<false> when creating a payment from a billing event or from a batch.
209 my($self, %options) = @_;
211 local $SIG{HUP} = 'IGNORE';
212 local $SIG{INT} = 'IGNORE';
213 local $SIG{QUIT} = 'IGNORE';
214 local $SIG{TERM} = 'IGNORE';
215 local $SIG{TSTP} = 'IGNORE';
216 local $SIG{PIPE} = 'IGNORE';
218 my $oldAutoCommit = $FS::UID::AutoCommit;
219 local $FS::UID::AutoCommit = 0;
223 if ( $self->invnum ) {
224 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
226 $dbh->rollback if $oldAutoCommit;
227 return "Unknown cust_bill.invnum: ". $self->invnum;
229 $self->custnum($cust_bill->custnum );
232 my $error = $self->check;
233 return $error if $error;
235 my $cust_main = $self->cust_main;
236 my $old_balance = $cust_main->balance;
238 $error = $self->SUPER::insert;
240 $dbh->rollback if $oldAutoCommit;
241 return "error inserting cust_pay: $error";
244 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
245 if ( my $months = $self->discount_term ) {
246 # XXX this should be moved out somewhere, but discount_term_values
248 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
249 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
251 # %billing_pkgs contains this customer's active monthly packages.
252 # Recurring fees for those packages will be credited and then rebilled
253 # for the full discount term. Other packages on the last invoice
254 # (canceled, non-monthly recurring, or one-time charges) will be
256 my %billing_pkgs = map { $_->pkgnum => $_ }
257 grep { $_->part_pkg->freq eq '1' }
258 $cust_main->billing_pkgs;
259 my $credit = 0; # sum of recurring charges from that invoice
260 my $last_bill_date = 0; # the real bill date
261 foreach my $item ( $cust_bill->cust_bill_pkg ) {
262 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
263 $credit += $item->recur;
264 $last_bill_date = $item->cust_pkg->last_bill
265 if defined($item->cust_pkg)
266 and $item->cust_pkg->last_bill > $last_bill_date
269 my $cust_credit = new FS::cust_credit {
270 'custnum' => $self->custnum,
271 'amount' => sprintf('%.2f', $credit),
272 'reason' => 'customer chose to prepay for discount',
274 $error = $cust_credit->insert('reason_type' => $credit_type);
276 $dbh->rollback if $oldAutoCommit;
277 return "error inserting prepayment credit: $error";
281 # bill for the entire term
282 $_->bill($_->last_bill) foreach (values %billing_pkgs);
283 $error = $cust_main->bill(
284 # no recurring_only, we want unbilled packages with start dates to
286 'no_usage_reset' => 1,
287 'time' => $last_bill_date, # not $cust_bill->_date
288 'pkg_list' => [ values %billing_pkgs ],
289 'freq_override' => $months,
292 $dbh->rollback if $oldAutoCommit;
293 return "error inserting cust_pay: $error";
295 $error = $cust_main->apply_payments_and_credits;
297 $dbh->rollback if $oldAutoCommit;
298 return "error inserting cust_pay: $error";
300 my $new_balance = $cust_main->balance;
301 if ($new_balance > 0) {
302 $dbh->rollback if $oldAutoCommit;
303 return "balance after prepay discount attempt: $new_balance";
305 # user friendly: override the "apply only to this invoice" mode
312 if ( $self->invnum ) {
313 my $cust_bill_pay = new FS::cust_bill_pay {
314 'invnum' => $self->invnum,
315 'paynum' => $self->paynum,
316 'amount' => $self->paid,
317 '_date' => $self->_date,
319 $error = $cust_bill_pay->insert(%options);
321 if ( $ignore_noapply ) {
322 warn "warning: error inserting cust_bill_pay: $error ".
323 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
325 $dbh->rollback if $oldAutoCommit;
326 return "error inserting cust_bill_pay: $error";
331 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
333 #false laziness w/ cust_credit::insert
334 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
335 my @errors = $cust_main->unsuspend;
337 # side-fx with nested transactions? upstack rolls back?
338 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
344 #bill setup fees for voip_cdr bill_every_call packages
345 #some false laziness w/search in freeside-cdrd
347 'LEFT JOIN part_pkg USING ( pkgpart ) '.
348 "LEFT JOIN part_pkg_option
349 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
350 AND part_pkg_option.optionname = 'bill_every_call' )";
352 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
353 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
355 my @cust_pkg = qsearch({
356 'table' => 'cust_pkg',
357 'addl_from' => $addl_from,
358 'hashref' => { 'custnum' => $self->custnum,
362 'extra_sql' => $extra_sql,
366 warn "voip_cdr bill_every_call packages found; billing customer\n";
367 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
369 warn "WARNING: Error billing customer: $bill_error\n";
372 #end of billing setup fees for voip_cdr bill_every_call packages
374 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
377 my $trigger = $conf->config('payment_receipt-trigger',
378 $self->cust_main->agentnum) || 'cust_pay';
379 if ( $trigger eq 'cust_pay' ) {
380 my $error = $self->send_receipt(
381 'manual' => $options{'manual'},
382 'cust_bill' => $cust_bill,
383 'cust_main' => $cust_main,
385 warn "can't send payment receipt/statement: $error" if $error;
392 =item void [ REASON ]
394 Voids this payment: deletes the payment and all associated applications and
395 adds a record of the voided payment to the FS::cust_pay_void table.
402 local $SIG{HUP} = 'IGNORE';
403 local $SIG{INT} = 'IGNORE';
404 local $SIG{QUIT} = 'IGNORE';
405 local $SIG{TERM} = 'IGNORE';
406 local $SIG{TSTP} = 'IGNORE';
407 local $SIG{PIPE} = 'IGNORE';
409 my $oldAutoCommit = $FS::UID::AutoCommit;
410 local $FS::UID::AutoCommit = 0;
413 my $cust_pay_void = new FS::cust_pay_void ( {
414 map { $_ => $self->get($_) } $self->fields
416 $cust_pay_void->reason(shift) if scalar(@_);
417 my $error = $cust_pay_void->insert;
419 my $cust_pay_pending =
420 qsearchs('cust_pay_pending', { paynum => $self->paynum });
421 if ( $cust_pay_pending ) {
422 $cust_pay_pending->set('void_paynum', $self->paynum);
423 $cust_pay_pending->set('paynum', '');
424 $error ||= $cust_pay_pending->replace;
427 $error ||= $self->delete;
430 $dbh->rollback if $oldAutoCommit;
434 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
442 Unless the closed flag is set, deletes this payment and all associated
443 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
444 cases, you want to use the void method instead to leave a record of the
449 # very similar to FS::cust_credit::delete
452 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
454 local $SIG{HUP} = 'IGNORE';
455 local $SIG{INT} = 'IGNORE';
456 local $SIG{QUIT} = 'IGNORE';
457 local $SIG{TERM} = 'IGNORE';
458 local $SIG{TSTP} = 'IGNORE';
459 local $SIG{PIPE} = 'IGNORE';
461 my $oldAutoCommit = $FS::UID::AutoCommit;
462 local $FS::UID::AutoCommit = 0;
465 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
466 my $error = $app->delete;
468 $dbh->rollback if $oldAutoCommit;
473 my $error = $self->SUPER::delete(@_);
475 $dbh->rollback if $oldAutoCommit;
479 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
485 =item replace [ OLD_RECORD ]
487 You can, but probably shouldn't modify payments...
489 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
490 supplied, replaces this record. If there is an error, returns the error,
491 otherwise returns false.
497 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
498 $self->SUPER::replace(@_);
503 Checks all fields to make sure this is a valid payment. If there is an error,
504 returns the error, otherwise returns false. Called by the insert method.
511 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
514 $self->ut_numbern('paynum')
515 || $self->ut_numbern('custnum')
516 || $self->ut_numbern('_date')
517 || $self->ut_money('paid')
518 || $self->ut_alphan('otaker')
519 || $self->ut_textn('paybatch')
520 || $self->ut_textn('payunique')
521 || $self->ut_enum('closed', [ '', 'Y' ])
522 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
523 || $self->ut_textn('bank')
524 || $self->ut_alphan('depositor')
525 || $self->ut_numbern('account')
526 || $self->ut_numbern('teller')
527 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
528 || $self->payinfo_check()
530 return $error if $error;
532 return "paid must be > 0 " if $self->paid <= 0;
534 return "unknown cust_main.custnum: ". $self->custnum
536 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
538 $self->_date(time) unless $self->_date;
540 return "invalid discount_term"
541 if ($self->discount_term && $self->discount_term < 2);
543 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
544 foreach (qw(bank depositor account teller)) {
545 return "$_ required" if $self->get($_) eq '';
549 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
550 # # UNIQUE index should catch this too, without race conditions, but this
551 # # should give a better error message the other 99.9% of the time...
552 # if ( length($self->payunique)
553 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
554 # #well, it *could* be a better error message
555 # return "duplicate transaction".
556 # " - a payment with unique identifer ". $self->payunique.
563 =item send_receipt HASHREF | OPTION => VALUE ...
565 Sends a payment receipt for this payment..
573 Flag indicating the payment is being made manually.
577 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
582 Customer (FS::cust_main) object (for efficiency).
590 my $opt = ref($_[0]) ? shift : { @_ };
592 my $cust_bill = $opt->{'cust_bill'};
593 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
595 my $conf = new FS::Conf;
597 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
599 my @invoicing_list = $cust_main->invoicing_list_emailonly;
600 return '' unless @invoicing_list;
602 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
606 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
607 #|| ! $conf->exists('invoice_html_statement')
611 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
614 my %substitutions = ();
615 $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
617 my $queue = new FS::queue {
618 'job' => 'FS::Misc::process_send_email',
619 'paynum' => $self->paynum,
620 'custnum' => $cust_main->custnum,
622 $error = $queue->insert(
623 FS::msg_template->by_key($msgnum)->prepare(
624 'cust_main' => $cust_main,
626 'from_config' => 'payment_receipt_from',
627 'substitutions' => \%substitutions,
629 'msgtype' => 'receipt', # override msg_template's default
632 } elsif ( $conf->exists('payment_receipt_email') ) {
634 my $receipt_template = new Text::Template (
636 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
638 warn "can't create payment receipt template: $Text::Template::ERROR";
642 my $payby = $self->payby;
643 my $payinfo = $self->payinfo;
644 $payby =~ s/^BILL$/Check/ if $payinfo;
645 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
646 $payinfo = $self->paymask
648 $payinfo = $self->decrypt($payinfo);
650 $payby =~ s/^CHEK$/Electronic check/;
653 'date' => time2str("%a %B %o, %Y", $self->_date),
654 'name' => $cust_main->name,
655 'paynum' => $self->paynum,
656 'paid' => sprintf("%.2f", $self->paid),
657 'payby' => ucfirst(lc($payby)),
658 'payinfo' => $payinfo,
659 'balance' => $cust_main->balance,
660 'company_name' => $conf->config('company_name', $cust_main->agentnum),
663 $fill_in{'invnum'} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
665 if ( $opt->{'cust_pkg'} ) {
666 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
667 #setup date, other things?
670 my $queue = new FS::queue {
671 'job' => 'FS::Misc::process_send_generated_email',
672 'paynum' => $self->paynum,
673 'custnum' => $cust_main->custnum,
674 'msgtype' => 'receipt',
676 $error = $queue->insert(
677 'from' => $conf->config('invoice_from', $cust_main->agentnum),
678 #invoice_from??? well as good as any
679 'to' => \@invoicing_list,
680 'subject' => 'Payment receipt',
681 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
686 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
690 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
692 my $queue = new FS::queue {
693 'job' => 'FS::cust_bill::queueable_email',
694 'paynum' => $self->paynum,
695 'custnum' => $cust_main->custnum,
698 $error = $queue->insert(
699 'invnum' => $cust_bill->invnum,
700 'template' => 'statement',
701 'notice_name' => 'Statement',
707 warn "send_receipt: $error\n" if $error;
712 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
719 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
720 sort { $a->_date <=> $b->_date
721 || $a->invnum <=> $b->invnum }
722 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
726 =item cust_pay_refund
728 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
733 sub cust_pay_refund {
735 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
736 sort { $a->_date <=> $b->_date }
737 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
744 Returns the amount of this payment that is still unapplied; which is
745 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
746 applications (see L<FS::cust_pay_refund>).
752 my $amount = $self->paid;
753 $amount -= $_->amount foreach ( $self->cust_bill_pay );
754 $amount -= $_->amount foreach ( $self->cust_pay_refund );
755 sprintf("%.2f", $amount );
760 Returns the amount of this payment that has not been refuned; which is
761 paid minus all refund applications (see L<FS::cust_pay_refund>).
767 my $amount = $self->paid;
768 $amount -= $_->amount foreach ( $self->cust_pay_refund );
769 sprintf("%.2f", $amount );
774 Returns the "paid" field.
789 =item batch_insert CUST_PAY_OBJECT, ...
791 Class method which inserts multiple payments. Takes a list of FS::cust_pay
792 objects. Returns a list, each element representing the status of inserting the
793 corresponding payment - empty. If there is an error inserting any payment, the
794 entire transaction is rolled back, i.e. all payments are inserted or none are.
796 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
797 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
798 those objects will be inserted with the paynum of the payment, and for
799 each one, an error message or an empty string will be inserted into the
804 my @errors = FS::cust_pay->batch_insert(@cust_pay);
805 my $num_errors = scalar(grep $_, @errors);
806 if ( $num_errors == 0 ) {
807 #success; all payments were inserted
809 #failure; no payments were inserted.
815 my $self = shift; #class method
817 local $SIG{HUP} = 'IGNORE';
818 local $SIG{INT} = 'IGNORE';
819 local $SIG{QUIT} = 'IGNORE';
820 local $SIG{TERM} = 'IGNORE';
821 local $SIG{TSTP} = 'IGNORE';
822 local $SIG{PIPE} = 'IGNORE';
824 my $oldAutoCommit = $FS::UID::AutoCommit;
825 local $FS::UID::AutoCommit = 0;
831 foreach my $cust_pay (@_) {
832 my $error = $cust_pay->insert( 'manual' => 1 );
833 push @errors, $error;
834 $num_errors++ if $error;
836 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
838 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
839 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
843 $cust_bill_pay->set('paynum', $cust_pay->paynum);
844 my $apply_error = $cust_bill_pay->insert;
845 push @errors, $apply_error || '';
846 $num_errors++ if $apply_error;
850 } elsif ( !$error ) { #normal case: apply payments as usual
851 $cust_pay->cust_main->apply_payments;
857 $dbh->rollback if $oldAutoCommit;
859 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
868 Returns an SQL fragment to retreive the unapplied amount.
873 my ($class, $start, $end) = @_;
874 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
875 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
876 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
877 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
881 ( SELECT SUM(amount) FROM cust_bill_pay
882 WHERE cust_pay.paynum = cust_bill_pay.paynum
883 $bill_start $bill_end )
887 ( SELECT SUM(amount) FROM cust_pay_refund
888 WHERE cust_pay.paynum = cust_pay_refund.paynum
889 $refund_start $refund_end )
898 my @fields = grep { $_ ne 'payinfo' } $self->fields;
899 +{ ( map { $_=>$self->$_ } @fields ),
905 # Used by FS::Upgrade to migrate to a new database.
909 sub _upgrade_data { #class method
910 my ($class, %opt) = @_;
912 warn "$me upgrading $class\n" if $DEBUG;
914 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
917 # otaker/ivan upgrade
920 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
922 #not the most efficient, but hey, it only has to run once
924 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
925 " AND usernum IS NULL ".
926 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
927 " WHERE cust_main.custnum = cust_pay.custnum ) ";
929 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
931 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
932 $sth->execute or die $sth->errstr;
933 my $total = $sth->fetchrow_arrayref->[0];
934 #warn "$total cust_pay records to update\n"
936 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
941 my @cust_pay = qsearch( {
942 'table' => 'cust_pay',
944 'extra_sql' => $where,
945 'order_by' => 'ORDER BY paynum',
948 foreach my $cust_pay (@cust_pay) {
950 my $h_cust_pay = $cust_pay->h_search('insert');
952 next if $cust_pay->otaker eq $h_cust_pay->history_user;
953 #$cust_pay->otaker($h_cust_pay->history_user);
954 $cust_pay->set('otaker', $h_cust_pay->history_user);
956 $cust_pay->set('otaker', 'legacy');
959 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
960 my $error = $cust_pay->replace;
963 warn " *** WARNING: Error updating order taker for payment paynum ".
964 $cust_pay->paynun. ": $error\n";
968 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
971 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
972 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
978 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
982 # payinfo N/A upgrade
985 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
987 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
989 my @na_cust_pay = qsearch( {
990 'table' => 'cust_pay',
991 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
992 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
995 foreach my $na ( @na_cust_pay ) {
997 next unless $na->payinfo eq 'N/A';
999 my $cust_pay_pending =
1000 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1001 unless ( $cust_pay_pending ) {
1002 warn " *** WARNING: not-yet recoverable N/A card for payment ".
1003 $na->paynum. " (no cust_pay_pending)\n";
1006 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1007 my $error = $na->replace;
1009 warn " *** WARNING: Error updating payinfo for payment paynum ".
1010 $na->paynun. ": $error\n";
1016 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1020 # otaker->usernum upgrade
1023 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1024 $class->_upgrade_otaker(%opt);
1025 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1027 # if we do this anywhere else, it should become an FS::Upgrade method
1028 my $num_to_upgrade = $class->count('paybatch is not null');
1029 my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1030 if ( $num_to_upgrade > 0 ) {
1031 warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1032 if ( $opt{queue} ) {
1033 if ( $num_jobs > 0 ) {
1034 warn "Upgrade already queued.\n";
1036 warn "Scheduling upgrade.\n";
1037 my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1041 process_upgrade_paybatch();
1046 sub process_upgrade_paybatch {
1048 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1049 local $FS::UID::AutoCommit = 1;
1052 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1054 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1055 my $search = FS::Cursor->new( {
1056 'table' => 'cust_pay',
1057 'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1059 while (my $cust_pay = $search->fetch) {
1060 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1061 $cust_pay->set('paybatch' => '');
1062 my $error = $cust_pay->replace;
1063 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1068 # migrate gateway info from the misused 'paybatch' field
1071 # not only cust_pay, but also voided and refunded payments
1072 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1073 local $FS::Record::nowarn_classload=1;
1074 # really inefficient, but again, only has to run once
1075 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1076 my $and_batchnum_is_null =
1077 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1078 my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1079 my $search = FS::Cursor->new({
1081 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1082 "AND (paybatch IS NOT NULL ".
1083 "OR (paybatch IS NULL AND auth IS NULL
1084 $and_batchnum_is_null ) )
1085 ORDER BY $pkey DESC"
1087 while ( my $object = $search->fetch ) {
1088 if ( $object->paybatch eq '' ) {
1089 # repair for a previous upgrade that didn't save 'auth'
1090 my $pkey = $object->primary_key;
1091 # find the last history record that had a paybatch value
1093 table => "h_$table",
1095 $pkey => $object->$pkey,
1096 paybatch => { op=>'!=', value=>''},
1097 history_action => 'replace_old',
1099 order_by => 'ORDER BY history_date DESC LIMIT 1',
1102 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1105 # if the paybatch didn't have an auth string, then it's fine
1106 $h->paybatch =~ /:(\w+):/ or next;
1107 # set paybatch to what it was in that record
1108 $object->set('paybatch', $h->paybatch)
1109 # and then upgrade it like the old records
1112 my $parsed = $object->_parse_paybatch;
1113 if (keys %$parsed) {
1114 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1115 $object->set('auth' => $parsed->{authorization});
1116 $object->set('paybatch', '');
1117 my $error = $object->replace;
1118 warn "error parsing CARD/CHEK paybatch fields on $object #".
1119 $object->get($object->primary_key).":\n $error\n"
1124 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1134 =item batch_import HASHREF
1136 Inserts new payments.
1143 my $fh = $param->{filehandle};
1144 my $format = $param->{'format'};
1146 my $agentnum = $param->{agentnum};
1147 my $_date = $param->{_date};
1148 $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1149 my $paybatch = $param->{'paybatch'};
1151 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1152 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1154 # here is the agent virtualization
1155 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1159 if ( $format eq 'simple' ) {
1160 @fields = qw( custnum agent_custid paid payinfo );
1162 } elsif ( $format eq 'extended' ) {
1163 die "unimplemented\n";
1167 die "unknown format $format";
1170 eval "use Text::CSV_XS;";
1173 my $csv = new Text::CSV_XS;
1177 local $SIG{HUP} = 'IGNORE';
1178 local $SIG{INT} = 'IGNORE';
1179 local $SIG{QUIT} = 'IGNORE';
1180 local $SIG{TERM} = 'IGNORE';
1181 local $SIG{TSTP} = 'IGNORE';
1182 local $SIG{PIPE} = 'IGNORE';
1184 my $oldAutoCommit = $FS::UID::AutoCommit;
1185 local $FS::UID::AutoCommit = 0;
1189 while ( defined($line=<$fh>) ) {
1191 $csv->parse($line) or do {
1192 $dbh->rollback if $oldAutoCommit;
1193 return "can't parse: ". $csv->error_input();
1196 my @columns = $csv->fields();
1200 paybatch => $paybatch,
1202 $cust_pay{_date} = $_date if $_date;
1205 foreach my $field ( @fields ) {
1207 if ( $field eq 'agent_custid'
1209 && $columns[0] =~ /\S+/ )
1212 my $agent_custid = $columns[0];
1213 my %hash = ( 'agent_custid' => $agent_custid,
1214 'agentnum' => $agentnum,
1217 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1218 $dbh->rollback if $oldAutoCommit;
1219 return "can't specify custnum with agent_custid $agent_custid";
1222 $cust_main = qsearchs({
1223 'table' => 'cust_main',
1224 'hashref' => \%hash,
1225 'extra_sql' => $extra_sql,
1228 unless ( $cust_main ) {
1229 $dbh->rollback if $oldAutoCommit;
1230 return "can't find customer with agent_custid $agent_custid";
1234 $columns[0] = $cust_main->custnum;
1237 $cust_pay{$field} = shift @columns;
1240 if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1241 && length($1) == $custnum_length ) {
1242 $cust_pay{custnum} = $2;
1245 my $cust_pay = new FS::cust_pay( \%cust_pay );
1246 my $error = $cust_pay->insert;
1249 $dbh->rollback if $oldAutoCommit;
1250 return "can't insert payment for $line: $error";
1253 if ( $format eq 'simple' ) {
1254 # include agentnum for less surprise?
1255 $cust_main = qsearchs({
1256 'table' => 'cust_main',
1257 'hashref' => { 'custnum' => $cust_pay->custnum },
1258 'extra_sql' => $extra_sql,
1262 unless ( $cust_main ) {
1263 $dbh->rollback if $oldAutoCommit;
1264 return "can't find customer to which payments apply at line: $line";
1267 $error = $cust_main->apply_payments_and_credits;
1269 $dbh->rollback if $oldAutoCommit;
1270 return "can't apply payments to customer for $line: $error";
1278 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1280 return "Empty file!" unless $imported;
1290 Delete and replace methods.
1294 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1295 schema.html from the base documentation.