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 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
413 # # UNIQUE index should catch this too, without race conditions, but this
414 # # should give a better error message the other 99.9% of the time...
415 # if ( length($self->payunique)
416 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
417 # #well, it *could* be a better error message
418 # return "duplicate transaction".
419 # " - a payment with unique identifer ". $self->payunique.
426 =item batch_insert CUST_PAY_OBJECT, ...
428 Class method which inserts multiple payments. Takes a list of FS::cust_pay
429 objects. Returns a list, each element representing the status of inserting the
430 corresponding payment - empty. If there is an error inserting any payment, the
431 entire transaction is rolled back, i.e. all payments are inserted or none are.
435 my @errors = FS::cust_pay->batch_insert(@cust_pay);
436 my $num_errors = scalar(grep $_, @errors);
437 if ( $num_errors == 0 ) {
438 #success; all payments were inserted
440 #failure; no payments were inserted.
446 my $self = shift; #class method
448 local $SIG{HUP} = 'IGNORE';
449 local $SIG{INT} = 'IGNORE';
450 local $SIG{QUIT} = 'IGNORE';
451 local $SIG{TERM} = 'IGNORE';
452 local $SIG{TSTP} = 'IGNORE';
453 local $SIG{PIPE} = 'IGNORE';
455 my $oldAutoCommit = $FS::UID::AutoCommit;
456 local $FS::UID::AutoCommit = 0;
462 my $error = $_->insert( 'manual' => 1 );
466 $_->cust_main->apply_payments;
472 $dbh->rollback if $oldAutoCommit;
474 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
483 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
490 sort { $a->_date <=> $b->_date
491 || $a->invnum <=> $b->invnum }
492 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
496 =item cust_pay_refund
498 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
503 sub cust_pay_refund {
505 sort { $a->_date <=> $b->_date }
506 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
513 Returns the amount of this payment that is still unapplied; which is
514 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
515 applications (see L<FS::cust_pay_refund>).
521 my $amount = $self->paid;
522 $amount -= $_->amount foreach ( $self->cust_bill_pay );
523 $amount -= $_->amount foreach ( $self->cust_pay_refund );
524 sprintf("%.2f", $amount );
529 Returns the amount of this payment that has not been refuned; which is
530 paid minus all refund applications (see L<FS::cust_pay_refund>).
536 my $amount = $self->paid;
537 $amount -= $_->amount foreach ( $self->cust_pay_refund );
538 sprintf("%.2f", $amount );
544 Returns the parent customer object (see L<FS::cust_main>).
550 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
555 Returns a name for the payby field.
561 FS::payby->shortname( $self->payby );
566 Returns a gatewaynum for the processing gateway.
570 Returns a name for the processing gateway.
574 Returns a name for the processing gateway.
578 Returns a name for the processing gateway.
582 sub gatewaynum { shift->_parse_paybatch->{'gatewaynum'}; }
583 sub processor { shift->_parse_paybatch->{'processor'}; }
584 sub authorization { shift->_parse_paybatch->{'authorization'}; }
585 sub order_number { shift->_parse_paybatch->{'order_number'}; }
587 #sucks that this stuff is in paybatch like this in the first place,
588 #but at least other code can start to use new field names
589 #(code nicked from FS::cust_main::realtime_refund_bop)
590 sub _parse_paybatch {
593 $self->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
595 #"Can't parse paybatch for paynum $options{'paynum'}: ".
596 # $cust_pay->paybatch;
598 my( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
600 if ( $gatewaynum ) { #gateway for the payment to be refunded
602 my $payment_gateway =
603 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
605 die "payment gateway $gatewaynum not found" #?
606 unless $payment_gateway;
608 $processor = $payment_gateway->gateway_module;
613 'gatewaynum' => $gatewaynum,
614 'processor' => $processor,
615 'authorization' => $auth,
616 'order_number' => $order_number,
629 Returns an SQL fragment to retreive the unapplied amount.
638 ( SELECT SUM(amount) FROM cust_bill_pay
639 WHERE cust_pay.paynum = cust_bill_pay.paynum )
643 ( SELECT SUM(amount) FROM cust_pay_refund
644 WHERE cust_pay.paynum = cust_pay_refund.paynum )
655 Delete and replace methods.
659 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
660 schema.html from the base documentation.