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;
28 $me = '[FS::cust_pay]';
32 #ask FS::UID to run this stuff for us later
33 FS::UID->install_callback( sub {
35 $unsuspendauto = $conf->exists('unsuspendauto');
38 @encrypted_fields = ('payinfo');
42 FS::cust_pay - Object methods for cust_pay objects
48 $record = new FS::cust_pay \%hash;
49 $record = new FS::cust_pay { 'column' => 'value' };
51 $error = $record->insert;
53 $error = $new_record->replace($old_record);
55 $error = $record->delete;
57 $error = $record->check;
61 An FS::cust_pay object represents a payment; the transfer of money from a
62 customer. FS::cust_pay inherits from FS::Record. The following fields are
69 primary key (assigned automatically for new payments)
73 customer (see L<FS::cust_main>)
77 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
78 L<Time::Local> and L<Date::Parse> for conversion functions.
82 Amount of this payment
86 order taker (see L<FS::access_user>)
90 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
94 Payment Information (See L<FS::payinfo_Mixin> for data format)
98 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
102 text field for tracking card processing or other batch grouping
106 Optional unique identifer to prevent duplicate transactions.
110 books closed flag, empty or `Y'
114 Desired pkgnum when using experimental package balances.
118 The bank where the payment was deposited.
122 The name of the depositor.
126 The deposit account number.
140 Creates a new payment. To add the payment to the databse, see L<"insert">.
144 sub table { 'cust_pay'; }
145 sub cust_linked { $_[0]->cust_main_custnum; }
146 sub cust_unlinked_msg {
148 "WARNING: can't find cust_main.custnum ". $self->custnum.
149 ' (cust_pay.paynum '. $self->paynum. ')';
152 =item insert [ OPTION => VALUE ... ]
154 Adds this payment to the database.
156 For backwards-compatibility and convenience, if the additional field invnum
157 is defined, an FS::cust_bill_pay record for the full amount of the payment
158 will be created. In this case, custnum is optional.
160 If the additional field discount_term is defined then a prepayment discount
161 is taken for that length of time. It is an error for the customer to owe
162 after this payment is made.
164 A hash of optional arguments may be passed. Currently "manual" is supported.
165 If true, a payment receipt is sent instead of a statement when
166 'payment_receipt_email' configuration option is set.
171 my($self, %options) = @_;
173 local $SIG{HUP} = 'IGNORE';
174 local $SIG{INT} = 'IGNORE';
175 local $SIG{QUIT} = 'IGNORE';
176 local $SIG{TERM} = 'IGNORE';
177 local $SIG{TSTP} = 'IGNORE';
178 local $SIG{PIPE} = 'IGNORE';
180 my $oldAutoCommit = $FS::UID::AutoCommit;
181 local $FS::UID::AutoCommit = 0;
185 if ( $self->invnum ) {
186 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
188 $dbh->rollback if $oldAutoCommit;
189 return "Unknown cust_bill.invnum: ". $self->invnum;
191 $self->custnum($cust_bill->custnum );
194 my $error = $self->check;
195 return $error if $error;
197 my $cust_main = $self->cust_main;
198 my $old_balance = $cust_main->balance;
200 $error = $self->SUPER::insert;
202 $dbh->rollback if $oldAutoCommit;
203 return "error inserting cust_pay: $error";
206 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
207 if ( my $months = $self->discount_term ) {
208 # XXX this should be moved out somewhere, but discount_term_values
210 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
211 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
213 # %billing_pkgs contains this customer's active monthly packages.
214 # Recurring fees for those packages will be credited and then rebilled
215 # for the full discount term. Other packages on the last invoice
216 # (canceled, non-monthly recurring, or one-time charges) will be
218 my %billing_pkgs = map { $_->pkgnum => $_ }
219 grep { $_->part_pkg->freq eq '1' }
220 $cust_main->billing_pkgs;
221 my $credit = 0; # sum of recurring charges from that invoice
222 my $last_bill_date = 0; # the real bill date
223 foreach my $item ( $cust_bill->cust_bill_pkg ) {
224 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
225 $credit += $item->recur;
226 $last_bill_date = $item->cust_pkg->last_bill
227 if defined($item->cust_pkg)
228 and $item->cust_pkg->last_bill > $last_bill_date
231 my $cust_credit = new FS::cust_credit {
232 'custnum' => $self->custnum,
233 'amount' => sprintf('%.2f', $credit),
234 'reason' => 'customer chose to prepay for discount',
236 $error = $cust_credit->insert('reason_type' => $credit_type);
238 $dbh->rollback if $oldAutoCommit;
239 return "error inserting prepayment credit: $error";
243 # bill for the entire term
244 $_->bill($_->last_bill) foreach (values %billing_pkgs);
245 $error = $cust_main->bill(
246 # no recurring_only, we want unbilled packages with start dates to
248 'no_usage_reset' => 1,
249 'time' => $last_bill_date, # not $cust_bill->_date
250 'pkg_list' => [ values %billing_pkgs ],
251 'freq_override' => $months,
254 $dbh->rollback if $oldAutoCommit;
255 return "error inserting cust_pay: $error";
257 $error = $cust_main->apply_payments_and_credits;
259 $dbh->rollback if $oldAutoCommit;
260 return "error inserting cust_pay: $error";
262 my $new_balance = $cust_main->balance;
263 if ($new_balance > 0) {
264 $dbh->rollback if $oldAutoCommit;
265 return "balance after prepay discount attempt: $new_balance";
267 # user friendly: override the "apply only to this invoice" mode
274 if ( $self->invnum ) {
275 my $cust_bill_pay = new FS::cust_bill_pay {
276 'invnum' => $self->invnum,
277 'paynum' => $self->paynum,
278 'amount' => $self->paid,
279 '_date' => $self->_date,
281 $error = $cust_bill_pay->insert(%options);
283 if ( $ignore_noapply ) {
284 warn "warning: error inserting cust_bill_pay: $error ".
285 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
287 $dbh->rollback if $oldAutoCommit;
288 return "error inserting cust_bill_pay: $error";
293 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
295 #false laziness w/ cust_credit::insert
296 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
297 my @errors = $cust_main->unsuspend;
299 # side-fx with nested transactions? upstack rolls back?
300 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
306 #bill setup fees for voip_cdr bill_every_call packages
307 #some false laziness w/search in freeside-cdrd
309 'LEFT JOIN part_pkg USING ( pkgpart ) '.
310 "LEFT JOIN part_pkg_option
311 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
312 AND part_pkg_option.optionname = 'bill_every_call' )";
314 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
315 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
317 my @cust_pkg = qsearch({
318 'table' => 'cust_pkg',
319 'addl_from' => $addl_from,
320 'hashref' => { 'custnum' => $self->custnum,
324 'extra_sql' => $extra_sql,
328 warn "voip_cdr bill_every_call packages found; billing customer\n";
329 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
331 warn "WARNING: Error billing customer: $bill_error\n";
334 #end of billing setup fees for voip_cdr bill_every_call packages
336 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
339 my $trigger = $conf->config('payment_receipt-trigger',
340 $self->cust_main->agentnum) || 'cust_pay';
341 if ( $trigger eq 'cust_pay' ) {
342 my $error = $self->send_receipt(
343 'manual' => $options{'manual'},
344 'cust_bill' => $cust_bill,
345 'cust_main' => $cust_main,
347 warn "can't send payment receipt/statement: $error" if $error;
354 =item void [ REASON ]
356 Voids this payment: deletes the payment and all associated applications and
357 adds a record of the voided payment to the FS::cust_pay_void table.
364 local $SIG{HUP} = 'IGNORE';
365 local $SIG{INT} = 'IGNORE';
366 local $SIG{QUIT} = 'IGNORE';
367 local $SIG{TERM} = 'IGNORE';
368 local $SIG{TSTP} = 'IGNORE';
369 local $SIG{PIPE} = 'IGNORE';
371 my $oldAutoCommit = $FS::UID::AutoCommit;
372 local $FS::UID::AutoCommit = 0;
375 my $cust_pay_void = new FS::cust_pay_void ( {
376 map { $_ => $self->get($_) } $self->fields
378 $cust_pay_void->reason(shift) if scalar(@_);
379 my $error = $cust_pay_void->insert;
381 $dbh->rollback if $oldAutoCommit;
385 $error = $self->delete;
387 $dbh->rollback if $oldAutoCommit;
391 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
399 Unless the closed flag is set, deletes this payment and all associated
400 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
401 cases, you want to use the void method instead to leave a record of the
406 # very similar to FS::cust_credit::delete
409 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
411 local $SIG{HUP} = 'IGNORE';
412 local $SIG{INT} = 'IGNORE';
413 local $SIG{QUIT} = 'IGNORE';
414 local $SIG{TERM} = 'IGNORE';
415 local $SIG{TSTP} = 'IGNORE';
416 local $SIG{PIPE} = 'IGNORE';
418 my $oldAutoCommit = $FS::UID::AutoCommit;
419 local $FS::UID::AutoCommit = 0;
422 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
423 my $error = $app->delete;
425 $dbh->rollback if $oldAutoCommit;
430 my $error = $self->SUPER::delete(@_);
432 $dbh->rollback if $oldAutoCommit;
436 if ( $conf->exists('deletepayments')
437 && $conf->config('deletepayments') ne '' ) {
439 my $cust_main = $self->cust_main;
441 my $error = send_email(
442 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
443 #invoice_from??? well as good as any
444 'to' => $conf->config('deletepayments'),
445 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
447 "This is an automatic message from your Freeside installation\n",
448 "informing you that the following payment has been deleted:\n",
450 'paynum: '. $self->paynum. "\n",
451 'custnum: '. $self->custnum.
452 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
453 'paid: $'. sprintf("%.2f", $self->paid). "\n",
454 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
455 'payby: '. $self->payby. "\n",
456 'payinfo: '. $self->paymask. "\n",
457 'paybatch: '. $self->paybatch. "\n",
462 $dbh->rollback if $oldAutoCommit;
463 return "can't send payment deletion notification: $error";
468 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
474 =item replace [ OLD_RECORD ]
476 You can, but probably shouldn't modify payments...
478 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
479 supplied, replaces this record. If there is an error, returns the error,
480 otherwise returns false.
486 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
487 $self->SUPER::replace(@_);
492 Checks all fields to make sure this is a valid payment. If there is an error,
493 returns the error, otherwise returns false. Called by the insert method.
500 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
503 $self->ut_numbern('paynum')
504 || $self->ut_numbern('custnum')
505 || $self->ut_numbern('_date')
506 || $self->ut_money('paid')
507 || $self->ut_alphan('otaker')
508 || $self->ut_textn('paybatch')
509 || $self->ut_textn('payunique')
510 || $self->ut_enum('closed', [ '', 'Y' ])
511 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
512 || $self->ut_textn('bank')
513 || $self->ut_alphan('depositor')
514 || $self->ut_numbern('account')
515 || $self->ut_numbern('teller')
516 || $self->payinfo_check()
518 return $error if $error;
520 return "paid must be > 0 " if $self->paid <= 0;
522 return "unknown cust_main.custnum: ". $self->custnum
524 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
526 $self->_date(time) unless $self->_date;
528 return "invalid discount_term"
529 if ($self->discount_term && $self->discount_term < 2);
531 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
532 foreach (qw(bank depositor account teller)) {
533 return "$_ required" if $self->get($_) eq '';
537 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
538 # # UNIQUE index should catch this too, without race conditions, but this
539 # # should give a better error message the other 99.9% of the time...
540 # if ( length($self->payunique)
541 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
542 # #well, it *could* be a better error message
543 # return "duplicate transaction".
544 # " - a payment with unique identifer ". $self->payunique.
551 =item send_receipt HASHREF | OPTION => VALUE ...
553 Sends a payment receipt for this payment..
561 Flag indicating the payment is being made manually.
565 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
570 Customer (FS::cust_main) object (for efficiency).
578 my $opt = ref($_[0]) ? shift : { @_ };
580 my $cust_bill = $opt->{'cust_bill'};
581 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
583 my $conf = new FS::Conf;
585 return '' unless $conf->exists('payment_receipt', $cust_main->agentnum);
587 my @invoicing_list = $cust_main->invoicing_list_emailonly;
588 return '' unless @invoicing_list;
590 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
594 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
595 #|| ! $conf->exists('invoice_html_statement')
599 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
601 my $msg_template = FS::msg_template->by_key($msgnum);
602 $error = $msg_template->send(
603 'cust_main' => $cust_main,
605 'from_config' => 'payment_receipt_from',
608 } elsif ( $conf->exists('payment_receipt_email') ) {
610 my $receipt_template = new Text::Template (
612 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
614 warn "can't create payment receipt template: $Text::Template::ERROR";
618 my $payby = $self->payby;
619 my $payinfo = $self->payinfo;
620 $payby =~ s/^BILL$/Check/ if $payinfo;
621 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
622 $payinfo = $self->paymask
624 $payinfo = $self->decrypt($payinfo);
626 $payby =~ s/^CHEK$/Electronic check/;
629 'date' => time2str("%a %B %o, %Y", $self->_date),
630 'name' => $cust_main->name,
631 'paynum' => $self->paynum,
632 'paid' => sprintf("%.2f", $self->paid),
633 'payby' => ucfirst(lc($payby)),
634 'payinfo' => $payinfo,
635 'balance' => $cust_main->balance,
636 'company_name' => $conf->config('company_name', $cust_main->agentnum),
639 if ( $opt->{'cust_pkg'} ) {
640 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
641 #setup date, other things?
645 'from' => $conf->config('invoice_from', $cust_main->agentnum),
646 #invoice_from??? well as good as any
647 'to' => \@invoicing_list,
648 'subject' => 'Payment receipt',
649 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
654 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
660 my $queue = new FS::queue {
661 'paynum' => $self->paynum,
662 'job' => 'FS::cust_bill::queueable_email',
665 $error = $queue->insert(
666 'invnum' => $cust_bill->invnum,
667 'template' => 'statement',
668 'notice_name' => 'Statement',
674 warn "send_receipt: $error\n" if $error;
679 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
686 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
687 sort { $a->_date <=> $b->_date
688 || $a->invnum <=> $b->invnum }
689 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
693 =item cust_pay_refund
695 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
700 sub cust_pay_refund {
702 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
703 sort { $a->_date <=> $b->_date }
704 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
711 Returns the amount of this payment that is still unapplied; which is
712 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
713 applications (see L<FS::cust_pay_refund>).
719 my $amount = $self->paid;
720 $amount -= $_->amount foreach ( $self->cust_bill_pay );
721 $amount -= $_->amount foreach ( $self->cust_pay_refund );
722 sprintf("%.2f", $amount );
727 Returns the amount of this payment that has not been refuned; which is
728 paid minus all refund applications (see L<FS::cust_pay_refund>).
734 my $amount = $self->paid;
735 $amount -= $_->amount foreach ( $self->cust_pay_refund );
736 sprintf("%.2f", $amount );
741 Returns the "paid" field.
756 =item batch_insert CUST_PAY_OBJECT, ...
758 Class method which inserts multiple payments. Takes a list of FS::cust_pay
759 objects. Returns a list, each element representing the status of inserting the
760 corresponding payment - empty. If there is an error inserting any payment, the
761 entire transaction is rolled back, i.e. all payments are inserted or none are.
763 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
764 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
765 those objects will be inserted with the paynum of the payment, and for
766 each one, an error message or an empty string will be inserted into the
771 my @errors = FS::cust_pay->batch_insert(@cust_pay);
772 my $num_errors = scalar(grep $_, @errors);
773 if ( $num_errors == 0 ) {
774 #success; all payments were inserted
776 #failure; no payments were inserted.
782 my $self = shift; #class method
784 local $SIG{HUP} = 'IGNORE';
785 local $SIG{INT} = 'IGNORE';
786 local $SIG{QUIT} = 'IGNORE';
787 local $SIG{TERM} = 'IGNORE';
788 local $SIG{TSTP} = 'IGNORE';
789 local $SIG{PIPE} = 'IGNORE';
791 my $oldAutoCommit = $FS::UID::AutoCommit;
792 local $FS::UID::AutoCommit = 0;
798 foreach my $cust_pay (@_) {
799 my $error = $cust_pay->insert( 'manual' => 1 );
800 push @errors, $error;
801 $num_errors++ if $error;
803 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
805 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
806 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
810 $cust_bill_pay->set('paynum', $cust_pay->paynum);
811 my $apply_error = $cust_bill_pay->insert;
812 push @errors, $apply_error || '';
813 $num_errors++ if $apply_error;
817 } elsif ( !$error ) { #normal case: apply payments as usual
818 $cust_pay->cust_main->apply_payments;
824 $dbh->rollback if $oldAutoCommit;
826 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
835 Returns an SQL fragment to retreive the unapplied amount.
840 my ($class, $start, $end) = @_;
841 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
842 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
843 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
844 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
848 ( SELECT SUM(amount) FROM cust_bill_pay
849 WHERE cust_pay.paynum = cust_bill_pay.paynum
850 $bill_start $bill_end )
854 ( SELECT SUM(amount) FROM cust_pay_refund
855 WHERE cust_pay.paynum = cust_pay_refund.paynum
856 $refund_start $refund_end )
865 # Used by FS::Upgrade to migrate to a new database.
869 sub _upgrade_data { #class method
870 my ($class, %opts) = @_;
872 warn "$me upgrading $class\n" if $DEBUG;
875 # otaker/ivan upgrade
878 #not the most efficient, but hey, it only has to run once
880 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
881 " AND usernum IS NULL ".
882 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
883 " WHERE cust_main.custnum = cust_pay.custnum ) ";
885 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
887 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
888 $sth->execute or die $sth->errstr;
889 my $total = $sth->fetchrow_arrayref->[0];
890 #warn "$total cust_pay records to update\n"
892 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
897 my @cust_pay = qsearch( {
898 'table' => 'cust_pay',
900 'extra_sql' => $where,
901 'order_by' => 'ORDER BY paynum',
904 foreach my $cust_pay (@cust_pay) {
906 my $h_cust_pay = $cust_pay->h_search('insert');
908 next if $cust_pay->otaker eq $h_cust_pay->history_user;
909 #$cust_pay->otaker($h_cust_pay->history_user);
910 $cust_pay->set('otaker', $h_cust_pay->history_user);
912 $cust_pay->set('otaker', 'legacy');
915 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
916 my $error = $cust_pay->replace;
919 warn " *** WARNING: Error updating order taker for payment paynum ".
920 $cust_pay->paynun. ": $error\n";
924 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
927 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
928 warn "$me $count/$total (". sprintf('%.2f',100*$count/$total). '%)'. "\n";
935 # payinfo N/A upgrade
938 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
940 my @na_cust_pay = qsearch( {
941 'table' => 'cust_pay',
942 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
943 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
946 foreach my $na ( @na_cust_pay ) {
948 next unless $na->payinfo eq 'N/A';
950 my $cust_pay_pending =
951 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
952 unless ( $cust_pay_pending ) {
953 warn " *** WARNING: not-yet recoverable N/A card for payment ".
954 $na->paynum. " (no cust_pay_pending)\n";
957 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
958 my $error = $na->replace;
960 warn " *** WARNING: Error updating payinfo for payment paynum ".
961 $na->paynun. ": $error\n";
968 # otaker->usernum upgrade
971 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
972 $class->_upgrade_otaker(%opts);
973 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
983 =item batch_import HASHREF
985 Inserts new payments.
992 my $fh = $param->{filehandle};
993 my $agentnum = $param->{agentnum};
994 my $format = $param->{'format'};
995 my $paybatch = $param->{'paybatch'};
997 # here is the agent virtualization
998 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1002 if ( $format eq 'simple' ) {
1003 @fields = qw( custnum agent_custid paid payinfo );
1005 } elsif ( $format eq 'extended' ) {
1006 die "unimplemented\n";
1010 die "unknown format $format";
1013 eval "use Text::CSV_XS;";
1016 my $csv = new Text::CSV_XS;
1020 local $SIG{HUP} = 'IGNORE';
1021 local $SIG{INT} = 'IGNORE';
1022 local $SIG{QUIT} = 'IGNORE';
1023 local $SIG{TERM} = 'IGNORE';
1024 local $SIG{TSTP} = 'IGNORE';
1025 local $SIG{PIPE} = 'IGNORE';
1027 my $oldAutoCommit = $FS::UID::AutoCommit;
1028 local $FS::UID::AutoCommit = 0;
1032 while ( defined($line=<$fh>) ) {
1034 $csv->parse($line) or do {
1035 $dbh->rollback if $oldAutoCommit;
1036 return "can't parse: ". $csv->error_input();
1039 my @columns = $csv->fields();
1043 paybatch => $paybatch,
1047 foreach my $field ( @fields ) {
1049 if ( $field eq 'agent_custid'
1051 && $columns[0] =~ /\S+/ )
1054 my $agent_custid = $columns[0];
1055 my %hash = ( 'agent_custid' => $agent_custid,
1056 'agentnum' => $agentnum,
1059 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1060 $dbh->rollback if $oldAutoCommit;
1061 return "can't specify custnum with agent_custid $agent_custid";
1064 $cust_main = qsearchs({
1065 'table' => 'cust_main',
1066 'hashref' => \%hash,
1067 'extra_sql' => $extra_sql,
1070 unless ( $cust_main ) {
1071 $dbh->rollback if $oldAutoCommit;
1072 return "can't find customer with agent_custid $agent_custid";
1076 $columns[0] = $cust_main->custnum;
1079 $cust_pay{$field} = shift @columns;
1082 my $cust_pay = new FS::cust_pay( \%cust_pay );
1083 my $error = $cust_pay->insert;
1086 $dbh->rollback if $oldAutoCommit;
1087 return "can't insert payment for $line: $error";
1090 if ( $format eq 'simple' ) {
1091 # include agentnum for less surprise?
1092 $cust_main = qsearchs({
1093 'table' => 'cust_main',
1094 'hashref' => { 'custnum' => $cust_pay->custnum },
1095 'extra_sql' => $extra_sql,
1099 unless ( $cust_main ) {
1100 $dbh->rollback if $oldAutoCommit;
1101 return "can't find customer to which payments apply at line: $line";
1104 $error = $cust_main->apply_payments_and_credits;
1106 $dbh->rollback if $oldAutoCommit;
1107 return "can't apply payments to customer for $line: $error";
1115 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1117 return "Empty file!" unless $imported;
1127 Delete and replace methods.
1131 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1132 schema.html from the base documentation.