4 use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin
6 use vars qw( $DEBUG $me $conf @encrypted_fields
10 use Business::CreditCard;
12 use FS::UID qw( getotaker driver_name );
13 use FS::Misc qw( send_email );
14 use FS::Misc::DateTime qw( parse_datetime ); #for batch_import
15 use FS::Record qw( dbh qsearch qsearchs );
18 use FS::cust_main_Mixin;
19 use FS::payinfo_transaction_Mixin;
21 use FS::cust_bill_pay;
22 use FS::cust_pay_refund;
25 use FS::cust_pay_void;
26 use FS::upgrade_journal;
31 $me = '[FS::cust_pay]';
35 #ask FS::UID to run this stuff for us later
36 FS::UID->install_callback( sub {
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 Credit card type, if appropriate; autodetected.
105 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
109 obsolete text field for tracking card processing or other batch grouping
113 Optional unique identifer to prevent duplicate transactions.
117 books closed flag, empty or `Y'
121 Desired pkgnum when using experimental package balances.
125 Flag to only allow manual application of payment, empty or 'Y'
129 The bank where the payment was deposited.
133 The name of the depositor.
137 The deposit account number.
145 The number of the batch this payment came from (see L<FS::pay_batch>),
146 or null if it was processed through a realtime gateway or entered manually.
150 The number of the realtime or batch gateway L<FS::payment_gateway>) this
151 payment was processed through. Null if it was entered manually or processed
152 by the "system default" gateway, which doesn't have a number.
156 The name of the processor module (Business::OnlinePayment, ::BatchPayment,
157 or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
158 redundant with C<gatewaynum>.
162 The authorization number returned by the credit card network.
166 The transaction ID returned by the gateway, if any. This is usually what
167 you would use to initiate a void or refund of the payment.
177 Creates a new payment. To add the payment to the databse, see L<"insert">.
181 sub table { 'cust_pay'; }
182 sub cust_linked { $_[0]->cust_main_custnum; }
183 sub cust_unlinked_msg {
185 "WARNING: can't find cust_main.custnum ". $self->custnum.
186 ' (cust_pay.paynum '. $self->paynum. ')';
189 =item insert [ OPTION => VALUE ... ]
191 Adds this payment to the database.
193 For backwards-compatibility and convenience, if the additional field invnum
194 is defined, an FS::cust_bill_pay record for the full amount of the payment
195 will be created. In this case, custnum is optional.
197 If the additional field discount_term is defined then a prepayment discount
198 is taken for that length of time. It is an error for the customer to owe
199 after this payment is made.
201 A hash of optional arguments may be passed. Currently "manual" is supported.
202 If true, a payment receipt is sent instead of a statement when
203 'payment_receipt_email' configuration option is set.
205 About the "manual" flag: Normally, if the 'payment_receipt' config option
206 is set, and the customer has an invoice email address, inserting a payment
207 causes a I<statement> to be emailed to the customer. If the payment is
208 considered "manual" (or if the customer has no invoices), then it will
209 instead send a I<payment receipt>. "manual" should be true whenever a
210 payment is created directly from the web interface, from a user-initiated
211 realtime payment, or from a third-party payment via self-service. It should
212 be I<false> when creating a payment from a billing event or from a batch.
217 my($self, %options) = @_;
219 local $SIG{HUP} = 'IGNORE';
220 local $SIG{INT} = 'IGNORE';
221 local $SIG{QUIT} = 'IGNORE';
222 local $SIG{TERM} = 'IGNORE';
223 local $SIG{TSTP} = 'IGNORE';
224 local $SIG{PIPE} = 'IGNORE';
226 my $oldAutoCommit = $FS::UID::AutoCommit;
227 local $FS::UID::AutoCommit = 0;
230 my $conf = new FS::Conf;
233 if ( $self->invnum ) {
234 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
236 $dbh->rollback if $oldAutoCommit;
237 return "Unknown cust_bill.invnum: ". $self->invnum;
239 if ($self->custnum && ($cust_bill->custnum ne $self->custnum)) {
240 $dbh->rollback if $oldAutoCommit;
241 return "Invoice custnum ".$cust_bill->custnum
242 ." does not match specified custnum ".$self->custnum
243 ." for invoice ".$self->invnum;
245 $self->custnum($cust_bill->custnum );
248 my $error = $self->check;
249 return $error if $error;
251 my $cust_main = $self->cust_main;
252 my $old_balance = $cust_main->balance;
254 $error = $self->SUPER::insert;
256 $dbh->rollback if $oldAutoCommit;
257 return "error inserting cust_pay: $error";
260 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
261 if ( my $months = $self->discount_term ) {
262 # XXX this should be moved out somewhere, but discount_term_values
264 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
265 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
267 # %billing_pkgs contains this customer's active monthly packages.
268 # Recurring fees for those packages will be credited and then rebilled
269 # for the full discount term. Other packages on the last invoice
270 # (canceled, non-monthly recurring, or one-time charges) will be
272 my %billing_pkgs = map { $_->pkgnum => $_ }
273 grep { $_->part_pkg->freq eq '1' }
274 $cust_main->billing_pkgs;
275 my $credit = 0; # sum of recurring charges from that invoice
276 my $last_bill_date = 0; # the real bill date
277 foreach my $item ( $cust_bill->cust_bill_pkg ) {
278 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
279 $credit += $item->recur;
280 $last_bill_date = $item->cust_pkg->last_bill
281 if defined($item->cust_pkg)
282 and $item->cust_pkg->last_bill > $last_bill_date
285 my $cust_credit = new FS::cust_credit {
286 'custnum' => $self->custnum,
287 'amount' => sprintf('%.2f', $credit),
288 'reason' => 'customer chose to prepay for discount',
290 $error = $cust_credit->insert('reason_type' => $credit_type);
292 $dbh->rollback if $oldAutoCommit;
293 return "error inserting prepayment credit: $error";
297 # bill for the entire term
298 $_->bill($_->last_bill) foreach (values %billing_pkgs);
299 $error = $cust_main->bill(
300 # no recurring_only, we want unbilled packages with start dates to
302 'no_usage_reset' => 1,
303 'time' => $last_bill_date, # not $cust_bill->_date
304 'pkg_list' => [ values %billing_pkgs ],
305 'freq_override' => $months,
308 $dbh->rollback if $oldAutoCommit;
309 return "error inserting cust_pay: $error";
311 $error = $cust_main->apply_payments_and_credits;
313 $dbh->rollback if $oldAutoCommit;
314 return "error inserting cust_pay: $error";
316 my $new_balance = $cust_main->balance;
317 if ($new_balance > 0) {
318 $dbh->rollback if $oldAutoCommit;
319 return "balance after prepay discount attempt: $new_balance";
321 # user friendly: override the "apply only to this invoice" mode
328 if ( $self->invnum ) {
329 my $cust_bill_pay = new FS::cust_bill_pay {
330 'invnum' => $self->invnum,
331 'paynum' => $self->paynum,
332 'amount' => $self->paid,
333 '_date' => $self->_date,
335 $error = $cust_bill_pay->insert(%options);
337 if ( $ignore_noapply ) {
338 warn "warning: error inserting cust_bill_pay: $error ".
339 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
341 $dbh->rollback if $oldAutoCommit;
342 return "error inserting cust_bill_pay: $error";
347 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
349 # possibly trigger package unsuspend, doesn't abort transaction on failure
350 $self->unsuspend_balance if $old_balance;
352 #bill setup fees for voip_cdr bill_every_call packages
353 #some false laziness w/search in freeside-cdrd
355 'LEFT JOIN part_pkg USING ( pkgpart ) '.
356 "LEFT JOIN part_pkg_option
357 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
358 AND part_pkg_option.optionname = 'bill_every_call' )";
360 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
361 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
363 my @cust_pkg = qsearch({
364 'table' => 'cust_pkg',
365 'addl_from' => $addl_from,
366 'hashref' => { 'custnum' => $self->custnum,
370 'extra_sql' => $extra_sql,
374 warn "voip_cdr bill_every_call packages found; billing customer\n";
375 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
377 warn "WARNING: Error billing customer: $bill_error\n";
380 #end of billing setup fees for voip_cdr bill_every_call packages
382 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
385 my $trigger = $conf->config('payment_receipt-trigger',
386 $self->cust_main->agentnum) || 'cust_pay';
387 if ( $trigger eq 'cust_pay' ) {
388 my $error = $self->send_receipt(
389 'manual' => $options{'manual'},
390 'cust_bill' => $cust_bill,
391 'cust_main' => $cust_main,
393 warn "can't send payment receipt/statement: $error" if $error;
396 #run payment events immediately
397 my $due_cust_event = $self->cust_main->due_cust_event(
398 'eventtable' => 'cust_pay',
399 'objects' => [ $self ],
401 if ( !ref($due_cust_event) ) {
402 warn "Error searching for cust_pay billing events: $due_cust_event\n";
404 foreach my $cust_event (@$due_cust_event) {
405 next unless $cust_event->test_conditions;
406 if ( my $error = $cust_event->do_event() ) {
407 warn "Error running cust_pay billing event: $error\n";
416 =item void [ REASON ]
418 Voids this payment: deletes the payment and all associated applications and
419 adds a record of the voided payment to the FS::cust_pay_void table.
426 local $SIG{HUP} = 'IGNORE';
427 local $SIG{INT} = 'IGNORE';
428 local $SIG{QUIT} = 'IGNORE';
429 local $SIG{TERM} = 'IGNORE';
430 local $SIG{TSTP} = 'IGNORE';
431 local $SIG{PIPE} = 'IGNORE';
433 my $oldAutoCommit = $FS::UID::AutoCommit;
434 local $FS::UID::AutoCommit = 0;
437 my $cust_pay_void = new FS::cust_pay_void ( {
438 map { $_ => $self->get($_) } $self->fields
440 $cust_pay_void->reason(shift) if scalar(@_);
441 my $error = $cust_pay_void->insert;
443 my $cust_pay_pending =
444 qsearchs('cust_pay_pending', { paynum => $self->paynum });
445 if ( $cust_pay_pending ) {
446 $cust_pay_pending->set('void_paynum', $self->paynum);
447 $cust_pay_pending->set('paynum', '');
448 $error ||= $cust_pay_pending->replace;
451 $error ||= $self->delete;
454 $dbh->rollback if $oldAutoCommit;
458 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
466 Unless the closed flag is set, deletes this payment and all associated
467 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
468 cases, you want to use the void method instead to leave a record of the
473 # very similar to FS::cust_credit::delete
476 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
478 local $SIG{HUP} = 'IGNORE';
479 local $SIG{INT} = 'IGNORE';
480 local $SIG{QUIT} = 'IGNORE';
481 local $SIG{TERM} = 'IGNORE';
482 local $SIG{TSTP} = 'IGNORE';
483 local $SIG{PIPE} = 'IGNORE';
485 my $oldAutoCommit = $FS::UID::AutoCommit;
486 local $FS::UID::AutoCommit = 0;
489 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
490 my $error = $app->delete;
492 $dbh->rollback if $oldAutoCommit;
497 my $error = $self->SUPER::delete(@_);
499 $dbh->rollback if $oldAutoCommit;
503 if ( $conf->exists('deletepayments')
504 && $conf->config('deletepayments') ne '' ) {
506 my $cust_main = $self->cust_main;
508 my $error = send_email(
509 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
510 #invoice_from??? well as good as any
511 'to' => $conf->config('deletepayments'),
512 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
514 "This is an automatic message from your Freeside installation\n",
515 "informing you that the following payment has been deleted:\n",
517 'paynum: '. $self->paynum. "\n",
518 'custnum: '. $self->custnum.
519 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
520 'paid: $'. sprintf("%.2f", $self->paid). "\n",
521 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
522 'payby: '. $self->payby. "\n",
523 'payinfo: '. $self->paymask. "\n",
524 'paybatch: '. $self->paybatch. "\n",
529 $dbh->rollback if $oldAutoCommit;
530 return "can't send payment deletion notification: $error";
535 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
541 =item replace [ OLD_RECORD ]
543 You can, but probably shouldn't modify payments...
545 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
546 supplied, replaces this record. If there is an error, returns the error,
547 otherwise returns false.
553 return "Can't modify closed payment"
554 if $self->closed =~ /^Y/i && !$FS::payinfo_Mixin::allow_closed_replace;
555 $self->SUPER::replace(@_);
560 Checks all fields to make sure this is a valid payment. If there is an error,
561 returns the error, otherwise returns false. Called by the insert method.
568 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
571 $self->ut_numbern('paynum')
572 || $self->ut_numbern('custnum')
573 || $self->ut_numbern('_date')
574 || $self->ut_money('paid')
575 || $self->ut_alphan('otaker')
576 || $self->ut_textn('paybatch')
577 || $self->ut_textn('payunique')
578 || $self->ut_enum('closed', [ '', 'Y' ])
579 || $self->ut_flag('no_auto_apply')
580 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
581 || $self->ut_textn('bank')
582 || $self->ut_alphan('depositor')
583 || $self->ut_numbern('account')
584 || $self->ut_numbern('teller')
585 || $self->ut_foreign_keyn('batchnum', 'pay_batch', 'batchnum')
586 || $self->payinfo_check()
588 return $error if $error;
590 return "paid must be > 0 " if $self->paid <= 0;
592 return "unknown cust_main.custnum: ". $self->custnum
594 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
596 $self->_date(time) unless $self->_date;
598 return "invalid discount_term"
599 if ($self->discount_term && $self->discount_term < 2);
601 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
602 foreach (qw(bank depositor account teller)) {
603 return "$_ required" if $self->get($_) eq '';
607 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
608 # # UNIQUE index should catch this too, without race conditions, but this
609 # # should give a better error message the other 99.9% of the time...
610 # if ( length($self->payunique)
611 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
612 # #well, it *could* be a better error message
613 # return "duplicate transaction".
614 # " - a payment with unique identifer ". $self->payunique.
621 =item send_receipt HASHREF | OPTION => VALUE ...
623 Sends a payment receipt for this payment..
631 Flag indicating the payment is being made manually.
635 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
640 Customer (FS::cust_main) object (for efficiency).
648 my $opt = ref($_[0]) ? shift : { @_ };
650 my $cust_bill = $opt->{'cust_bill'};
651 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
653 my $conf = new FS::Conf;
655 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
657 return '' if ($conf->config_bool('allow_payment_receipt_noemail', $cust_main->agentnum) && $cust_main->paymentreceipt_noemail);
659 my @invoicing_list = $cust_main->invoicing_list_emailonly;
660 return '' unless @invoicing_list;
662 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
666 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
667 #|| ! $conf->exists('invoice_html_statement')
672 $error = $self->send_message_receipt(
673 'cust_main' => $cust_main,
674 'cust_bill' => $opt->{cust_bill},
675 'cust_pkg' => $opt->{cust_pkg},
676 'invoicing_list' => \@invoicing_list,
677 'msgnum' => $conf->config('payment_receipt_msgnum', $cust_main->agentnum)
680 } elsif ( ! $cust_main->invoice_noemail ) { #not manual
682 # check to see if they want to send specific message template as receipt for auto payments
683 if ( $conf->config('payment_receipt_msgnum_auto', $cust_main->agentnum) ) {
684 $error = $self->send_message_receipt(
685 'cust_main' => $cust_main,
686 'cust_bill' => $opt->{cust_bill},
687 'msgnum' => $conf->config('payment_receipt_msgnum_auto', $cust_main->agentnum),
691 my $queue = new FS::queue {
692 'job' => 'FS::cust_bill::queueable_email',
693 'paynum' => $self->paynum,
694 'custnum' => $cust_main->custnum,
698 'invnum' => $cust_bill->invnum,
702 if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
703 $opt{'mode'} = $mode;
705 # backward compatibility, no good fix for this yet as some people may
706 # still have "invoice_latex_statement" and such options
707 $opt{'template'} = 'statement';
708 $opt{'notice_name'} = 'Statement';
711 $error = $queue->insert(%opt);
715 warn "send_receipt: $error\n" if $error;
718 =item send_message_receipt
720 sends out a message receipt.
721 $error = $self->send_message_receipt(
722 'cust_main' => $cust_main,
723 'cust_bill' => $opt->{cust_bill},
724 'cust_pkg' => $opt->{cust_pkg},
725 'invoicing_list' => \@invoicing_list,
726 'msgnum' => $conf->config('payment_receipt_msgnum', $cust_main->agentnum)
731 sub send_message_receipt {
732 my ($self, %opt) = @_;
733 my $cust_main = $opt{'cust_main'};
734 my $cust_bill = $opt{'cust_bill'};
735 my $cust_pkg = $opt{'cust_pkg'};
736 my @invoicing_list = ref($opt{'invoicing_list'}) ? @{ $opt{'invoicing_list'} } : ( $opt{'invoicing_list'} );
737 my $msgnum = $opt{'msgnum'};
742 my %substitutions = ();
743 $substitutions{invnum} = $cust_bill->invnum if $cust_bill;
745 my $msg_template = qsearchs('msg_template',{ msgnum => $msgnum});
746 unless ($msg_template) {
747 return "send_receipt could not load msg_template";
750 my $queue = new FS::queue {
751 'job' => 'FS::Misc::process_send_email',
752 'paynum' => $self->paynum,
753 'custnum' => $cust_main->custnum,
755 $error = $queue->insert(
756 FS::msg_template->by_key($msgnum)->prepare(
757 'cust_main' => $cust_main,
759 'from_config' => 'payment_receipt_from',
760 'substitutions' => \%substitutions,
762 'msgtype' => 'receipt', # override msg_template's default
764 } elsif ( $conf->exists('payment_receipt_email') ) {
766 my $receipt_template = new Text::Template (
768 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
770 return "can't create payment receipt template: $Text::Template::ERROR";
773 my $payby = $self->payby;
774 my $payinfo = $self->payinfo;
775 $payby =~ s/^BILL$/Check/ if $payinfo;
776 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
777 $payinfo = $self->paymask
779 $payinfo = $self->decrypt($payinfo);
781 $payby =~ s/^CHEK$/Electronic check/;
784 'date' => time2str("%a %B %o, %Y", $self->_date),
785 'name' => $cust_main->name,
786 'paynum' => $self->paynum,
787 'paid' => sprintf("%.2f", $self->paid),
788 'payby' => ucfirst(lc($payby)),
789 'payinfo' => $payinfo,
790 'balance' => $cust_main->balance,
791 'company_name' => $conf->config('company_name', $cust_main->agentnum),
794 $fill_in{'invnum'} = $cust_bill->invnum if $cust_bill;
797 $fill_in{'pkg'} = $cust_pkg->part_pkg->pkg;
798 #setup date, other things?
801 my $queue = new FS::queue {
802 'job' => 'FS::Misc::process_send_generated_email',
803 'paynum' => $self->paynum,
804 'custnum' => $cust_main->custnum,
805 'msgtype' => 'receipt',
807 $error = $queue->insert(
808 'from' => $conf->invoice_from_full( $cust_main->agentnum ),
809 #invoice_from??? well as good as any
810 'to' => \@invoicing_list,
811 'subject' => 'Payment receipt',
812 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
815 $error = "payment_receipt is on, but no payment_receipt_msgnum\n";
823 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
830 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
831 sort { $a->_date <=> $b->_date
832 || $a->invnum <=> $b->invnum }
833 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
837 =item cust_pay_refund
839 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
844 sub cust_pay_refund {
846 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
847 sort { $a->_date <=> $b->_date }
848 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
855 Returns the amount of this payment that is still unapplied; which is
856 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
857 applications (see L<FS::cust_pay_refund>).
863 my $amount = $self->paid;
864 $amount -= $_->amount foreach ( $self->cust_bill_pay );
865 $amount -= $_->amount foreach ( $self->cust_pay_refund );
866 sprintf("%.2f", $amount );
871 Returns the amount of this payment that has not been refuned; which is
872 paid minus all refund applications (see L<FS::cust_pay_refund>).
878 my $amount = $self->paid;
879 $amount -= $_->amount foreach ( $self->cust_pay_refund );
880 sprintf("%.2f", $amount );
885 Returns the "paid" field.
894 =item delete_cust_bill_pay OPTIONS
896 Deletes all associated cust_bill_pay records.
898 If option 'unapplied' is a specified, only deletes until
899 this object's 'unapplied' value is >= the specified amount.
900 (Deletes in order returned by L</cust_bill_pay>.)
904 sub delete_cust_bill_pay {
908 local $SIG{HUP} = 'IGNORE';
909 local $SIG{INT} = 'IGNORE';
910 local $SIG{QUIT} = 'IGNORE';
911 local $SIG{TERM} = 'IGNORE';
912 local $SIG{TSTP} = 'IGNORE';
913 local $SIG{PIPE} = 'IGNORE';
915 my $oldAutoCommit = $FS::UID::AutoCommit;
916 local $FS::UID::AutoCommit = 0;
919 my $unapplied = $self->unapplied; #only need to look it up once
923 # Maybe we should reverse the order these get deleted in?
924 # ie delete newest first?
925 # keeping consistent with how bop refunds work, for now...
926 foreach my $cust_bill_pay ( $self->cust_bill_pay ) {
927 last if $opt{'unapplied'} && ($unapplied > $opt{'unapplied'});
928 $unapplied += $cust_bill_pay->amount;
929 $error = $cust_bill_pay->delete;
934 $dbh->rollback if $oldAutoCommit;
938 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
944 Accepts input for creating a new FS::cust_refund object.
945 Unapplies payment from invoices up to the amount of the refund,
946 creates the refund and applies payment to refund. Allows entire
947 process to be handled in one transaction.
949 Causes a fatal error if called on CARD or CHEK payments.
956 die "Cannot call cust_pay->refund on " . $self->payby
957 if grep { $_ eq $self->payby } qw(CARD CHEK);
959 local $SIG{HUP} = 'IGNORE';
960 local $SIG{INT} = 'IGNORE';
961 local $SIG{QUIT} = 'IGNORE';
962 local $SIG{TERM} = 'IGNORE';
963 local $SIG{TSTP} = 'IGNORE';
964 local $SIG{PIPE} = 'IGNORE';
966 my $oldAutoCommit = $FS::UID::AutoCommit;
967 local $FS::UID::AutoCommit = 0;
970 my $error = $self->delete_cust_bill_pay('amount' => $hash->{'amount'});
973 $dbh->rollback if $oldAutoCommit;
977 $hash->{'paynum'} = $self->paynum;
978 my $new = new FS::cust_refund ( $hash );
979 $error = $new->insert;
982 $dbh->rollback if $oldAutoCommit;
986 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
990 ### refund_to_unapply/unapply_refund false laziness with FS::cust_credit
992 =item refund_to_unapply
994 Returns L<FS::cust_pay_refund> objects that will be deleted by L</unapply_refund>
995 (all currently applied refunds that aren't closed.)
996 Returns empty list if payment itself is closed.
1000 sub refund_to_unapply {
1002 return () if $self->closed;
1004 'table' => 'cust_pay_refund',
1005 'hashref' => { 'paynum' => $self->paynum },
1006 'addl_from' => 'LEFT JOIN cust_refund USING (refundnum)',
1007 'extra_sql' => "AND cust_refund.closed IS NULL AND cust_refund.source_paynum IS NULL",
1011 =item unapply_refund
1013 Deletes all objects returned by L</refund_to_unapply>.
1017 sub unapply_refund {
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;
1030 foreach my $cust_pay_refund ($self->refund_to_unapply) {
1031 my $error = $cust_pay_refund->delete;
1033 dbh->rollback if $oldAutoCommit;
1038 dbh->commit or die dbh->errstr if $oldAutoCommit;
1044 =head1 CLASS METHODS
1048 =item batch_insert CUST_PAY_OBJECT, ...
1050 Class method which inserts multiple payments. Takes a list of FS::cust_pay
1051 objects. Returns a list, each element representing the status of inserting the
1052 corresponding payment - empty. If there is an error inserting any payment, the
1053 entire transaction is rolled back, i.e. all payments are inserted or none are.
1055 FS::cust_pay objects may have the pseudo-field 'apply_to', containing a
1056 reference to an array of (uninserted) FS::cust_bill_pay objects. If so,
1057 those objects will be inserted with the paynum of the payment, and for
1058 each one, an error message or an empty string will be inserted into the
1063 my @errors = FS::cust_pay->batch_insert(@cust_pay);
1064 my $num_errors = scalar(grep $_, @errors);
1065 if ( $num_errors == 0 ) {
1066 #success; all payments were inserted
1068 #failure; no payments were inserted.
1074 my $self = shift; #class method
1076 local $SIG{HUP} = 'IGNORE';
1077 local $SIG{INT} = 'IGNORE';
1078 local $SIG{QUIT} = 'IGNORE';
1079 local $SIG{TERM} = 'IGNORE';
1080 local $SIG{TSTP} = 'IGNORE';
1081 local $SIG{PIPE} = 'IGNORE';
1083 my $oldAutoCommit = $FS::UID::AutoCommit;
1084 local $FS::UID::AutoCommit = 0;
1090 foreach my $cust_pay (@_) {
1091 my $error = $cust_pay->insert( 'manual' => 1 );
1092 push @errors, $error;
1093 $num_errors++ if $error;
1095 if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
1097 foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
1098 if ( $error ) { # insert placeholders if cust_pay wasn't inserted
1102 $cust_bill_pay->set('paynum', $cust_pay->paynum);
1103 my $apply_error = $cust_bill_pay->insert;
1104 push @errors, $apply_error || '';
1105 $num_errors++ if $apply_error;
1109 } elsif ( !$error ) { #normal case: apply payments as usual
1110 $cust_pay->cust_main->apply_payments( 'manual'=>1 );
1115 if ( $num_errors ) {
1116 $dbh->rollback if $oldAutoCommit;
1118 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1127 Returns an SQL fragment to retreive the unapplied amount.
1132 my ($class, $start, $end) = @_;
1133 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
1134 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
1135 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
1136 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
1140 ( SELECT SUM(amount) FROM cust_bill_pay
1141 WHERE cust_pay.paynum = cust_bill_pay.paynum
1142 $bill_start $bill_end )
1146 ( SELECT SUM(amount) FROM cust_pay_refund
1147 WHERE cust_pay.paynum = cust_pay_refund.paynum
1148 $refund_start $refund_end )
1160 #my( $self, %opt ) = @_;
1163 +{ 'paynum' => $self->paynum,
1164 '_date' => $self->_date,
1165 'date' => time2str("%b %o, %Y", $self->_date),
1166 'date_short' => time2str("%m-%d-%Y", $self->_date),
1167 'paid' => sprintf('%.2f', $self->paid),
1168 'payby' => $self->payby,
1169 'paycardtype' => $self->paycardtype,
1170 'paymask' => $self->paymask,
1171 'processor' => $self->processor,
1172 'auth' => $self->auth,
1173 'order_number' => $self->order_number,
1181 # Used by FS::Upgrade to migrate to a new database.
1185 sub _upgrade_data { #class method
1186 my ($class, %opt) = @_;
1188 warn "$me upgrading $class\n" if $DEBUG;
1190 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1193 # otaker/ivan upgrade
1196 unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
1198 #not the most efficient, but hey, it only has to run once
1200 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
1201 " AND usernum IS NULL ".
1202 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
1203 " WHERE cust_main.custnum = cust_pay.custnum ) ";
1205 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
1207 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
1208 $sth->execute or die $sth->errstr;
1209 my $total = $sth->fetchrow_arrayref->[0];
1210 #warn "$total cust_pay records to update\n"
1212 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
1217 my @cust_pay = qsearch( {
1218 'table' => 'cust_pay',
1220 'extra_sql' => $where,
1221 'order_by' => 'ORDER BY paynum',
1224 foreach my $cust_pay (@cust_pay) {
1226 my $h_cust_pay = $cust_pay->h_search('insert');
1227 if ( $h_cust_pay ) {
1228 next if $cust_pay->otaker eq $h_cust_pay->history_user;
1229 #$cust_pay->otaker($h_cust_pay->history_user);
1230 $cust_pay->set('otaker', $h_cust_pay->history_user);
1232 $cust_pay->set('otaker', 'legacy');
1235 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1236 my $error = $cust_pay->replace;
1239 warn " *** WARNING: Error updating order taker for payment paynum ".
1240 $cust_pay->paynun. ": $error\n";
1244 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1247 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
1248 warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
1254 FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
1258 # payinfo N/A upgrade
1261 unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
1263 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
1265 my @na_cust_pay = qsearch( {
1266 'table' => 'cust_pay',
1267 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
1268 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
1271 foreach my $na ( @na_cust_pay ) {
1273 next unless $na->payinfo eq 'N/A';
1275 my $cust_pay_pending =
1276 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
1277 unless ( $cust_pay_pending ) {
1278 warn " *** WARNING: not-yet recoverable N/A card for payment ".
1279 $na->paynum. " (no cust_pay_pending)\n";
1282 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
1283 my $error = $na->replace;
1285 warn " *** WARNING: Error updating payinfo for payment paynum ".
1286 $na->paynun. ": $error\n";
1292 FS::upgrade_journal->set_done('cust_pay__payinfo_na');
1296 # otaker->usernum upgrade
1299 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
1300 $class->_upgrade_otaker(%opt);
1301 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
1303 # if we do this anywhere else, it should become an FS::Upgrade method
1304 my $num_to_upgrade = $class->count('paybatch is not null');
1305 my $num_jobs = FS::queue->count('job = \'FS::cust_pay::process_upgrade_paybatch\' and status != \'failed\'');
1306 if ( $num_to_upgrade > 0 ) {
1307 warn "Need to migrate paybatch field in $num_to_upgrade payments.\n";
1308 if ( $opt{queue} ) {
1309 if ( $num_jobs > 0 ) {
1310 warn "Upgrade already queued.\n";
1312 warn "Scheduling upgrade.\n";
1313 my $job = FS::queue->new({ job => 'FS::cust_pay::process_upgrade_paybatch' });
1317 process_upgrade_paybatch();
1322 # don't set paycardtype until 4.x
1324 #$class->upgrade_set_cardtype;
1326 # for batch payments, make sure paymask is set
1328 local $FS::payinfo_Mixin::allow_closed_replace = 1;
1329 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1331 my $cursor = FS::Cursor->new({
1332 table => 'cust_pay',
1333 extra_sql => ' WHERE paymask IS NULL AND payinfo IS NOT NULL
1334 AND payby IN(\'CARD\', \'CHEK\')
1335 AND batchnum IS NOT NULL',
1338 # records from cursors for some reason don't decrypt payinfo, so
1339 # call replace_old to fetch the record "normally"
1340 while (my $cust_pay = $cursor->fetch) {
1341 $cust_pay = $cust_pay->replace_old;
1342 $cust_pay->set('paymask', $cust_pay->mask_payinfo);
1343 my $error = $cust_pay->replace;
1345 die "$error (setting masked payinfo on payment#". $cust_pay->paynum.
1352 sub process_upgrade_paybatch {
1354 local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
1355 local $FS::UID::AutoCommit = 1;
1358 # migrate batchnums from the misused 'paybatch' field to 'batchnum'
1360 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
1361 my $search = FS::Cursor->new( {
1362 'table' => 'cust_pay',
1363 'addl_from' => " JOIN pay_batch ON cust_pay.paybatch = CAST(pay_batch.batchnum AS $text) ",
1365 while (my $cust_pay = $search->fetch) {
1366 $cust_pay->set('batchnum' => $cust_pay->paybatch);
1367 $cust_pay->set('paybatch' => '');
1368 my $error = $cust_pay->replace;
1369 warn "error setting batchnum on cust_pay #".$cust_pay->paynum.":\n $error"
1374 # migrate gateway info from the misused 'paybatch' field
1377 # not only cust_pay, but also voided and refunded payments
1378 if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
1379 local $FS::Record::nowarn_classload=1;
1380 # really inefficient, but again, only has to run once
1381 foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
1382 my $and_batchnum_is_null =
1383 ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
1384 my $pkey = ($table =~ /^cust_pay/ ? 'paynum' : 'refundnum');
1385 my $search = FS::Cursor->new({
1387 extra_sql => "WHERE payby IN('CARD','CHEK') ".
1388 "AND (paybatch IS NOT NULL ".
1389 "OR (paybatch IS NULL AND auth IS NULL
1390 $and_batchnum_is_null ) )
1391 ORDER BY $pkey DESC"
1393 while ( my $object = $search->fetch ) {
1394 if ( $object->paybatch eq '' ) {
1395 # repair for a previous upgrade that didn't save 'auth'
1396 my $pkey = $object->primary_key;
1397 # find the last history record that had a paybatch value
1399 table => "h_$table",
1401 $pkey => $object->$pkey,
1402 paybatch => { op=>'!=', value=>''},
1403 history_action => 'replace_old',
1405 order_by => 'ORDER BY history_date DESC LIMIT 1',
1408 warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
1411 # if the paybatch didn't have an auth string, then it's fine
1412 $h->paybatch =~ /:(\w+):/ or next;
1413 # set paybatch to what it was in that record
1414 $object->set('paybatch', $h->paybatch)
1415 # and then upgrade it like the old records
1418 my $parsed = $object->_parse_paybatch;
1419 if (keys %$parsed) {
1420 $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
1421 $object->set('auth' => $parsed->{authorization});
1422 $object->set('paybatch', '');
1423 my $error = $object->replace;
1424 warn "error parsing CARD/CHEK paybatch fields on $object #".
1425 $object->get($object->primary_key).":\n $error\n"
1430 FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
1440 =item process_batch_import
1444 sub process_batch_import {
1449 my $custnum = $hash{'custnum'};
1450 my $agentnum = $hash{'agentnum'};
1451 my $agent_custid = $hash{'agent_custid'};
1453 $hash{'_date'} = parse_datetime($hash{'_date'})
1454 if $hash{'_date'} && $hash{'_date'} =~ /\D/;
1455 #remove custnum_prefix
1456 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1457 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1460 && $custnum =~ /^$custnum_prefix(0*([1-9]\d*))$/
1461 && length($1) == $custnum_length
1465 # check agentnum against custnum and
1466 # translate agent_custid into regular custnum
1467 if ($custnum && $agent_custid) {
1468 die "can't specify both custnum and agent_custid\n";
1469 } elsif ($agentnum || $agent_custid) {
1470 # here is the agent virtualization
1471 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1473 $search{'agentnum'} = $agentnum
1475 $search{'agent_custid'} = $agent_custid
1477 $search{'custnum'} = $custnum
1479 my $cust_main = qsearchs({
1480 'table' => 'cust_main',
1481 'hashref' => \%search,
1482 'extra_sql' => $extra_sql,
1484 die "can't find customer with" .
1485 ($agentnum ? " agentnum $agentnum" : '') .
1486 ($custnum ? " custnum $custnum" : '') .
1487 ($agent_custid ? " agent_custid $agent_custid" : '') . "\n"
1489 die "mismatched customer number\n"
1490 if $custnum && ($custnum ne $cust_main->custnum);
1491 $custnum = $cust_main->custnum;
1493 $hash{'custnum'} = $custnum;
1494 delete($hash{'agent_custid'});
1499 'table' => 'cust_pay',
1500 'params' => [ '_date', 'agentnum', 'payby', 'paybatch' ],
1501 #agent_custid isn't a cust_pay field, see hash callback
1502 'formats' => { 'simple' =>
1503 [ qw(custnum agent_custid paid payinfo invnum) ] },
1504 'format_types' => { 'simple' => '' }, #force infer from file extension
1505 'default_csv' => 1, #if not .xls, will read as csv, regardless of extension
1506 'format_hash_callbacks' => { 'simple' => $hashcb },
1507 'insert_args_callback' => sub { ( 'manual'=>1 ); },
1508 'postinsert_callback' => sub {
1509 my $cust_pay = shift;
1510 my $cust_main = $cust_pay->cust_main
1511 or return "can't find customer to which payments apply";
1512 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
1514 ? "can't apply payments to customer ".$cust_pay->custnum."$error"
1519 FS::Record::process_batch_import( $job, $opt, @_ );
1523 =item batch_import HASHREF
1525 Inserts new payments.
1532 my $fh = $param->{filehandle};
1533 my $format = $param->{'format'};
1535 my $agentnum = $param->{agentnum};
1536 my $_date = $param->{_date};
1537 $_date = parse_datetime($_date) if $_date && $_date =~ /\D/;
1538 my $paybatch = $param->{'paybatch'};
1540 my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
1541 my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
1543 # here is the agent virtualization
1544 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
1548 if ( $format eq 'simple' ) {
1549 @fields = qw( custnum agent_custid paid payinfo invnum );
1551 } elsif ( $format eq 'extended' ) {
1552 die "unimplemented\n";
1556 die "unknown format $format";
1559 eval "use Text::CSV_XS;";
1562 my $csv = new Text::CSV_XS;
1566 local $SIG{HUP} = 'IGNORE';
1567 local $SIG{INT} = 'IGNORE';
1568 local $SIG{QUIT} = 'IGNORE';
1569 local $SIG{TERM} = 'IGNORE';
1570 local $SIG{TSTP} = 'IGNORE';
1571 local $SIG{PIPE} = 'IGNORE';
1573 my $oldAutoCommit = $FS::UID::AutoCommit;
1574 local $FS::UID::AutoCommit = 0;
1578 while ( defined($line=<$fh>) ) {
1580 $csv->parse($line) or do {
1581 $dbh->rollback if $oldAutoCommit;
1582 return "can't parse: ". $csv->error_input();
1585 my @columns = $csv->fields();
1589 paybatch => $paybatch,
1591 $cust_pay{_date} = $_date if $_date;
1594 foreach my $field ( @fields ) {
1596 if ( $field eq 'agent_custid'
1598 && $columns[0] =~ /\S+/ )
1601 my $agent_custid = $columns[0];
1602 my %hash = ( 'agent_custid' => $agent_custid,
1603 'agentnum' => $agentnum,
1606 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1607 $dbh->rollback if $oldAutoCommit;
1608 return "can't specify custnum with agent_custid $agent_custid";
1611 $cust_main = qsearchs({
1612 'table' => 'cust_main',
1613 'hashref' => \%hash,
1614 'extra_sql' => $extra_sql,
1617 unless ( $cust_main ) {
1618 $dbh->rollback if $oldAutoCommit;
1619 return "can't find customer with agent_custid $agent_custid";
1623 $columns[0] = $cust_main->custnum;
1626 $cust_pay{$field} = shift @columns;
1629 if ( $custnum_prefix && $cust_pay{custnum} =~ /^$custnum_prefix(0*([1-9]\d*))$/
1630 && length($1) == $custnum_length ) {
1631 $cust_pay{custnum} = $2;
1634 my $custnum = $cust_pay{custnum};
1636 my $cust_pay = new FS::cust_pay( \%cust_pay );
1637 my $error = $cust_pay->insert;
1639 if ( ! $error && $cust_pay->custnum != $custnum ) {
1640 #invnum was defined, and ->insert set custnum to the customer for that
1641 #invoice, but it wasn't the one the import specified.
1642 $dbh->rollback if $oldAutoCommit;
1643 $error = "specified invoice #". $cust_pay{invnum}.
1644 " is for custnum ". $cust_pay->custnum.
1645 ", not specified custnum $custnum";
1649 $dbh->rollback if $oldAutoCommit;
1650 return "can't insert payment for $line: $error";
1653 if ( $format eq 'simple' ) {
1654 # include agentnum for less surprise?
1655 $cust_main = qsearchs({
1656 'table' => 'cust_main',
1657 'hashref' => { 'custnum' => $cust_pay->custnum },
1658 'extra_sql' => $extra_sql,
1662 unless ( $cust_main ) {
1663 $dbh->rollback if $oldAutoCommit;
1664 return "can't find customer to which payments apply at line: $line";
1667 $error = $cust_main->apply_payments_and_credits;
1669 $dbh->rollback if $oldAutoCommit;
1670 return "can't apply payments to customer for $line: $error";
1678 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1680 return "Empty file!" unless $imported;
1690 Delete and replace methods.
1694 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1695 schema.html from the base documentation.