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
74 =item closed - books closed flag, empty or `Y'
84 Creates a new payment. To add the payment to the databse, see L<"insert">.
88 sub table { 'cust_pay'; }
89 sub cust_linked { $_[0]->cust_main_custnum; }
90 sub cust_unlinked_msg {
92 "WARNING: can't find cust_main.custnum ". $self->custnum.
93 ' (cust_pay.paynum '. $self->paynum. ')';
98 Adds this payment to the database.
100 For backwards-compatibility and convenience, if the additional field invnum
101 is defined, an FS::cust_bill_pay record for the full amount of the payment
102 will be created. In this case, custnum is optional. An hash of optional
103 arguments may be passed. Currently "manual" is supported. If true, a
104 payment receipt is sent instead of a statement when 'payment_receipt_email'
105 configuration option is set.
110 my ($self, %options) = @_;
112 local $SIG{HUP} = 'IGNORE';
113 local $SIG{INT} = 'IGNORE';
114 local $SIG{QUIT} = 'IGNORE';
115 local $SIG{TERM} = 'IGNORE';
116 local $SIG{TSTP} = 'IGNORE';
117 local $SIG{PIPE} = 'IGNORE';
119 my $oldAutoCommit = $FS::UID::AutoCommit;
120 local $FS::UID::AutoCommit = 0;
124 if ( $self->invnum ) {
125 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
127 $dbh->rollback if $oldAutoCommit;
128 return "Unknown cust_bill.invnum: ". $self->invnum;
130 $self->custnum($cust_bill->custnum );
134 my $error = $self->check;
135 return $error if $error;
137 my $cust_main = $self->cust_main;
138 my $old_balance = $cust_main->balance;
140 $error = $self->SUPER::insert;
142 $dbh->rollback if $oldAutoCommit;
143 return "error inserting $self: $error";
146 if ( $self->invnum ) {
147 my $cust_bill_pay = new FS::cust_bill_pay {
148 'invnum' => $self->invnum,
149 'paynum' => $self->paynum,
150 'amount' => $self->paid,
151 '_date' => $self->_date,
153 $error = $cust_bill_pay->insert;
155 if ( $ignore_noapply ) {
156 warn "warning: error inserting $cust_bill_pay: $error ".
157 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
159 $dbh->rollback if $oldAutoCommit;
160 return "error inserting $cust_bill_pay: $error";
165 if ( $self->paybatch =~ /^webui-/ ) {
166 my @cust_pay = qsearch('cust_pay', {
167 'custnum' => $self->custnum,
168 'paybatch' => $self->paybatch,
170 if ( scalar(@cust_pay) > 1 ) {
171 $dbh->rollback if $oldAutoCommit;
172 return "a payment with webui token ". $self->paybatch. " already exists";
176 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
178 #false laziness w/ cust_credit::insert
179 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
180 my @errors = $cust_main->unsuspend;
182 # side-fx with nested transactions? upstack rolls back?
183 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
189 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
191 #my $cust_main = $self->cust_main;
192 if ( $conf->exists('payment_receipt_email')
193 && grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list
196 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
199 if ( ( exists($options{'manual'}) && $options{'manual'} )
200 || ! $conf->exists('invoice_html_statement')
204 my $receipt_template = new Text::Template (
206 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
208 warn "can't create payment receipt template: $Text::Template::ERROR";
212 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
213 $cust_main->invoicing_list;
215 my $payby = $self->payby;
216 my $payinfo = $self->payinfo;
217 $payby =~ s/^BILL$/Check/ if $payinfo;
218 $payinfo = $self->paymask if $payby eq 'CARD' || $payby eq 'CHEK';
219 $payby =~ s/^CHEK$/Electronic check/;
222 'from' => $conf->config('invoice_from'), #??? well as good as any
223 'to' => \@invoicing_list,
224 'subject' => 'Payment receipt',
225 'body' => [ $receipt_template->fill_in( HASH => {
226 'date' => time2str("%a %B %o, %Y", $self->_date),
227 'name' => $cust_main->name,
228 'paynum' => $self->paynum,
229 'paid' => sprintf("%.2f", $self->paid),
230 'payby' => ucfirst(lc($payby)),
231 'payinfo' => $payinfo,
232 'balance' => $cust_main->balance,
238 my $queue = new FS::queue {
239 'paynum' => $self->paynum,
240 'job' => 'FS::cust_bill::queueable_email',
242 $error = $queue->insert(
243 'invnum' => $cust_bill->invnum,
244 'template' => 'statement',
250 warn "can't send payment receipt/statement: $error";
259 =item void [ REASON ]
261 Voids this payment: deletes the payment and all associated applications and
262 adds a record of the voided payment to the FS::cust_pay_void table.
269 local $SIG{HUP} = 'IGNORE';
270 local $SIG{INT} = 'IGNORE';
271 local $SIG{QUIT} = 'IGNORE';
272 local $SIG{TERM} = 'IGNORE';
273 local $SIG{TSTP} = 'IGNORE';
274 local $SIG{PIPE} = 'IGNORE';
276 my $oldAutoCommit = $FS::UID::AutoCommit;
277 local $FS::UID::AutoCommit = 0;
280 my $cust_pay_void = new FS::cust_pay_void ( {
281 map { $_ => $self->get($_) } $self->fields
283 $cust_pay_void->reason(shift) if scalar(@_);
284 my $error = $cust_pay_void->insert;
286 $dbh->rollback if $oldAutoCommit;
290 $error = $self->delete;
292 $dbh->rollback if $oldAutoCommit;
296 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
304 Unless the closed flag is set, deletes this payment and all associated
305 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
306 cases, you want to use the void method instead to leave a record of the
311 # very similar to FS::cust_credit::delete
314 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
316 local $SIG{HUP} = 'IGNORE';
317 local $SIG{INT} = 'IGNORE';
318 local $SIG{QUIT} = 'IGNORE';
319 local $SIG{TERM} = 'IGNORE';
320 local $SIG{TSTP} = 'IGNORE';
321 local $SIG{PIPE} = 'IGNORE';
323 my $oldAutoCommit = $FS::UID::AutoCommit;
324 local $FS::UID::AutoCommit = 0;
327 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
328 my $error = $app->delete;
330 $dbh->rollback if $oldAutoCommit;
335 my $error = $self->SUPER::delete(@_);
337 $dbh->rollback if $oldAutoCommit;
341 if ( $conf->config('deletepayments') ne '' ) {
343 my $cust_main = $self->cust_main;
345 my $error = send_email(
346 'from' => $conf->config('invoice_from'), #??? well as good as any
347 'to' => $conf->config('deletepayments'),
348 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
350 "This is an automatic message from your Freeside installation\n",
351 "informing you that the following payment has been deleted:\n",
353 'paynum: '. $self->paynum. "\n",
354 'custnum: '. $self->custnum.
355 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
356 'paid: $'. sprintf("%.2f", $self->paid). "\n",
357 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
358 'payby: '. $self->payby. "\n",
359 'payinfo: '. $self->paymask. "\n",
360 'paybatch: '. $self->paybatch. "\n",
365 $dbh->rollback if $oldAutoCommit;
366 return "can't send payment deletion notification: $error";
371 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
377 =item replace OLD_RECORD
379 You can, but probably shouldn't modify payments...
384 #return "Can't modify payment!"
386 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
387 $self->SUPER::replace(@_);
392 Checks all fields to make sure this is a valid payment. If there is an error,
393 returns the error, otherwise returns false. Called by the insert method.
401 $self->ut_numbern('paynum')
402 || $self->ut_numbern('custnum')
403 || $self->ut_money('paid')
404 || $self->ut_numbern('_date')
405 || $self->ut_textn('paybatch')
406 || $self->ut_textn('payunique')
407 || $self->ut_enum('closed', [ '', 'Y' ])
408 || $self->payinfo_check()
410 return $error if $error;
412 return "paid must be > 0 " if $self->paid <= 0;
414 return "unknown cust_main.custnum: ". $self->custnum
416 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
418 $self->_date(time) unless $self->_date;
420 # UNIQUE index should catch this too, without race conditions, but this
421 # should give a better error message the other 99.9% of the time...
422 if ( length($self->payunique)
423 && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
424 return "duplicate transaction"; #well, it *could* be a better error message
430 =item batch_insert CUST_PAY_OBJECT, ...
432 Class method which inserts multiple payments. Takes a list of FS::cust_pay
433 objects. Returns a list, each element representing the status of inserting the
434 corresponding payment - empty. If there is an error inserting any payment, the
435 entire transaction is rolled back, i.e. all payments are inserted or none are.
439 my @errors = FS::cust_pay->batch_insert(@cust_pay);
440 my $num_errors = scalar(grep $_, @errors);
441 if ( $num_errors == 0 ) {
442 #success; all payments were inserted
444 #failure; no payments were inserted.
450 my $self = shift; #class method
452 local $SIG{HUP} = 'IGNORE';
453 local $SIG{INT} = 'IGNORE';
454 local $SIG{QUIT} = 'IGNORE';
455 local $SIG{TERM} = 'IGNORE';
456 local $SIG{TSTP} = 'IGNORE';
457 local $SIG{PIPE} = 'IGNORE';
459 my $oldAutoCommit = $FS::UID::AutoCommit;
460 local $FS::UID::AutoCommit = 0;
466 my $error = $_->insert( 'manual' => 1 );
470 $_->cust_main->apply_payments;
476 $dbh->rollback if $oldAutoCommit;
478 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
487 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
494 sort { $a->_date <=> $b->_date
495 || $a->invnum <=> $b->invnum }
496 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
500 =item cust_pay_refund
502 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
507 sub cust_pay_refund {
509 sort { $a->_date <=> $b->_date }
510 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
517 Returns the amount of this payment that is still unapplied; which is
518 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
519 applications (see L<FS::cust_pay_refund>).
525 my $amount = $self->paid;
526 $amount -= $_->amount foreach ( $self->cust_bill_pay );
527 $amount -= $_->amount foreach ( $self->cust_pay_refund );
528 sprintf("%.2f", $amount );
533 Returns the amount of this payment that has not been refuned; which is
534 paid minus all refund applications (see L<FS::cust_pay_refund>).
540 my $amount = $self->paid;
541 $amount -= $_->amount foreach ( $self->cust_pay_refund );
542 sprintf("%.2f", $amount );
548 Returns the parent customer object (see L<FS::cust_main>).
554 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
561 Delete and replace methods.
565 L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>, schema.html from the