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 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.
141 Creates a new payment. To add the payment to the databse, see L<"insert">.
145 sub table { 'cust_pay'; }
146 sub cust_linked { $_[0]->cust_main_custnum; }
147 sub cust_unlinked_msg {
149 "WARNING: can't find cust_main.custnum ". $self->custnum.
150 ' (cust_pay.paynum '. $self->paynum. ')';
153 =item insert [ OPTION => VALUE ... ]
155 Adds this payment to the database.
157 For backwards-compatibility and convenience, if the additional field invnum
158 is defined, an FS::cust_bill_pay record for the full amount of the payment
159 will be created. In this case, custnum is optional.
161 If the additional field discount_term is defined then a prepayment discount
162 is taken for that length of time. It is an error for the customer to owe
163 after this payment is made.
165 A hash of optional arguments may be passed. Currently "manual" is supported.
166 If true, a payment receipt is sent instead of a statement when
167 'payment_receipt_email' configuration option is set.
172 my($self, %options) = @_;
174 local $SIG{HUP} = 'IGNORE';
175 local $SIG{INT} = 'IGNORE';
176 local $SIG{QUIT} = 'IGNORE';
177 local $SIG{TERM} = 'IGNORE';
178 local $SIG{TSTP} = 'IGNORE';
179 local $SIG{PIPE} = 'IGNORE';
181 my $oldAutoCommit = $FS::UID::AutoCommit;
182 local $FS::UID::AutoCommit = 0;
186 if ( $self->invnum ) {
187 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
189 $dbh->rollback if $oldAutoCommit;
190 return "Unknown cust_bill.invnum: ". $self->invnum;
192 $self->custnum($cust_bill->custnum );
195 my $error = $self->check;
196 return $error if $error;
198 my $cust_main = $self->cust_main;
199 my $old_balance = $cust_main->balance;
201 $error = $self->SUPER::insert;
203 $dbh->rollback if $oldAutoCommit;
204 return "error inserting cust_pay: $error";
207 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
208 if ( my $months = $self->discount_term ) {
209 # XXX this should be moved out somewhere, but discount_term_values
211 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
212 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
214 # %billing_pkgs contains this customer's active monthly packages.
215 # Recurring fees for those packages will be credited and then rebilled
216 # for the full discount term. Other packages on the last invoice
217 # (canceled, non-monthly recurring, or one-time charges) will be
219 my %billing_pkgs = map { $_->pkgnum => $_ }
220 grep { $_->part_pkg->freq eq '1' }
221 $cust_main->billing_pkgs;
222 my $credit = 0; # sum of recurring charges from that invoice
223 my $last_bill_date = 0; # the real bill date
224 foreach my $item ( $cust_bill->cust_bill_pkg ) {
225 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
226 $credit += $item->recur;
227 $last_bill_date = $item->cust_pkg->last_bill
228 if defined($item->cust_pkg)
229 and $item->cust_pkg->last_bill > $last_bill_date
232 my $cust_credit = new FS::cust_credit {
233 'custnum' => $self->custnum,
234 'amount' => sprintf('%.2f', $credit),
235 'reason' => 'customer chose to prepay for discount',
237 $error = $cust_credit->insert('reason_type' => $credit_type);
239 $dbh->rollback if $oldAutoCommit;
240 return "error inserting prepayment credit: $error";
244 # bill for the entire term
245 $_->bill($_->last_bill) foreach (values %billing_pkgs);
246 $error = $cust_main->bill(
247 # no recurring_only, we want unbilled packages with start dates to
249 'no_usage_reset' => 1,
250 'time' => $last_bill_date, # not $cust_bill->_date
251 'pkg_list' => [ values %billing_pkgs ],
252 'freq_override' => $months,
255 $dbh->rollback if $oldAutoCommit;
256 return "error inserting cust_pay: $error";
258 $error = $cust_main->apply_payments_and_credits;
260 $dbh->rollback if $oldAutoCommit;
261 return "error inserting cust_pay: $error";
263 my $new_balance = $cust_main->balance;
264 if ($new_balance > 0) {
265 $dbh->rollback if $oldAutoCommit;
266 return "balance after prepay discount attempt: $new_balance";
268 # user friendly: override the "apply only to this invoice" mode
275 if ( $self->invnum ) {
276 my $cust_bill_pay = new FS::cust_bill_pay {
277 'invnum' => $self->invnum,
278 'paynum' => $self->paynum,
279 'amount' => $self->paid,
280 '_date' => $self->_date,
282 $error = $cust_bill_pay->insert(%options);
284 if ( $ignore_noapply ) {
285 warn "warning: error inserting cust_bill_pay: $error ".
286 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
288 $dbh->rollback if $oldAutoCommit;
289 return "error inserting cust_bill_pay: $error";
294 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
296 #false laziness w/ cust_credit::insert
297 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
298 my @errors = $cust_main->unsuspend;
300 # side-fx with nested transactions? upstack rolls back?
301 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
307 #bill setup fees for voip_cdr bill_every_call packages
308 #some false laziness w/search in freeside-cdrd
310 'LEFT JOIN part_pkg USING ( pkgpart ) '.
311 "LEFT JOIN part_pkg_option
312 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
313 AND part_pkg_option.optionname = 'bill_every_call' )";
315 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
316 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
318 my @cust_pkg = qsearch({
319 'table' => 'cust_pkg',
320 'addl_from' => $addl_from,
321 'hashref' => { 'custnum' => $self->custnum,
325 'extra_sql' => $extra_sql,
329 warn "voip_cdr bill_every_call packages found; billing customer\n";
330 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
332 warn "WARNING: Error billing customer: $bill_error\n";
335 #end of billing setup fees for voip_cdr bill_every_call packages
337 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
340 my $trigger = $conf->config('payment_receipt-trigger',
341 $self->cust_main->agentnum) || 'cust_pay';
342 if ( $trigger eq 'cust_pay' ) {
343 my $error = $self->send_receipt(
344 'manual' => $options{'manual'},
345 'cust_bill' => $cust_bill,
346 'cust_main' => $cust_main,
348 warn "can't send payment receipt/statement: $error" if $error;
355 =item void [ REASON ]
357 Voids this payment: deletes the payment and all associated applications and
358 adds a record of the voided payment to the FS::cust_pay_void table.
365 local $SIG{HUP} = 'IGNORE';
366 local $SIG{INT} = 'IGNORE';
367 local $SIG{QUIT} = 'IGNORE';
368 local $SIG{TERM} = 'IGNORE';
369 local $SIG{TSTP} = 'IGNORE';
370 local $SIG{PIPE} = 'IGNORE';
372 my $oldAutoCommit = $FS::UID::AutoCommit;
373 local $FS::UID::AutoCommit = 0;
376 my $cust_pay_void = new FS::cust_pay_void ( {
377 map { $_ => $self->get($_) } $self->fields
379 $cust_pay_void->reason(shift) if scalar(@_);
380 my $error = $cust_pay_void->insert;
382 $dbh->rollback if $oldAutoCommit;
386 $error = $self->delete;
388 $dbh->rollback if $oldAutoCommit;
392 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
400 Unless the closed flag is set, deletes this payment and all associated
401 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
402 cases, you want to use the void method instead to leave a record of the
407 # very similar to FS::cust_credit::delete
410 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
412 local $SIG{HUP} = 'IGNORE';
413 local $SIG{INT} = 'IGNORE';
414 local $SIG{QUIT} = 'IGNORE';
415 local $SIG{TERM} = 'IGNORE';
416 local $SIG{TSTP} = 'IGNORE';
417 local $SIG{PIPE} = 'IGNORE';
419 my $oldAutoCommit = $FS::UID::AutoCommit;
420 local $FS::UID::AutoCommit = 0;
423 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
424 my $error = $app->delete;
426 $dbh->rollback if $oldAutoCommit;
431 my $error = $self->SUPER::delete(@_);
433 $dbh->rollback if $oldAutoCommit;
437 if ( $conf->exists('deletepayments')
438 && $conf->config('deletepayments') ne '' ) {
440 my $cust_main = $self->cust_main;
442 my $error = send_email(
443 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
444 #invoice_from??? well as good as any
445 'to' => $conf->config('deletepayments'),
446 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
448 "This is an automatic message from your Freeside installation\n",
449 "informing you that the following payment has been deleted:\n",
451 'paynum: '. $self->paynum. "\n",
452 'custnum: '. $self->custnum.
453 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
454 'paid: $'. sprintf("%.2f", $self->paid). "\n",
455 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
456 'payby: '. $self->payby. "\n",
457 'payinfo: '. $self->paymask. "\n",
458 'paybatch: '. $self->paybatch. "\n",
463 $dbh->rollback if $oldAutoCommit;
464 return "can't send payment deletion notification: $error";
469 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
475 =item replace [ OLD_RECORD ]
477 You can, but probably shouldn't modify payments...
479 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
480 supplied, replaces this record. If there is an error, returns the error,
481 otherwise returns false.
487 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
488 $self->SUPER::replace(@_);
493 Checks all fields to make sure this is a valid payment. If there is an error,
494 returns the error, otherwise returns false. Called by the insert method.
501 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
504 $self->ut_numbern('paynum')
505 || $self->ut_numbern('custnum')
506 || $self->ut_numbern('_date')
507 || $self->ut_money('paid')
508 || $self->ut_alphan('otaker')
509 || $self->ut_textn('paybatch')
510 || $self->ut_textn('payunique')
511 || $self->ut_enum('closed', [ '', 'Y' ])
512 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
513 || $self->ut_textn('bank')
514 || $self->ut_alphan('depositor')
515 || $self->ut_numbern('account')
516 || $self->ut_numbern('teller')
517 || $self->payinfo_check()
519 return $error if $error;
521 return "paid must be > 0 " if $self->paid <= 0;
523 return "unknown cust_main.custnum: ". $self->custnum
525 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
527 $self->_date(time) unless $self->_date;
529 return "invalid discount_term"
530 if ($self->discount_term && $self->discount_term < 2);
532 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
533 foreach (qw(bank depositor account teller)) {
534 return "$_ required" if $self->get($_) eq '';
538 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
539 # # UNIQUE index should catch this too, without race conditions, but this
540 # # should give a better error message the other 99.9% of the time...
541 # if ( length($self->payunique)
542 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
543 # #well, it *could* be a better error message
544 # return "duplicate transaction".
545 # " - a payment with unique identifer ". $self->payunique.
552 =item send_receipt HASHREF | OPTION => VALUE ...
554 Sends a payment receipt for this payment..
562 Flag indicating the payment is being made manually.
566 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
571 Customer (FS::cust_main) object (for efficiency).
579 my $opt = ref($_[0]) ? shift : { @_ };
581 my $cust_bill = $opt->{'cust_bill'};
582 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
584 my $conf = new FS::Conf;
586 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
588 my @invoicing_list = $cust_main->invoicing_list_emailonly;
589 return '' unless @invoicing_list;
591 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
595 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
596 #|| ! $conf->exists('invoice_html_statement')
600 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
602 my $msg_template = FS::msg_template->by_key($msgnum);
603 $error = $msg_template->send(
604 'cust_main' => $cust_main,
606 'from_config' => 'payment_receipt_from',
609 } elsif ( $conf->exists('payment_receipt_email') ) {
611 my $receipt_template = new Text::Template (
613 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
615 warn "can't create payment receipt template: $Text::Template::ERROR";
619 my $payby = $self->payby;
620 my $payinfo = $self->payinfo;
621 $payby =~ s/^BILL$/Check/ if $payinfo;
622 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
623 $payinfo = $self->paymask
625 $payinfo = $self->decrypt($payinfo);
627 $payby =~ s/^CHEK$/Electronic check/;
630 'date' => time2str("%a %B %o, %Y", $self->_date),
631 'name' => $cust_main->name,
632 'paynum' => $self->paynum,
633 'paid' => sprintf("%.2f", $self->paid),
634 'payby' => ucfirst(lc($payby)),
635 'payinfo' => $payinfo,
636 'balance' => $cust_main->balance,
637 'company_name' => $conf->config('company_name', $cust_main->agentnum),
640 if ( $opt->{'cust_pkg'} ) {
641 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
642 #setup date, other things?
646 'from' => $conf->config('invoice_from', $cust_main->agentnum),
647 #invoice_from??? well as good as any
648 'to' => \@invoicing_list,
649 'subject' => 'Payment receipt',
650 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
655 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
661 my $queue = new FS::queue {
662 'paynum' => $self->paynum,
663 'job' => 'FS::cust_bill::queueable_email',
666 $error = $queue->insert(
667 'invnum' => $cust_bill->invnum,
668 'template' => 'statement',
669 'notice_name' => 'Statement',
675 warn "send_receipt: $error\n" if $error;
680 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
687 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
688 sort { $a->_date <=> $b->_date
689 || $a->invnum <=> $b->invnum }
690 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
694 =item cust_pay_refund
696 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
701 sub cust_pay_refund {
703 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
704 sort { $a->_date <=> $b->_date }
705 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
712 Returns the amount of this payment that is still unapplied; which is
713 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
714 applications (see L<FS::cust_pay_refund>).
720 my $amount = $self->paid;
721 $amount -= $_->amount foreach ( $self->cust_bill_pay );
722 $amount -= $_->amount foreach ( $self->cust_pay_refund );
723 sprintf("%.2f", $amount );
728 Returns the amount of this payment that has not been refuned; which is
729 paid minus all refund applications (see L<FS::cust_pay_refund>).
735 my $amount = $self->paid;
736 $amount -= $_->amount foreach ( $self->cust_pay_refund );
737 sprintf("%.2f", $amount );
742 Returns the "paid" field.
757 =item batch_insert CUST_PAY_OBJECT, ...
759 Class method which inserts multiple payments. Takes a list of FS::cust_pay
760 objects. Returns a list, each element representing the status of inserting the
761 corresponding payment - empty. If there is an error inserting any payment, the
762 entire transaction is rolled back, i.e. all payments are inserted or none are.
764 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
765 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
766 those objects will be inserted with the paynum of the payment, and for
767 each one, an error message or an empty string will be inserted into the
772 my @errors = FS::cust_pay->batch_insert(@cust_pay);
773 my $num_errors = scalar(grep $_, @errors);
774 if ( $num_errors == 0 ) {
775 #success; all payments were inserted
777 #failure; no payments were inserted.
783 my $self = shift; #class method
785 local $SIG{HUP} = 'IGNORE';
786 local $SIG{INT} = 'IGNORE';
787 local $SIG{QUIT} = 'IGNORE';
788 local $SIG{TERM} = 'IGNORE';
789 local $SIG{TSTP} = 'IGNORE';
790 local $SIG{PIPE} = 'IGNORE';
792 my $oldAutoCommit = $FS::UID::AutoCommit;
793 local $FS::UID::AutoCommit = 0;
799 foreach my $cust_pay (@_) {
800 my $error = $cust_pay->insert( 'manual' => 1 );
801 push @errors, $error;
802 $num_errors++ if $error;
804 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
806 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
807 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
811 $cust_bill_pay->set('paynum', $cust_pay->paynum);
812 my $apply_error = $cust_bill_pay->insert;
813 push @errors, $apply_error || '';
814 $num_errors++ if $apply_error;
818 } elsif ( !$error ) { #normal case: apply payments as usual
819 $cust_pay->cust_main->apply_payments;
825 $dbh->rollback if $oldAutoCommit;
827 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
836 Returns an SQL fragment to retreive the unapplied amount.
841 my ($class, $start, $end) = @_;
842 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
843 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
844 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
845 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
849 ( SELECT SUM(amount) FROM cust_bill_pay
850 WHERE cust_pay.paynum = cust_bill_pay.paynum
851 $bill_start $bill_end )
855 ( SELECT SUM(amount) FROM cust_pay_refund
856 WHERE cust_pay.paynum = cust_pay_refund.paynum
857 $refund_start $refund_end )
866 # Used by FS::Upgrade to migrate to a new database.
870 sub _upgrade_data { #class method
871 my ($class, %opts) = @_;
873 warn "$me upgrading $class\n" if $DEBUG;
876 # otaker/ivan upgrade
879 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
881 #not the most efficient, but hey, it only has to run once
883 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
884 " AND usernum IS NULL ".
885 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
886 " WHERE cust_main.custnum = cust_pay.custnum ) ";
888 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
890 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
891 $sth->execute or die $sth->errstr;
892 my $total = $sth->fetchrow_arrayref->[0];
893 #warn "$total cust_pay records to update\n"
895 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
900 my @cust_pay = qsearch( {
901 'table' => 'cust_pay',
903 'extra_sql' => $where,
904 'order_by' => 'ORDER BY paynum',
907 foreach my $cust_pay (@cust_pay) {
909 my $h_cust_pay = $cust_pay->h_search('insert');
911 next if $cust_pay->otaker eq $h_cust_pay->history_user;
912 #$cust_pay->otaker($h_cust_pay->history_user);
913 $cust_pay->set('otaker', $h_cust_pay->history_user);
915 $cust_pay->set('otaker', 'legacy');
918 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
919 my $error = $cust_pay->replace;
922 warn " *** WARNING: Error updating order taker for payment paynum ".
923 $cust_pay->paynun. ": $error\n";
927 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
930 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
931 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
937 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
941 # payinfo N/A upgrade
944 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
946 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
948 my @na_cust_pay = qsearch( {
949 'table' => 'cust_pay',
950 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
951 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
954 foreach my $na ( @na_cust_pay ) {
956 next unless $na->payinfo eq 'N/A';
958 my $cust_pay_pending =
959 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
960 unless ( $cust_pay_pending ) {
961 warn " *** WARNING: not-yet recoverable N/A card for payment ".
962 $na->paynum. " (no cust_pay_pending)\n";
965 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
966 my $error = $na->replace;
968 warn " *** WARNING: Error updating payinfo for payment paynum ".
969 $na->paynun. ": $error\n";
975 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
979 # otaker->usernum upgrade
982 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
983 $class->_upgrade_otaker(%opts);
984 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
994 =item batch_import HASHREF
996 Inserts new payments.
1003 my $fh = $param->{filehandle};
1004 my $agentnum = $param->{agentnum};
1005 my $format = $param->{'format'};
1006 my $paybatch = $param->{'paybatch'};
1008 # here is the agent virtualization
1009 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1013 if ( $format eq 'simple' ) {
1014 @fields = qw( custnum agent_custid paid payinfo );
1016 } elsif ( $format eq 'extended' ) {
1017 die "unimplemented\n";
1021 die "unknown format $format";
1024 eval "use Text::CSV_XS;";
1027 my $csv = new Text::CSV_XS;
1031 local $SIG{HUP} = 'IGNORE';
1032 local $SIG{INT} = 'IGNORE';
1033 local $SIG{QUIT} = 'IGNORE';
1034 local $SIG{TERM} = 'IGNORE';
1035 local $SIG{TSTP} = 'IGNORE';
1036 local $SIG{PIPE} = 'IGNORE';
1038 my $oldAutoCommit = $FS::UID::AutoCommit;
1039 local $FS::UID::AutoCommit = 0;
1043 while ( defined($line=<$fh>) ) {
1045 $csv->parse($line) or do {
1046 $dbh->rollback if $oldAutoCommit;
1047 return "can't parse: ". $csv->error_input();
1050 my @columns = $csv->fields();
1054 paybatch => $paybatch,
1058 foreach my $field ( @fields ) {
1060 if ( $field eq 'agent_custid'
1062 && $columns[0] =~ /\S+/ )
1065 my $agent_custid = $columns[0];
1066 my %hash = ( 'agent_custid' => $agent_custid,
1067 'agentnum' => $agentnum,
1070 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1071 $dbh->rollback if $oldAutoCommit;
1072 return "can't specify custnum with agent_custid $agent_custid";
1075 $cust_main = qsearchs({
1076 'table' => 'cust_main',
1077 'hashref' => \%hash,
1078 'extra_sql' => $extra_sql,
1081 unless ( $cust_main ) {
1082 $dbh->rollback if $oldAutoCommit;
1083 return "can't find customer with agent_custid $agent_custid";
1087 $columns[0] = $cust_main->custnum;
1090 $cust_pay{$field} = shift @columns;
1093 my $cust_pay = new FS::cust_pay( \%cust_pay );
1094 my $error = $cust_pay->insert;
1097 $dbh->rollback if $oldAutoCommit;
1098 return "can't insert payment for $line: $error";
1101 if ( $format eq 'simple' ) {
1102 # include agentnum for less surprise?
1103 $cust_main = qsearchs({
1104 'table' => 'cust_main',
1105 'hashref' => { 'custnum' => $cust_pay->custnum },
1106 'extra_sql' => $extra_sql,
1110 unless ( $cust_main ) {
1111 $dbh->rollback if $oldAutoCommit;
1112 return "can't find customer to which payments apply at line: $line";
1115 $error = $cust_main->apply_payments_and_credits;
1117 $dbh->rollback if $oldAutoCommit;
1118 return "can't apply payments to customer for $line: $error";
1126 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1128 return "Empty file!" unless $imported;
1138 Delete and replace methods.
1142 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1143 schema.html from the base documentation.