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 );
10 use FS::cust_main_Mixin;
11 use FS::payinfo_Mixin;
13 use FS::cust_bill_pay;
14 use FS::cust_pay_refund;
16 use FS::cust_pay_void;
18 @ISA = qw(FS::Record FS::cust_main_Mixin FS::payinfo_Mixin );
22 #ask FS::UID to run this stuff for us later
23 FS::UID->install_callback( sub {
25 $unsuspendauto = $conf->exists('unsuspendauto');
28 @encrypted_fields = ('payinfo');
32 FS::cust_pay - Object methods for cust_pay objects
38 $record = new FS::cust_pay \%hash;
39 $record = new FS::cust_pay { 'column' => 'value' };
41 $error = $record->insert;
43 $error = $new_record->replace($old_record);
45 $error = $record->delete;
47 $error = $record->check;
51 An FS::cust_pay object represents a payment; the transfer of money from a
52 customer. FS::cust_pay inherits from FS::Record. The following fields are
57 =item paynum - primary key (assigned automatically for new payments)
59 =item custnum - customer (see L<FS::cust_main>)
61 =item paid - Amount of this payment
63 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
64 L<Time::Local> and L<Date::Parse> for conversion functions.
66 =item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
68 =item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
70 =item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
72 =item paybatch - text field for tracking card processing or other batch grouping
74 =item payunique - Optional unique identifer to prevent duplicate transactions.
76 =item closed - books closed flag, empty or `Y'
86 Creates a new payment. To add the payment to the databse, see L<"insert">.
90 sub table { 'cust_pay'; }
91 sub cust_linked { $_[0]->cust_main_custnum; }
92 sub cust_unlinked_msg {
94 "WARNING: can't find cust_main.custnum ". $self->custnum.
95 ' (cust_pay.paynum '. $self->paynum. ')';
100 Adds this payment to the database.
102 For backwards-compatibility and convenience, if the additional field invnum
103 is defined, an FS::cust_bill_pay record for the full amount of the payment
104 will be created. In this case, custnum is optional. An hash of optional
105 arguments may be passed. Currently "manual" is supported. If true, a
106 payment receipt is sent instead of a statement when 'payment_receipt_email'
107 configuration option is set.
112 my ($self, %options) = @_;
114 local $SIG{HUP} = 'IGNORE';
115 local $SIG{INT} = 'IGNORE';
116 local $SIG{QUIT} = 'IGNORE';
117 local $SIG{TERM} = 'IGNORE';
118 local $SIG{TSTP} = 'IGNORE';
119 local $SIG{PIPE} = 'IGNORE';
121 my $oldAutoCommit = $FS::UID::AutoCommit;
122 local $FS::UID::AutoCommit = 0;
126 if ( $self->invnum ) {
127 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
129 $dbh->rollback if $oldAutoCommit;
130 return "Unknown cust_bill.invnum: ". $self->invnum;
132 $self->custnum($cust_bill->custnum );
136 my $error = $self->check;
137 return $error if $error;
139 my $cust_main = $self->cust_main;
140 my $old_balance = $cust_main->balance;
142 $error = $self->SUPER::insert;
144 $dbh->rollback if $oldAutoCommit;
145 return "error inserting $self: $error";
148 if ( $self->invnum ) {
149 my $cust_bill_pay = new FS::cust_bill_pay {
150 'invnum' => $self->invnum,
151 'paynum' => $self->paynum,
152 'amount' => $self->paid,
153 '_date' => $self->_date,
155 $error = $cust_bill_pay->insert;
157 if ( $ignore_noapply ) {
158 warn "warning: error inserting $cust_bill_pay: $error ".
159 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
161 $dbh->rollback if $oldAutoCommit;
162 return "error inserting $cust_bill_pay: $error";
167 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
169 #false laziness w/ cust_credit::insert
170 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
171 my @errors = $cust_main->unsuspend;
173 # side-fx with nested transactions? upstack rolls back?
174 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
180 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
182 #my $cust_main = $self->cust_main;
183 if ( $conf->exists('payment_receipt_email')
184 && grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list
187 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
190 if ( ( exists($options{'manual'}) && $options{'manual'} )
191 || ! $conf->exists('invoice_html_statement')
195 my $receipt_template = new Text::Template (
197 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
199 warn "can't create payment receipt template: $Text::Template::ERROR";
203 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
204 $cust_main->invoicing_list;
206 my $payby = $self->payby;
207 my $payinfo = $self->payinfo;
208 $payby =~ s/^BILL$/Check/ if $payinfo;
209 $payinfo = $self->paymask if $payby eq 'CARD' || $payby eq 'CHEK';
210 $payby =~ s/^CHEK$/Electronic check/;
213 'from' => $conf->config('invoice_from'), #??? well as good as any
214 'to' => \@invoicing_list,
215 'subject' => 'Payment receipt',
216 'body' => [ $receipt_template->fill_in( HASH => {
217 'date' => time2str("%a %B %o, %Y", $self->_date),
218 'name' => $cust_main->name,
219 'paynum' => $self->paynum,
220 'paid' => sprintf("%.2f", $self->paid),
221 'payby' => ucfirst(lc($payby)),
222 'payinfo' => $payinfo,
223 'balance' => $cust_main->balance,
229 my $queue = new FS::queue {
230 'paynum' => $self->paynum,
231 'job' => 'FS::cust_bill::queueable_email',
233 $error = $queue->insert(
234 'invnum' => $cust_bill->invnum,
235 'template' => 'statement',
241 warn "can't send payment receipt/statement: $error";
250 =item void [ REASON ]
252 Voids this payment: deletes the payment and all associated applications and
253 adds a record of the voided payment to the FS::cust_pay_void table.
260 local $SIG{HUP} = 'IGNORE';
261 local $SIG{INT} = 'IGNORE';
262 local $SIG{QUIT} = 'IGNORE';
263 local $SIG{TERM} = 'IGNORE';
264 local $SIG{TSTP} = 'IGNORE';
265 local $SIG{PIPE} = 'IGNORE';
267 my $oldAutoCommit = $FS::UID::AutoCommit;
268 local $FS::UID::AutoCommit = 0;
271 my $cust_pay_void = new FS::cust_pay_void ( {
272 map { $_ => $self->get($_) } $self->fields
274 $cust_pay_void->reason(shift) if scalar(@_);
275 my $error = $cust_pay_void->insert;
277 $dbh->rollback if $oldAutoCommit;
281 $error = $self->delete;
283 $dbh->rollback if $oldAutoCommit;
287 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
295 Unless the closed flag is set, deletes this payment and all associated
296 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
297 cases, you want to use the void method instead to leave a record of the
302 # very similar to FS::cust_credit::delete
305 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
307 local $SIG{HUP} = 'IGNORE';
308 local $SIG{INT} = 'IGNORE';
309 local $SIG{QUIT} = 'IGNORE';
310 local $SIG{TERM} = 'IGNORE';
311 local $SIG{TSTP} = 'IGNORE';
312 local $SIG{PIPE} = 'IGNORE';
314 my $oldAutoCommit = $FS::UID::AutoCommit;
315 local $FS::UID::AutoCommit = 0;
318 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
319 my $error = $app->delete;
321 $dbh->rollback if $oldAutoCommit;
326 my $error = $self->SUPER::delete(@_);
328 $dbh->rollback if $oldAutoCommit;
332 if ( $conf->config('deletepayments') ne '' ) {
334 my $cust_main = $self->cust_main;
336 my $error = send_email(
337 'from' => $conf->config('invoice_from'), #??? well as good as any
338 'to' => $conf->config('deletepayments'),
339 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
341 "This is an automatic message from your Freeside installation\n",
342 "informing you that the following payment has been deleted:\n",
344 'paynum: '. $self->paynum. "\n",
345 'custnum: '. $self->custnum.
346 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
347 'paid: $'. sprintf("%.2f", $self->paid). "\n",
348 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
349 'payby: '. $self->payby. "\n",
350 'payinfo: '. $self->paymask. "\n",
351 'paybatch: '. $self->paybatch. "\n",
356 $dbh->rollback if $oldAutoCommit;
357 return "can't send payment deletion notification: $error";
362 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
368 =item replace OLD_RECORD
370 You can, but probably shouldn't modify payments...
375 #return "Can't modify payment!"
377 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
378 $self->SUPER::replace(@_);
383 Checks all fields to make sure this is a valid payment. If there is an error,
384 returns the error, otherwise returns false. Called by the insert method.
392 $self->ut_numbern('paynum')
393 || $self->ut_numbern('custnum')
394 || $self->ut_money('paid')
395 || $self->ut_numbern('_date')
396 || $self->ut_textn('paybatch')
397 || $self->ut_textn('payunique')
398 || $self->ut_enum('closed', [ '', 'Y' ])
399 || $self->payinfo_check()
401 return $error if $error;
403 return "paid must be > 0 " if $self->paid <= 0;
405 return "unknown cust_main.custnum: ". $self->custnum
407 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
409 $self->_date(time) unless $self->_date;
411 # UNIQUE index should catch this too, without race conditions, but this
412 # should give a better error message the other 99.9% of the time...
413 if ( length($self->payunique)
414 && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
415 #well, it *could* be a better error message
416 return "duplicate transaction".
417 " - a payment with unique identifer ". $self->payunique.
424 =item batch_insert CUST_PAY_OBJECT, ...
426 Class method which inserts multiple payments. Takes a list of FS::cust_pay
427 objects. Returns a list, each element representing the status of inserting the
428 corresponding payment - empty. If there is an error inserting any payment, the
429 entire transaction is rolled back, i.e. all payments are inserted or none are.
433 my @errors = FS::cust_pay->batch_insert(@cust_pay);
434 my $num_errors = scalar(grep $_, @errors);
435 if ( $num_errors == 0 ) {
436 #success; all payments were inserted
438 #failure; no payments were inserted.
444 my $self = shift; #class method
446 local $SIG{HUP} = 'IGNORE';
447 local $SIG{INT} = 'IGNORE';
448 local $SIG{QUIT} = 'IGNORE';
449 local $SIG{TERM} = 'IGNORE';
450 local $SIG{TSTP} = 'IGNORE';
451 local $SIG{PIPE} = 'IGNORE';
453 my $oldAutoCommit = $FS::UID::AutoCommit;
454 local $FS::UID::AutoCommit = 0;
460 my $error = $_->insert( 'manual' => 1 );
464 $_->cust_main->apply_payments;
470 $dbh->rollback if $oldAutoCommit;
472 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
481 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
488 sort { $a->_date <=> $b->_date
489 || $a->invnum <=> $b->invnum }
490 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
494 =item cust_pay_refund
496 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
501 sub cust_pay_refund {
503 sort { $a->_date <=> $b->_date }
504 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
511 Returns the amount of this payment that is still unapplied; which is
512 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
513 applications (see L<FS::cust_pay_refund>).
519 my $amount = $self->paid;
520 $amount -= $_->amount foreach ( $self->cust_bill_pay );
521 $amount -= $_->amount foreach ( $self->cust_pay_refund );
522 sprintf("%.2f", $amount );
527 Returns the amount of this payment that has not been refuned; which is
528 paid minus all refund applications (see L<FS::cust_pay_refund>).
534 my $amount = $self->paid;
535 $amount -= $_->amount foreach ( $self->cust_pay_refund );
536 sprintf("%.2f", $amount );
542 Returns the parent customer object (see L<FS::cust_main>).
548 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
559 Returns an SQL fragment to retreive the unapplied amount.
568 ( SELECT SUM(amount) FROM cust_bill_pay
569 WHERE cust_pay.paynum = cust_bill_pay.paynum )
573 ( SELECT SUM(amount) FROM cust_pay_refund
574 WHERE cust_pay.paynum = cust_pay_refund.paynum )
585 Delete and replace methods.
589 L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>, schema.html from the