1 package FS::cust_pay_batch;
2 use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
6 use Carp qw( carp confess );
7 use Business::CreditCard 0.28;
8 use FS::Record qw(dbh qsearch qsearchs);
10 # 1 is mostly method/subroutine entry and options
11 # 2 traces progress of some operations
12 # 3 is even more information including possibly sensitive data
15 #@encrypted_fields = ('payinfo');
16 sub nohistory_fields { ('payinfo'); }
20 FS::cust_pay_batch - Object methods for batch cards
24 use FS::cust_pay_batch;
26 $record = new FS::cust_pay_batch \%hash;
27 $record = new FS::cust_pay_batch { 'column' => 'value' };
29 $error = $record->insert;
31 $error = $new_record->replace($old_record);
33 $error = $record->delete;
35 $error = $record->check;
37 #deprecated# $error = $record->retriable;
41 An FS::cust_pay_batch object represents a credit card transaction ready to be
42 batched (sent to a processor). FS::cust_pay_batch inherits from FS::Record.
43 Typically called by the collect method of an FS::cust_main object. The
44 following fields are currently supported:
48 =item paybatchnum - primary key (automatically assigned)
50 =item batchnum - indentifies group in batch
52 =item payby - CARD/CHEK
56 =item exp - card expiration
60 =item invnum - invoice
62 =item custnum - customer
64 =item payname - name on card
66 =item paytype - account type ((personal|business) (checking|savings))
84 =item status - 'Approved' or 'Declined'
86 =item error_message - the error returned by the gateway if any
88 =item failure_status - the normalized L<Business::BatchPayment> failure
99 Creates a new record. To add the record to the database, see L<"insert">.
101 Note that this stores the hash reference, not a distinct copy of the hash it
102 points to. You can ask the object for a copy with the I<hash> method.
106 sub table { 'cust_pay_batch'; }
110 Adds this record to the database. If there is an error, returns the error,
111 otherwise returns false.
115 Delete this record from the database. If there is an error, returns the error,
116 otherwise returns false.
118 =item replace OLD_RECORD
120 Replaces the OLD_RECORD with this one in the database. If there is an error,
121 returns the error, otherwise returns false.
125 Checks all fields to make sure this is a valid transaction. If there is
126 an error, returns the error, otherwise returns false. Called by the insert
134 my $conf = new FS::Conf;
137 $self->ut_numbern('paybatchnum')
138 || $self->ut_numbern('trancode') #deprecated
139 || $self->ut_money('amount')
140 || $self->ut_number('invnum')
141 || $self->ut_number('custnum')
142 || $self->ut_text('address1')
143 || $self->ut_textn('address2')
144 || ($conf->exists('cust_main-no_city_in_address')
145 ? $self->ut_textn('city')
146 : $self->ut_text('city'))
147 || $self->ut_textn('state')
150 return $error if $error;
152 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
153 $self->setfield('last',$1);
155 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
158 $error = $self->payinfo_check();
159 return $error if $error;
161 if ( $self->payby eq 'CHEK' ) {
162 # because '' is on the list of paytypes:
163 my $paytype = $self->paytype or return "Bank account type required";
164 if (grep { $_ eq $paytype} FS::cust_payby->paytypes) {
167 return "Bank account type '$paytype' is not allowed"
170 $self->set('paytype', '');
173 if ( $self->exp eq '' ) {
174 return "Expiration date required"
175 unless $self->payby =~ /^(CHEK|DCHK|WEST)$/;
178 if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
179 $self->exp("$1-$2-$3");
180 } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
181 if ( length($2) == 4 ) {
182 $self->exp("$2-$1-01");
183 } elsif ( $2 > 98 ) { #should pry change to check for "this year"
184 $self->exp("19$2-$1-01");
186 $self->exp("20$2-$1-01");
189 return "Illegal expiration date";
193 if ( $self->payname eq '' ) {
194 $self->payname( $self->first. " ". $self->getfield('last') );
196 $self->payname =~ /^([\w \,\.\-\']+)$/
197 or return "Illegal billing name";
201 #we have lots of old zips in there... don't hork up batch results cause of em
202 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
203 or return "Illegal zip: ". $self->zip;
206 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
209 #$error = $self->ut_zip('zip', $self->country);
210 #return $error if $error;
212 #check invnum, custnum, ?
219 Returns the customer (see L<FS::cust_main>) for this batched credit card
224 Returns the credit card expiration date in MMYY format. If this is a
225 CHEK payment, returns an empty string.
231 if ( $self->payby eq 'CARD' ) {
232 $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/;
233 return sprintf('%02u%02u', $2, ($1 % 100));
242 Returns the payment batch this payment belongs to (L<FS::pay_batch>).
246 #you know what, screw this in the new world of events. we should be able to
247 #get the event defs to retry (remove once.pm condition, add every.pm) without
248 #mucking about with statuses of previous cust_event records. right?
252 #Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
253 #credit card payment as retriable. Useful if the corresponding financial
254 #institution account was declined for temporary reasons and/or a manual
257 #Implementation details: For the named customer's invoice, changes the
258 #statustext of the 'done' (without statustext) event to 'retriable.'
264 confess "deprecated method cust_pay_batch->retriable called; try removing ".
265 "the once condition and adding an every condition?";
269 =item approve OPTIONS
271 Approve this payment. This will replace the existing record with the
272 same paybatchnum, set its status to 'Approved', and generate a payment
273 record (L<FS::cust_pay>). This should only be called from the batch
276 OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
281 # to break up the Big Wall of Code that is import_results
284 my $paybatchnum = $new->paybatchnum;
285 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
286 or return "cannot approve, paybatchnum $paybatchnum not found";
287 # leave these restrictions in place until TD EFT is converted over
289 return "cannot approve paybatchnum $paybatchnum, already resolved ('".$old->status."')"
291 $new->status('Approved');
292 my $error = $new->replace($old);
294 return "error approving paybatchnum $paybatchnum: $error\n";
297 return if $new->paycode eq "C";
299 my $cust_pay = new FS::cust_pay ( {
300 'custnum' => $new->custnum,
301 'payby' => $new->payby,
302 'payinfo' => $new->payinfo || $old->payinfo,
303 'paymask' => $new->mask_payinfo,
304 'paid' => $new->paid,
305 '_date' => $new->_date,
306 'usernum' => $new->usernum,
307 'batchnum' => $new->batchnum,
308 'invnum' => $old->invnum,
309 'gatewaynum' => $opt{'gatewaynum'},
310 'processor' => $opt{'processor'},
311 'auth' => $opt{'auth'},
312 'order_number' => $opt{'order_number'}
315 $error = $cust_pay->insert;
317 return "error inserting payment for paybatchnum $paybatchnum: $error\n";
319 $cust_pay->cust_main->apply_payments;
323 =item decline [ REASON [ STATUS ] ]
325 Decline this payment. This will replace the existing record with the
326 same paybatchnum, set its status to 'Declined', and run collection events
327 as appropriate. This should only be called from the batch import process.
329 REASON is a string description of the decline reason, defaulting to
330 'Returned payment', and will go into the "error_message" field.
332 STATUS is a normalized failure status defined by L<Business::BatchPayment>,
333 and will go into the "failure_status" field.
339 my $reason = shift || 'Returned payment';
340 my $failure_status = shift || '';
341 #my $conf = new FS::Conf;
343 my $paybatchnum = $new->paybatchnum;
344 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
345 or return "cannot decline, paybatchnum $paybatchnum not found";
346 if ( $old->status ) {
347 # Handle the case where payments are rejected after the batch has been
348 # approved. FS::pay_batch::import_results won't allow results to be
349 # imported to a closed batch unless batch-manual_approval is enabled,
350 # so we don't check it here.
351 # if ( $conf->exists('batch-manual_approval') and
352 if ( lc($old->status) eq 'approved' ) {
354 my $cust_pay = qsearchs('cust_pay', {
355 custnum => $new->custnum,
356 batchnum => $new->batchnum
358 # these should all be migrated over, but if it's not found, look for
359 # batchnum in the 'paybatch' field also
360 $cust_pay ||= qsearchs('cust_pay', {
361 custnum => $new->custnum,
362 paybatch => $new->batchnum
365 # should never happen...
366 return "failed to revoke paybatchnum $paybatchnum, payment not found";
368 $cust_pay->void($reason);
371 # normal case: refuse to do anything
372 return "cannot decline paybatchnum $paybatchnum, already resolved ('".$old->status."')";
375 $new->status('Declined');
376 $new->error_message($reason);
377 $new->failure_status($failure_status);
378 my $error = $new->replace($old);
380 return "error declining paybatchnum $paybatchnum: $error\n";
382 my $due_cust_event = $new->cust_main->due_cust_event(
383 'eventtable' => 'cust_pay_batch',
384 'objects' => [ $new ],
386 if ( !ref($due_cust_event) ) {
387 return $due_cust_event;
389 # XXX breaks transaction integrity
390 foreach my $cust_event (@$due_cust_event) {
391 next unless $cust_event->test_conditions;
392 if ( my $error = $cust_event->do_event() ) {
399 =item request_item [ OPTIONS ]
401 Returns a L<Business::BatchPayment::Item> object for this batch payment
402 entry. This can be submitted to a processor.
404 OPTIONS can be a list of key/values to append to the attributes. The most
405 useful case of this is "process_date" to set a processing date based on the
406 date the batch is being submitted.
414 eval "use Business::BatchPayment;";
415 die "couldn't load Business::BatchPayment: $@" if $@;
417 my $cust_main = $self->cust_main;
418 my $location = $cust_main->bill_location;
419 my $pay_batch = $self->pay_batch;
422 $payment{payment_type} = FS::payby->payby2bop( $pay_batch->payby );
423 if ( $payment{payment_type} eq 'CC' ) {
424 $payment{card_number} = $self->payinfo,
425 $payment{expiration} = $self->expmmyy,
426 } elsif ( $payment{payment_type} eq 'ECHECK' ) {
427 $self->payinfo =~ /(\d+)@(\d+)/; # or else what?
428 $payment{account_number} = $1;
429 $payment{routing_code} = $2;
430 $payment{account_type} = $self->paytype;
431 # XXX what if this isn't their regular payment method?
433 die "unsupported BatchPayment method: ".$pay_batch->payby;
437 if ( $cust_main->status =~ /^active|suspended|ordered$/ ) {
438 if ( $self->payinfo_used ) {
439 $recurring = 'S'; # subsequent
441 $recurring = 'F'; # first use
444 $recurring = 'N'; # non-recurring
447 Business::BatchPayment->create(Item =>
450 tid => $self->paybatchnum,
451 amount => $self->amount,
454 customer_id => $self->custnum,
455 first_name => $cust_main->first,
456 last_name => $cust_main->last,
457 company => $cust_main->company,
458 address => $location->address1,
459 ( map { $_ => $location->$_ } qw(address2 city state country zip) ),
461 invoice_number => $self->invnum,
462 recurring_billing => $recurring,
467 =item process_unbatch_and_delete
469 L</unbatch_and_delete> run as a queued job, accepts I<$job> and I<$param>.
473 sub process_unbatch_and_delete {
474 my ($job, $param) = @_;
475 my $self = qsearchs('cust_pay_batch',{ 'paybatchnum' => scalar($param->{'paybatchnum'}) })
476 or die 'Could not find paybatchnum ' . $param->{'paybatchnum'};
477 my $error = $self->unbatch_and_delete;
478 die $error if $error;
482 =item unbatch_and_delete
484 May only be called on a record with an empty status and an associated
485 L<FS::pay_batch> with a status of 'O' (not yet in transit.) Deletes all associated
486 records from L<FS::cust_bill_pay_batch> and then deletes this record.
487 If there is an error, returns the error, otherwise returns false.
491 sub unbatch_and_delete {
494 return 'Cannot unbatch a cust_pay_batch with status ' . $self->status
497 my $pay_batch = qsearchs('pay_batch',{ 'batchnum' => $self->batchnum })
498 or return 'Cannot find associated pay_batch record';
500 return 'Cannot unbatch from a pay_batch with status ' . $pay_batch->status
501 if $pay_batch->status ne 'O';
503 local $SIG{HUP} = 'IGNORE';
504 local $SIG{INT} = 'IGNORE';
505 local $SIG{QUIT} = 'IGNORE';
506 local $SIG{TERM} = 'IGNORE';
507 local $SIG{TSTP} = 'IGNORE';
508 local $SIG{PIPE} = 'IGNORE';
510 my $oldAutoCommit = $FS::UID::AutoCommit;
511 local $FS::UID::AutoCommit = 0;
514 # have not generated actual payments yet, so should be safe to delete
515 foreach my $cust_bill_pay_batch (
516 qsearch('cust_bill_pay_batch',{ 'paybatchnum' => $self->paybatchnum })
518 my $error = $cust_bill_pay_batch->delete;
520 $dbh->rollback if $oldAutoCommit;
525 my $error = $self->delete;
527 $dbh->rollback if $oldAutoCommit;
531 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
538 Returns the invoice linked to this batched payment. Deprecated, will be
544 carp "FS::cust_pay_batch->cust_bill is deprecated";
546 $self->invnum ? qsearchs('cust_bill', { invnum => $self->invnum }) : '';
553 There should probably be a configuration file with a list of allowed credit
558 L<FS::cust_main>, L<FS::Record>