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_enum('closed', [ '', 'Y' ])
407 || $self->payinfo_check()
409 return $error if $error;
411 return "paid must be > 0 " if $self->paid <= 0;
413 return "unknown cust_main.custnum: ". $self->custnum
415 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
417 $self->_date(time) unless $self->_date;
422 =item batch_insert CUST_PAY_OBJECT, ...
424 Class method which inserts multiple payments. Takes a list of FS::cust_pay
425 objects. Returns a list, each element representing the status of inserting the
426 corresponding payment - empty. If there is an error inserting any payment, the
427 entire transaction is rolled back, i.e. all payments are inserted or none are.
431 my @errors = FS::cust_pay->batch_insert(@cust_pay);
432 my $num_errors = scalar(grep $_, @errors);
433 if ( $num_errors == 0 ) {
434 #success; all payments were inserted
436 #failure; no payments were inserted.
442 my $self = shift; #class method
444 local $SIG{HUP} = 'IGNORE';
445 local $SIG{INT} = 'IGNORE';
446 local $SIG{QUIT} = 'IGNORE';
447 local $SIG{TERM} = 'IGNORE';
448 local $SIG{TSTP} = 'IGNORE';
449 local $SIG{PIPE} = 'IGNORE';
451 my $oldAutoCommit = $FS::UID::AutoCommit;
452 local $FS::UID::AutoCommit = 0;
458 my $error = $_->insert( 'manual' => 1 );
462 $_->cust_main->apply_payments;
468 $dbh->rollback if $oldAutoCommit;
470 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
479 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
486 sort { $a->_date <=> $b->_date
487 || $a->invnum <=> $b->invnum }
488 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
492 =item cust_pay_refund
494 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
499 sub cust_pay_refund {
501 sort { $a->_date <=> $b->_date }
502 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
509 Returns the amount of this payment that is still unapplied; which is
510 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
511 applications (see L<FS::cust_pay_refund>).
517 my $amount = $self->paid;
518 $amount -= $_->amount foreach ( $self->cust_bill_pay );
519 $amount -= $_->amount foreach ( $self->cust_pay_refund );
520 sprintf("%.2f", $amount );
525 Returns the amount of this payment that has not been refuned; which is
526 paid minus all refund applications (see L<FS::cust_pay_refund>).
532 my $amount = $self->paid;
533 $amount -= $_->amount foreach ( $self->cust_pay_refund );
534 sprintf("%.2f", $amount );
540 Returns the parent customer object (see L<FS::cust_main>).
546 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
553 Delete and replace methods.
557 L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>, schema.html from the