4 use vars qw( @ISA $conf $unsuspendauto $ignore_noapply @encrypted_fields );
6 use Business::CreditCard;
8 use FS::Misc qw(send_email);
9 use FS::Record qw( dbh qsearch qsearchs );
11 use FS::cust_main_Mixin;
12 use FS::payinfo_Mixin;
14 use FS::cust_bill_pay;
15 use FS::cust_pay_refund;
17 use FS::cust_pay_void;
19 @ISA = qw(FS::Record FS::cust_main_Mixin FS::payinfo_Mixin );
23 #ask FS::UID to run this stuff for us later
24 FS::UID->install_callback( sub {
26 $unsuspendauto = $conf->exists('unsuspendauto');
29 @encrypted_fields = ('payinfo');
33 FS::cust_pay - Object methods for cust_pay objects
39 $record = new FS::cust_pay \%hash;
40 $record = new FS::cust_pay { 'column' => 'value' };
42 $error = $record->insert;
44 $error = $new_record->replace($old_record);
46 $error = $record->delete;
48 $error = $record->check;
52 An FS::cust_pay object represents a payment; the transfer of money from a
53 customer. FS::cust_pay inherits from FS::Record. The following fields are
58 =item paynum - primary key (assigned automatically for new payments)
60 =item custnum - customer (see L<FS::cust_main>)
62 =item paid - Amount of this payment
64 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
65 L<Time::Local> and L<Date::Parse> for conversion functions.
67 =item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
69 =item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
71 =item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
73 =item paybatch - text field for tracking card processing or other batch grouping
75 =item payunique - Optional unique identifer to prevent duplicate transactions.
77 =item closed - books closed flag, empty or `Y'
87 Creates a new payment. To add the payment to the databse, see L<"insert">.
91 sub table { 'cust_pay'; }
92 sub cust_linked { $_[0]->cust_main_custnum; }
93 sub cust_unlinked_msg {
95 "WARNING: can't find cust_main.custnum ". $self->custnum.
96 ' (cust_pay.paynum '. $self->paynum. ')';
101 Adds this payment to the database.
103 For backwards-compatibility and convenience, if the additional field invnum
104 is defined, an FS::cust_bill_pay record for the full amount of the payment
105 will be created. In this case, custnum is optional. An hash of optional
106 arguments may be passed. Currently "manual" is supported. If true, a
107 payment receipt is sent instead of a statement when 'payment_receipt_email'
108 configuration option is set.
113 my ($self, %options) = @_;
115 local $SIG{HUP} = 'IGNORE';
116 local $SIG{INT} = 'IGNORE';
117 local $SIG{QUIT} = 'IGNORE';
118 local $SIG{TERM} = 'IGNORE';
119 local $SIG{TSTP} = 'IGNORE';
120 local $SIG{PIPE} = 'IGNORE';
122 my $oldAutoCommit = $FS::UID::AutoCommit;
123 local $FS::UID::AutoCommit = 0;
127 if ( $self->invnum ) {
128 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
130 $dbh->rollback if $oldAutoCommit;
131 return "Unknown cust_bill.invnum: ". $self->invnum;
133 $self->custnum($cust_bill->custnum );
137 my $error = $self->check;
138 return $error if $error;
140 my $cust_main = $self->cust_main;
141 my $old_balance = $cust_main->balance;
143 $error = $self->SUPER::insert;
145 $dbh->rollback if $oldAutoCommit;
146 return "error inserting $self: $error";
149 if ( $self->invnum ) {
150 my $cust_bill_pay = new FS::cust_bill_pay {
151 'invnum' => $self->invnum,
152 'paynum' => $self->paynum,
153 'amount' => $self->paid,
154 '_date' => $self->_date,
156 $error = $cust_bill_pay->insert;
158 if ( $ignore_noapply ) {
159 warn "warning: error inserting $cust_bill_pay: $error ".
160 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
162 $dbh->rollback if $oldAutoCommit;
163 return "error inserting $cust_bill_pay: $error";
168 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
170 #false laziness w/ cust_credit::insert
171 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
172 my @errors = $cust_main->unsuspend;
174 # side-fx with nested transactions? upstack rolls back?
175 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
181 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
183 #my $cust_main = $self->cust_main;
184 if ( $conf->exists('payment_receipt_email')
185 && grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list
188 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
191 if ( ( exists($options{'manual'}) && $options{'manual'} )
192 || ! $conf->exists('invoice_html_statement')
196 my $receipt_template = new Text::Template (
198 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
200 warn "can't create payment receipt template: $Text::Template::ERROR";
204 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
205 $cust_main->invoicing_list;
207 my $payby = $self->payby;
208 my $payinfo = $self->payinfo;
209 $payby =~ s/^BILL$/Check/ if $payinfo;
210 $payinfo = $self->paymask if $payby eq 'CARD' || $payby eq 'CHEK';
211 $payby =~ s/^CHEK$/Electronic check/;
214 'from' => $conf->config('invoice_from'), #??? well as good as any
215 'to' => \@invoicing_list,
216 'subject' => 'Payment receipt',
217 'body' => [ $receipt_template->fill_in( HASH => {
218 'date' => time2str("%a %B %o, %Y", $self->_date),
219 'name' => $cust_main->name,
220 'paynum' => $self->paynum,
221 'paid' => sprintf("%.2f", $self->paid),
222 'payby' => ucfirst(lc($payby)),
223 'payinfo' => $payinfo,
224 'balance' => $cust_main->balance,
230 my $queue = new FS::queue {
231 'paynum' => $self->paynum,
232 'job' => 'FS::cust_bill::queueable_email',
234 $error = $queue->insert(
235 'invnum' => $cust_bill->invnum,
236 'template' => 'statement',
242 warn "can't send payment receipt/statement: $error";
251 =item void [ REASON ]
253 Voids this payment: deletes the payment and all associated applications and
254 adds a record of the voided payment to the FS::cust_pay_void table.
261 local $SIG{HUP} = 'IGNORE';
262 local $SIG{INT} = 'IGNORE';
263 local $SIG{QUIT} = 'IGNORE';
264 local $SIG{TERM} = 'IGNORE';
265 local $SIG{TSTP} = 'IGNORE';
266 local $SIG{PIPE} = 'IGNORE';
268 my $oldAutoCommit = $FS::UID::AutoCommit;
269 local $FS::UID::AutoCommit = 0;
272 my $cust_pay_void = new FS::cust_pay_void ( {
273 map { $_ => $self->get($_) } $self->fields
275 $cust_pay_void->reason(shift) if scalar(@_);
276 my $error = $cust_pay_void->insert;
278 $dbh->rollback if $oldAutoCommit;
282 $error = $self->delete;
284 $dbh->rollback if $oldAutoCommit;
288 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
296 Unless the closed flag is set, deletes this payment and all associated
297 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
298 cases, you want to use the void method instead to leave a record of the
303 # very similar to FS::cust_credit::delete
306 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
308 local $SIG{HUP} = 'IGNORE';
309 local $SIG{INT} = 'IGNORE';
310 local $SIG{QUIT} = 'IGNORE';
311 local $SIG{TERM} = 'IGNORE';
312 local $SIG{TSTP} = 'IGNORE';
313 local $SIG{PIPE} = 'IGNORE';
315 my $oldAutoCommit = $FS::UID::AutoCommit;
316 local $FS::UID::AutoCommit = 0;
319 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
320 my $error = $app->delete;
322 $dbh->rollback if $oldAutoCommit;
327 my $error = $self->SUPER::delete(@_);
329 $dbh->rollback if $oldAutoCommit;
333 if ( $conf->config('deletepayments') ne '' ) {
335 my $cust_main = $self->cust_main;
337 my $error = send_email(
338 'from' => $conf->config('invoice_from'), #??? well as good as any
339 'to' => $conf->config('deletepayments'),
340 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
342 "This is an automatic message from your Freeside installation\n",
343 "informing you that the following payment has been deleted:\n",
345 'paynum: '. $self->paynum. "\n",
346 'custnum: '. $self->custnum.
347 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
348 'paid: $'. sprintf("%.2f", $self->paid). "\n",
349 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
350 'payby: '. $self->payby. "\n",
351 'payinfo: '. $self->paymask. "\n",
352 'paybatch: '. $self->paybatch. "\n",
357 $dbh->rollback if $oldAutoCommit;
358 return "can't send payment deletion notification: $error";
363 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
369 =item replace OLD_RECORD
371 You can, but probably shouldn't modify payments...
376 #return "Can't modify payment!"
378 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
379 $self->SUPER::replace(@_);
384 Checks all fields to make sure this is a valid payment. If there is an error,
385 returns the error, otherwise returns false. Called by the insert method.
393 $self->ut_numbern('paynum')
394 || $self->ut_numbern('custnum')
395 || $self->ut_money('paid')
396 || $self->ut_numbern('_date')
397 || $self->ut_textn('paybatch')
398 || $self->ut_textn('payunique')
399 || $self->ut_enum('closed', [ '', 'Y' ])
400 || $self->payinfo_check()
402 return $error if $error;
404 return "paid must be > 0 " if $self->paid <= 0;
406 return "unknown cust_main.custnum: ". $self->custnum
408 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
410 $self->_date(time) unless $self->_date;
412 # UNIQUE index should catch this too, without race conditions, but this
413 # should give a better error message the other 99.9% of the time...
414 if ( length($self->payunique)
415 && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
416 #well, it *could* be a better error message
417 return "duplicate transaction".
418 " - a payment with unique identifer ". $self->payunique.
425 =item batch_insert CUST_PAY_OBJECT, ...
427 Class method which inserts multiple payments. Takes a list of FS::cust_pay
428 objects. Returns a list, each element representing the status of inserting the
429 corresponding payment - empty. If there is an error inserting any payment, the
430 entire transaction is rolled back, i.e. all payments are inserted or none are.
434 my @errors = FS::cust_pay->batch_insert(@cust_pay);
435 my $num_errors = scalar(grep $_, @errors);
436 if ( $num_errors == 0 ) {
437 #success; all payments were inserted
439 #failure; no payments were inserted.
445 my $self = shift; #class method
447 local $SIG{HUP} = 'IGNORE';
448 local $SIG{INT} = 'IGNORE';
449 local $SIG{QUIT} = 'IGNORE';
450 local $SIG{TERM} = 'IGNORE';
451 local $SIG{TSTP} = 'IGNORE';
452 local $SIG{PIPE} = 'IGNORE';
454 my $oldAutoCommit = $FS::UID::AutoCommit;
455 local $FS::UID::AutoCommit = 0;
461 my $error = $_->insert( 'manual' => 1 );
465 $_->cust_main->apply_payments;
471 $dbh->rollback if $oldAutoCommit;
473 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
482 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
489 sort { $a->_date <=> $b->_date
490 || $a->invnum <=> $b->invnum }
491 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
495 =item cust_pay_refund
497 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
502 sub cust_pay_refund {
504 sort { $a->_date <=> $b->_date }
505 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
512 Returns the amount of this payment that is still unapplied; which is
513 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
514 applications (see L<FS::cust_pay_refund>).
520 my $amount = $self->paid;
521 $amount -= $_->amount foreach ( $self->cust_bill_pay );
522 $amount -= $_->amount foreach ( $self->cust_pay_refund );
523 sprintf("%.2f", $amount );
528 Returns the amount of this payment that has not been refuned; which is
529 paid minus all refund applications (see L<FS::cust_pay_refund>).
535 my $amount = $self->paid;
536 $amount -= $_->amount foreach ( $self->cust_pay_refund );
537 sprintf("%.2f", $amount );
543 Returns the parent customer object (see L<FS::cust_main>).
549 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
554 Returns a name for the payby field.
560 FS::payby->shortname( $self->payby );
565 Returns a gatewaynum for the processing gateway.
569 Returns a name for the processing gateway.
573 Returns a name for the processing gateway.
577 Returns a name for the processing gateway.
581 sub gatewaynum { shift->_parse_paybatch->{'gatewaynum'}; }
582 sub processor { shift->_parse_paybatch->{'processor'}; }
583 sub authorization { shift->_parse_paybatch->{'authorization'}; }
584 sub order_number { shift->_parse_paybatch->{'order_number'}; }
586 #sucks that this stuff is in paybatch like this in the first place,
587 #but at least other code can start to use new field names
588 #(code nicked from FS::cust_main::realtime_refund_bop)
589 sub _parse_paybatch {
592 $self->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
594 #"Can't parse paybatch for paynum $options{'paynum'}: ".
595 # $cust_pay->paybatch;
597 my( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
599 if ( $gatewaynum ) { #gateway for the payment to be refunded
601 my $payment_gateway =
602 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
604 die "payment gateway $gatewaynum not found" #?
605 unless $payment_gateway;
607 $processor = $payment_gateway->gateway_module;
612 'gatewaynum' => $gatewaynum,
613 'processor' => $processor,
614 'authorization' => $auth,
615 'order_number' => $order_number,
628 Returns an SQL fragment to retreive the unapplied amount.
637 ( SELECT SUM(amount) FROM cust_bill_pay
638 WHERE cust_pay.paynum = cust_bill_pay.paynum )
642 ( SELECT SUM(amount) FROM cust_pay_refund
643 WHERE cust_pay.paynum = cust_pay_refund.paynum )
654 Delete and replace methods.
658 L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>, schema.html from the