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.
135 The number of the batch this payment came from (see L<FS::pay_batch>),
136 or null if it was processed through a realtime gateway or entered manually.
146 Creates a new payment. To add the payment to the databse, see L<"insert">.
150 sub table { 'cust_pay'; }
151 sub cust_linked { $_[0]->cust_main_custnum; }
152 sub cust_unlinked_msg {
154 "WARNING: can't find cust_main.custnum ". $self->custnum.
155 ' (cust_pay.paynum '. $self->paynum. ')';
158 =item insert [ OPTION => VALUE ... ]
160 Adds this payment to the database.
162 For backwards-compatibility and convenience, if the additional field invnum
163 is defined, an FS::cust_bill_pay record for the full amount of the payment
164 will be created. In this case, custnum is optional.
166 If the additional field discount_term is defined then a prepayment discount
167 is taken for that length of time. It is an error for the customer to owe
168 after this payment is made.
170 A hash of optional arguments may be passed. Currently "manual" is supported.
171 If true, a payment receipt is sent instead of a statement when
172 'payment_receipt_email' configuration option is set.
177 my($self, %options) = @_;
179 local $SIG{HUP} = 'IGNORE';
180 local $SIG{INT} = 'IGNORE';
181 local $SIG{QUIT} = 'IGNORE';
182 local $SIG{TERM} = 'IGNORE';
183 local $SIG{TSTP} = 'IGNORE';
184 local $SIG{PIPE} = 'IGNORE';
186 my $oldAutoCommit = $FS::UID::AutoCommit;
187 local $FS::UID::AutoCommit = 0;
191 if ( $self->invnum ) {
192 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
194 $dbh->rollback if $oldAutoCommit;
195 return "Unknown cust_bill.invnum: ". $self->invnum;
197 $self->custnum($cust_bill->custnum );
200 my $error = $self->check;
201 return $error if $error;
203 my $cust_main = $self->cust_main;
204 my $old_balance = $cust_main->balance;
206 $error = $self->SUPER::insert;
208 $dbh->rollback if $oldAutoCommit;
209 return "error inserting cust_pay: $error";
212 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
213 if ( my $months = $self->discount_term ) {
214 # XXX this should be moved out somewhere, but discount_term_values
216 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
217 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
219 # %billing_pkgs contains this customer's active monthly packages.
220 # Recurring fees for those packages will be credited and then rebilled
221 # for the full discount term. Other packages on the last invoice
222 # (canceled, non-monthly recurring, or one-time charges) will be
224 my %billing_pkgs = map { $_->pkgnum => $_ }
225 grep { $_->part_pkg->freq eq '1' }
226 $cust_main->billing_pkgs;
227 my $credit = 0; # sum of recurring charges from that invoice
228 my $last_bill_date = 0; # the real bill date
229 foreach my $item ( $cust_bill->cust_bill_pkg ) {
230 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
231 $credit += $item->recur;
232 $last_bill_date = $item->cust_pkg->last_bill
233 if defined($item->cust_pkg)
234 and $item->cust_pkg->last_bill > $last_bill_date
237 my $cust_credit = new FS::cust_credit {
238 'custnum' => $self->custnum,
239 'amount' => sprintf('%.2f', $credit),
240 'reason' => 'customer chose to prepay for discount',
242 $error = $cust_credit->insert('reason_type' => $credit_type);
244 $dbh->rollback if $oldAutoCommit;
245 return "error inserting prepayment credit: $error";
249 # bill for the entire term
250 $_->bill($_->last_bill) foreach (values %billing_pkgs);
251 $error = $cust_main->bill(
252 # no recurring_only, we want unbilled packages with start dates to
254 'no_usage_reset' => 1,
255 'time' => $last_bill_date, # not $cust_bill->_date
256 'pkg_list' => [ values %billing_pkgs ],
257 'freq_override' => $months,
260 $dbh->rollback if $oldAutoCommit;
261 return "error inserting cust_pay: $error";
263 $error = $cust_main->apply_payments_and_credits;
265 $dbh->rollback if $oldAutoCommit;
266 return "error inserting cust_pay: $error";
268 my $new_balance = $cust_main->balance;
269 if ($new_balance > 0) {
270 $dbh->rollback if $oldAutoCommit;
271 return "balance after prepay discount attempt: $new_balance";
273 # user friendly: override the "apply only to this invoice" mode
280 if ( $self->invnum ) {
281 my $cust_bill_pay = new FS::cust_bill_pay {
282 'invnum' => $self->invnum,
283 'paynum' => $self->paynum,
284 'amount' => $self->paid,
285 '_date' => $self->_date,
287 $error = $cust_bill_pay->insert(%options);
289 if ( $ignore_noapply ) {
290 warn "warning: error inserting cust_bill_pay: $error ".
291 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
293 $dbh->rollback if $oldAutoCommit;
294 return "error inserting cust_bill_pay: $error";
299 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
301 #false laziness w/ cust_credit::insert
302 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
303 my @errors = $cust_main->unsuspend;
305 # side-fx with nested transactions? upstack rolls back?
306 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
312 #bill setup fees for voip_cdr bill_every_call packages
313 #some false laziness w/search in freeside-cdrd
315 'LEFT JOIN part_pkg USING ( pkgpart ) '.
316 "LEFT JOIN part_pkg_option
317 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
318 AND part_pkg_option.optionname = 'bill_every_call' )";
320 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
321 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
323 my @cust_pkg = qsearch({
324 'table' => 'cust_pkg',
325 'addl_from' => $addl_from,
326 'hashref' => { 'custnum' => $self->custnum,
330 'extra_sql' => $extra_sql,
334 warn "voip_cdr bill_every_call packages found; billing customer\n";
335 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
337 warn "WARNING: Error billing customer: $bill_error\n";
340 #end of billing setup fees for voip_cdr bill_every_call packages
342 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
345 my $trigger = $conf->config('payment_receipt-trigger',
346 $self->cust_main->agentnum) || 'cust_pay';
347 if ( $trigger eq 'cust_pay' ) {
348 my $error = $self->send_receipt(
349 'manual' => $options{'manual'},
350 'cust_bill' => $cust_bill,
351 'cust_main' => $cust_main,
353 warn "can't send payment receipt/statement: $error" if $error;
360 =item void [ REASON ]
362 Voids this payment: deletes the payment and all associated applications and
363 adds a record of the voided payment to the FS::cust_pay_void table.
370 local $SIG{HUP} = 'IGNORE';
371 local $SIG{INT} = 'IGNORE';
372 local $SIG{QUIT} = 'IGNORE';
373 local $SIG{TERM} = 'IGNORE';
374 local $SIG{TSTP} = 'IGNORE';
375 local $SIG{PIPE} = 'IGNORE';
377 my $oldAutoCommit = $FS::UID::AutoCommit;
378 local $FS::UID::AutoCommit = 0;
381 my $cust_pay_void = new FS::cust_pay_void ( {
382 map { $_ => $self->get($_) } $self->fields
384 $cust_pay_void->reason(shift) if scalar(@_);
385 my $error = $cust_pay_void->insert;
387 $dbh->rollback if $oldAutoCommit;
391 $error = $self->delete;
393 $dbh->rollback if $oldAutoCommit;
397 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
405 Unless the closed flag is set, deletes this payment and all associated
406 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
407 cases, you want to use the void method instead to leave a record of the
412 # very similar to FS::cust_credit::delete
415 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
417 local $SIG{HUP} = 'IGNORE';
418 local $SIG{INT} = 'IGNORE';
419 local $SIG{QUIT} = 'IGNORE';
420 local $SIG{TERM} = 'IGNORE';
421 local $SIG{TSTP} = 'IGNORE';
422 local $SIG{PIPE} = 'IGNORE';
424 my $oldAutoCommit = $FS::UID::AutoCommit;
425 local $FS::UID::AutoCommit = 0;
428 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
429 my $error = $app->delete;
431 $dbh->rollback if $oldAutoCommit;
436 my $error = $self->SUPER::delete(@_);
438 $dbh->rollback if $oldAutoCommit;
442 if ( $conf->exists('deletepayments')
443 && $conf->config('deletepayments') ne '' ) {
445 my $cust_main = $self->cust_main;
447 my $error = send_email(
448 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
449 #invoice_from??? well as good as any
450 'to' => $conf->config('deletepayments'),
451 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
453 "This is an automatic message from your Freeside installation\n",
454 "informing you that the following payment has been deleted:\n",
456 'paynum: '. $self->paynum. "\n",
457 'custnum: '. $self->custnum.
458 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
459 'paid: $'. sprintf("%.2f", $self->paid). "\n",
460 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
461 'payby: '. $self->payby. "\n",
462 'payinfo: '. $self->paymask. "\n",
463 'paybatch: '. $self->paybatch. "\n",
468 $dbh->rollback if $oldAutoCommit;
469 return "can't send payment deletion notification: $error";
474 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
480 =item replace [ OLD_RECORD ]
482 You can, but probably shouldn't modify payments...
484 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
485 supplied, replaces this record. If there is an error, returns the error,
486 otherwise returns false.
492 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
493 $self->SUPER::replace(@_);
498 Checks all fields to make sure this is a valid payment. If there is an error,
499 returns the error, otherwise returns false. Called by the insert method.
506 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
509 $self->ut_numbern('paynum')
510 || $self->ut_numbern('custnum')
511 || $self->ut_numbern('_date')
512 || $self->ut_money('paid')
513 || $self->ut_alphan('otaker')
514 || $self->ut_textn('paybatch')
515 || $self->ut_textn('payunique')
516 || $self->ut_enum('closed', [ '', 'Y' ])
517 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
518 || $self->ut_textn('bank')
519 || $self->ut_alphan('depositor')
520 || $self->ut_numbern('account')
521 || $self->ut_numbern('teller')
522 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
523 || $self->payinfo_check()
525 return $error if $error;
527 return "paid must be > 0 " if $self->paid <= 0;
529 return "unknown cust_main.custnum: ". $self->custnum
531 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
533 $self->_date(time) unless $self->_date;
535 return "invalid discount_term"
536 if ($self->discount_term && $self->discount_term < 2);
538 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
539 foreach (qw(bank depositor account teller)) {
540 return "$_ required" if $self->get($_) eq '';
544 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
545 # # UNIQUE index should catch this too, without race conditions, but this
546 # # should give a better error message the other 99.9% of the time...
547 # if ( length($self->payunique)
548 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
549 # #well, it *could* be a better error message
550 # return "duplicate transaction".
551 # " - a payment with unique identifer ". $self->payunique.
558 =item send_receipt HASHREF | OPTION => VALUE ...
560 Sends a payment receipt for this payment..
568 Flag indicating the payment is being made manually.
572 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
577 Customer (FS::cust_main) object (for efficiency).
585 my $opt = ref($_[0]) ? shift : { @_ };
587 my $cust_bill = $opt->{'cust_bill'};
588 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
590 my $conf = new FS::Conf;
592 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
594 my @invoicing_list = $cust_main->invoicing_list_emailonly;
595 return '' unless @invoicing_list;
597 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
601 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
602 #|| ! $conf->exists('invoice_html_statement')
606 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
608 my $msg_template = FS::msg_template->by_key($msgnum);
609 $error = $msg_template->send(
610 'cust_main' => $cust_main,
612 'from_config' => 'payment_receipt_from',
615 } elsif ( $conf->exists('payment_receipt_email') ) {
617 my $receipt_template = new Text::Template (
619 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
621 warn "can't create payment receipt template: $Text::Template::ERROR";
625 my $payby = $self->payby;
626 my $payinfo = $self->payinfo;
627 $payby =~ s/^BILL$/Check/ if $payinfo;
628 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
629 $payinfo = $self->paymask
631 $payinfo = $self->decrypt($payinfo);
633 $payby =~ s/^CHEK$/Electronic check/;
636 'date' => time2str("%a %B %o, %Y", $self->_date),
637 'name' => $cust_main->name,
638 'paynum' => $self->paynum,
639 'paid' => sprintf("%.2f", $self->paid),
640 'payby' => ucfirst(lc($payby)),
641 'payinfo' => $payinfo,
642 'balance' => $cust_main->balance,
643 'company_name' => $conf->config('company_name', $cust_main->agentnum),
646 if ( $opt->{'cust_pkg'} ) {
647 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
648 #setup date, other things?
652 'from' => $conf->config('invoice_from', $cust_main->agentnum),
653 #invoice_from??? well as good as any
654 'to' => \@invoicing_list,
655 'subject' => 'Payment receipt',
656 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
661 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
665 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
667 my $queue = new FS::queue {
668 'paynum' => $self->paynum,
669 'job' => 'FS::cust_bill::queueable_email',
672 $error = $queue->insert(
673 'invnum' => $cust_bill->invnum,
674 'template' => 'statement',
675 'notice_name' => 'Statement',
681 warn "send_receipt: $error\n" if $error;
686 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
693 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
694 sort { $a->_date <=> $b->_date
695 || $a->invnum <=> $b->invnum }
696 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
700 =item cust_pay_refund
702 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
707 sub cust_pay_refund {
709 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
710 sort { $a->_date <=> $b->_date }
711 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
718 Returns the amount of this payment that is still unapplied; which is
719 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
720 applications (see L<FS::cust_pay_refund>).
726 my $amount = $self->paid;
727 $amount -= $_->amount foreach ( $self->cust_bill_pay );
728 $amount -= $_->amount foreach ( $self->cust_pay_refund );
729 sprintf("%.2f", $amount );
734 Returns the amount of this payment that has not been refuned; which is
735 paid minus all refund applications (see L<FS::cust_pay_refund>).
741 my $amount = $self->paid;
742 $amount -= $_->amount foreach ( $self->cust_pay_refund );
743 sprintf("%.2f", $amount );
748 Returns the "paid" field.
763 =item batch_insert CUST_PAY_OBJECT, ...
765 Class method which inserts multiple payments. Takes a list of FS::cust_pay
766 objects. Returns a list, each element representing the status of inserting the
767 corresponding payment - empty. If there is an error inserting any payment, the
768 entire transaction is rolled back, i.e. all payments are inserted or none are.
770 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
771 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
772 those objects will be inserted with the paynum of the payment, and for
773 each one, an error message or an empty string will be inserted into the
778 my @errors = FS::cust_pay->batch_insert(@cust_pay);
779 my $num_errors = scalar(grep $_, @errors);
780 if ( $num_errors == 0 ) {
781 #success; all payments were inserted
783 #failure; no payments were inserted.
789 my $self = shift; #class method
791 local $SIG{HUP} = 'IGNORE';
792 local $SIG{INT} = 'IGNORE';
793 local $SIG{QUIT} = 'IGNORE';
794 local $SIG{TERM} = 'IGNORE';
795 local $SIG{TSTP} = 'IGNORE';
796 local $SIG{PIPE} = 'IGNORE';
798 my $oldAutoCommit = $FS::UID::AutoCommit;
799 local $FS::UID::AutoCommit = 0;
805 foreach my $cust_pay (@_) {
806 my $error = $cust_pay->insert( 'manual' => 1 );
807 push @errors, $error;
808 $num_errors++ if $error;
810 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
812 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
813 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
817 $cust_bill_pay->set('paynum', $cust_pay->paynum);
818 my $apply_error = $cust_bill_pay->insert;
819 push @errors, $apply_error || '';
820 $num_errors++ if $apply_error;
824 } elsif ( !$error ) { #normal case: apply payments as usual
825 $cust_pay->cust_main->apply_payments;
831 $dbh->rollback if $oldAutoCommit;
833 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
842 Returns an SQL fragment to retreive the unapplied amount.
847 my ($class, $start, $end) = @_;
848 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
849 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
850 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
851 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
855 ( SELECT SUM(amount) FROM cust_bill_pay
856 WHERE cust_pay.paynum = cust_bill_pay.paynum
857 $bill_start $bill_end )
861 ( SELECT SUM(amount) FROM cust_pay_refund
862 WHERE cust_pay.paynum = cust_pay_refund.paynum
863 $refund_start $refund_end )
872 # Used by FS::Upgrade to migrate to a new database.
876 sub _upgrade_data { #class method
877 my ($class, %opts) = @_;
879 warn "$me upgrading $class\n" if $DEBUG;
882 # otaker/ivan upgrade
885 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
887 #not the most efficient, but hey, it only has to run once
889 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
890 " AND usernum IS NULL ".
891 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
892 " WHERE cust_main.custnum = cust_pay.custnum ) ";
894 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
896 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
897 $sth->execute or die $sth->errstr;
898 my $total = $sth->fetchrow_arrayref->[0];
899 #warn "$total cust_pay records to update\n"
901 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
906 my @cust_pay = qsearch( {
907 'table' => 'cust_pay',
909 'extra_sql' => $where,
910 'order_by' => 'ORDER BY paynum',
913 foreach my $cust_pay (@cust_pay) {
915 my $h_cust_pay = $cust_pay->h_search('insert');
917 next if $cust_pay->otaker eq $h_cust_pay->history_user;
918 #$cust_pay->otaker($h_cust_pay->history_user);
919 $cust_pay->set('otaker', $h_cust_pay->history_user);
921 $cust_pay->set('otaker', 'legacy');
924 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
925 my $error = $cust_pay->replace;
928 warn " *** WARNING: Error updating order taker for payment paynum ".
929 $cust_pay->paynun. ": $error\n";
933 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
936 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
937 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
943 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
947 # payinfo N/A upgrade
950 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
952 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
954 my @na_cust_pay = qsearch( {
955 'table' => 'cust_pay',
956 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
957 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
960 foreach my $na ( @na_cust_pay ) {
962 next unless $na->payinfo eq 'N/A';
964 my $cust_pay_pending =
965 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
966 unless ( $cust_pay_pending ) {
967 warn " *** WARNING: not-yet recoverable N/A card for payment ".
968 $na->paynum. " (no cust_pay_pending)\n";
971 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
972 my $error = $na->replace;
974 warn " *** WARNING: Error updating payinfo for payment paynum ".
975 $na->paynun. ": $error\n";
981 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
985 # otaker->usernum upgrade
988 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
989 $class->_upgrade_otaker(%opts);
990 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
993 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
995 my @cust_pay = qsearch( {
996 'table' => 'cust_pay',
997 'addl_from' => ' JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS text) ',
999 foreach my $cust_pay (@cust_pay) {
1000 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1001 $cust_pay->set('paybatch' => '');
1002 my $error = $cust_pay->replace;
1003 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1015 =item batch_import HASHREF
1017 Inserts new payments.
1024 my $fh = $param->{filehandle};
1025 my $agentnum = $param->{agentnum};
1026 my $format = $param->{'format'};
1027 my $paybatch = $param->{'paybatch'};
1029 # here is the agent virtualization
1030 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1034 if ( $format eq 'simple' ) {
1035 @fields = qw( custnum agent_custid paid payinfo );
1037 } elsif ( $format eq 'extended' ) {
1038 die "unimplemented\n";
1042 die "unknown format $format";
1045 eval "use Text::CSV_XS;";
1048 my $csv = new Text::CSV_XS;
1052 local $SIG{HUP} = 'IGNORE';
1053 local $SIG{INT} = 'IGNORE';
1054 local $SIG{QUIT} = 'IGNORE';
1055 local $SIG{TERM} = 'IGNORE';
1056 local $SIG{TSTP} = 'IGNORE';
1057 local $SIG{PIPE} = 'IGNORE';
1059 my $oldAutoCommit = $FS::UID::AutoCommit;
1060 local $FS::UID::AutoCommit = 0;
1064 while ( defined($line=<$fh>) ) {
1066 $csv->parse($line) or do {
1067 $dbh->rollback if $oldAutoCommit;
1068 return "can't parse: ". $csv->error_input();
1071 my @columns = $csv->fields();
1075 paybatch => $paybatch,
1079 foreach my $field ( @fields ) {
1081 if ( $field eq 'agent_custid'
1083 && $columns[0] =~ /\S+/ )
1086 my $agent_custid = $columns[0];
1087 my %hash = ( 'agent_custid' => $agent_custid,
1088 'agentnum' => $agentnum,
1091 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1092 $dbh->rollback if $oldAutoCommit;
1093 return "can't specify custnum with agent_custid $agent_custid";
1096 $cust_main = qsearchs({
1097 'table' => 'cust_main',
1098 'hashref' => \%hash,
1099 'extra_sql' => $extra_sql,
1102 unless ( $cust_main ) {
1103 $dbh->rollback if $oldAutoCommit;
1104 return "can't find customer with agent_custid $agent_custid";
1108 $columns[0] = $cust_main->custnum;
1111 $cust_pay{$field} = shift @columns;
1114 my $cust_pay = new FS::cust_pay( \%cust_pay );
1115 my $error = $cust_pay->insert;
1118 $dbh->rollback if $oldAutoCommit;
1119 return "can't insert payment for $line: $error";
1122 if ( $format eq 'simple' ) {
1123 # include agentnum for less surprise?
1124 $cust_main = qsearchs({
1125 'table' => 'cust_main',
1126 'hashref' => { 'custnum' => $cust_pay->custnum },
1127 'extra_sql' => $extra_sql,
1131 unless ( $cust_main ) {
1132 $dbh->rollback if $oldAutoCommit;
1133 return "can't find customer to which payments apply at line: $line";
1136 $error = $cust_main->apply_payments_and_credits;
1138 $dbh->rollback if $oldAutoCommit;
1139 return "can't apply payments to customer for $line: $error";
1147 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1149 return "Empty file!" unless $imported;
1159 Delete and replace methods.
1163 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1164 schema.html from the base documentation.