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 if ( exists($options{ 'manual' }) && $options{ 'manual' } ) {
198 my $receipt_template = new Text::Template (
200 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
202 warn "can't create payment receipt template: $Text::Template::ERROR";
206 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
207 $cust_main->invoicing_list;
209 my $payby = $self->payby;
210 my $payinfo = $self->payinfo;
211 $payby =~ s/^BILL$/Check/ if $payinfo;
212 $payinfo = $self->paymask if $payby eq 'CARD' || $payby eq 'CHEK';
213 $payby =~ s/^CHEK$/Electronic check/;
216 'from' => $conf->config('invoice_from'), #??? well as good as any
217 'to' => \@invoicing_list,
218 'subject' => 'Payment receipt',
219 'body' => [ $receipt_template->fill_in( HASH => {
220 'date' => time2str("%a %B %o, %Y", $self->_date),
221 'name' => $cust_main->name,
222 'paynum' => $self->paynum,
223 'paid' => sprintf("%.2f", $self->paid),
224 'payby' => ucfirst(lc($payby)),
225 'payinfo' => $payinfo,
226 'balance' => $cust_main->balance,
231 $cust_bill = ($cust_main->cust_bill)[-1];
234 my $queue = new FS::queue {
235 'paynum' => $self->paynum,
236 'job' => 'FS::cust_bill::queueable_send',
238 $error = $queue->insert(
239 'invnum' => $cust_bill->invnum,
240 'template' => 'statement',
245 warn "can't send payment receipt/statement: $error";
254 =item void [ REASON ]
256 Voids this payment: deletes the payment and all associated applications and
257 adds a record of the voided payment to the FS::cust_pay_void table.
264 local $SIG{HUP} = 'IGNORE';
265 local $SIG{INT} = 'IGNORE';
266 local $SIG{QUIT} = 'IGNORE';
267 local $SIG{TERM} = 'IGNORE';
268 local $SIG{TSTP} = 'IGNORE';
269 local $SIG{PIPE} = 'IGNORE';
271 my $oldAutoCommit = $FS::UID::AutoCommit;
272 local $FS::UID::AutoCommit = 0;
275 my $cust_pay_void = new FS::cust_pay_void ( {
276 map { $_ => $self->get($_) } $self->fields
278 $cust_pay_void->reason(shift) if scalar(@_);
279 my $error = $cust_pay_void->insert;
281 $dbh->rollback if $oldAutoCommit;
285 $error = $self->delete;
287 $dbh->rollback if $oldAutoCommit;
291 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
299 Unless the closed flag is set, deletes this payment and all associated
300 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
301 cases, you want to use the void method instead to leave a record of the
306 # very similar to FS::cust_credit::delete
309 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
311 local $SIG{HUP} = 'IGNORE';
312 local $SIG{INT} = 'IGNORE';
313 local $SIG{QUIT} = 'IGNORE';
314 local $SIG{TERM} = 'IGNORE';
315 local $SIG{TSTP} = 'IGNORE';
316 local $SIG{PIPE} = 'IGNORE';
318 my $oldAutoCommit = $FS::UID::AutoCommit;
319 local $FS::UID::AutoCommit = 0;
322 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
323 my $error = $app->delete;
325 $dbh->rollback if $oldAutoCommit;
330 my $error = $self->SUPER::delete(@_);
332 $dbh->rollback if $oldAutoCommit;
336 if ( $conf->config('deletepayments') ne '' ) {
338 my $cust_main = $self->cust_main;
340 my $error = send_email(
341 'from' => $conf->config('invoice_from'), #??? well as good as any
342 'to' => $conf->config('deletepayments'),
343 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
345 "This is an automatic message from your Freeside installation\n",
346 "informing you that the following payment has been deleted:\n",
348 'paynum: '. $self->paynum. "\n",
349 'custnum: '. $self->custnum.
350 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
351 'paid: $'. sprintf("%.2f", $self->paid). "\n",
352 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
353 'payby: '. $self->payby. "\n",
354 'payinfo: '. $self->paymask. "\n",
355 'paybatch: '. $self->paybatch. "\n",
360 $dbh->rollback if $oldAutoCommit;
361 return "can't send payment deletion notification: $error";
366 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
372 =item replace OLD_RECORD
374 You can, but probably shouldn't modify payments...
379 #return "Can't modify payment!"
381 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
382 $self->SUPER::replace(@_);
387 Checks all fields to make sure this is a valid payment. If there is an error,
388 returns the error, otherwise returns false. Called by the insert method.
396 $self->ut_numbern('paynum')
397 || $self->ut_numbern('custnum')
398 || $self->ut_money('paid')
399 || $self->ut_numbern('_date')
400 || $self->ut_textn('paybatch')
401 || $self->ut_enum('closed', [ '', 'Y' ])
402 || $self->payinfo_check()
404 return $error if $error;
406 return "paid must be > 0 " if $self->paid <= 0;
408 return "unknown cust_main.custnum: ". $self->custnum
410 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
412 $self->_date(time) unless $self->_date;
417 =item batch_insert CUST_PAY_OBJECT, ...
419 Class method which inserts multiple payments. Takes a list of FS::cust_pay
420 objects. Returns a list, each element representing the status of inserting the
421 corresponding payment - empty. If there is an error inserting any payment, the
422 entire transaction is rolled back, i.e. all payments are inserted or none are.
426 my @errors = FS::cust_pay->batch_insert(@cust_pay);
427 my $num_errors = scalar(grep $_, @errors);
428 if ( $num_errors == 0 ) {
429 #success; all payments were inserted
431 #failure; no payments were inserted.
437 my $self = shift; #class method
439 local $SIG{HUP} = 'IGNORE';
440 local $SIG{INT} = 'IGNORE';
441 local $SIG{QUIT} = 'IGNORE';
442 local $SIG{TERM} = 'IGNORE';
443 local $SIG{TSTP} = 'IGNORE';
444 local $SIG{PIPE} = 'IGNORE';
446 my $oldAutoCommit = $FS::UID::AutoCommit;
447 local $FS::UID::AutoCommit = 0;
453 my $error = $_->insert( 'manual' => 1 );
457 $_->cust_main->apply_payments;
463 $dbh->rollback if $oldAutoCommit;
465 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
474 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
481 sort { $a->_date <=> $b->_date
482 || $a->invnum <=> $b->invnum }
483 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
487 =item cust_pay_refund
489 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
494 sub cust_pay_refund {
496 sort { $a->_date <=> $b->_date }
497 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
504 Returns the amount of this payment that is still unapplied; which is
505 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
506 applications (see L<FS::cust_pay_refund>).
512 my $amount = $self->paid;
513 $amount -= $_->amount foreach ( $self->cust_bill_pay );
514 $amount -= $_->amount foreach ( $self->cust_pay_refund );
515 sprintf("%.2f", $amount );
520 Returns the amount of this payment that has not been refuned; which is
521 paid minus all refund applications (see L<FS::cust_pay_refund>).
527 my $amount = $self->paid;
528 $amount -= $_->amount foreach ( $self->cust_pay_refund );
529 sprintf("%.2f", $amount );
535 Returns the parent customer object (see L<FS::cust_main>).
541 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
548 Delete and replace methods.
552 L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>, schema.html from the