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
82 =item status - 'Approved' or 'Declined'
84 =item error_message - the error returned by the gateway if any
86 =item failure_status - the normalized L<Business::BatchPayment> failure
97 Creates a new record. To add the record to the database, see L<"insert">.
99 Note that this stores the hash reference, not a distinct copy of the hash it
100 points to. You can ask the object for a copy with the I<hash> method.
104 sub table { 'cust_pay_batch'; }
108 Adds this record to the database. If there is an error, returns the error,
109 otherwise returns false.
113 Delete this record from the database. If there is an error, returns the error,
114 otherwise returns false.
116 =item replace OLD_RECORD
118 Replaces the OLD_RECORD with this one in the database. If there is an error,
119 returns the error, otherwise returns false.
123 Checks all fields to make sure this is a valid transaction. If there is
124 an error, returns the error, otherwise returns false. Called by the insert
132 my $conf = new FS::Conf;
135 $self->ut_numbern('paybatchnum')
136 || $self->ut_numbern('trancode') #deprecated
137 || $self->ut_money('amount')
138 || $self->ut_number('invnum')
139 || $self->ut_number('custnum')
140 || $self->ut_text('address1')
141 || $self->ut_textn('address2')
142 || ($conf->exists('cust_main-no_city_in_address')
143 ? $self->ut_textn('city')
144 : $self->ut_text('city'))
145 || $self->ut_textn('state')
148 return $error if $error;
150 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
151 $self->setfield('last',$1);
153 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
156 $error = $self->payinfo_check();
157 return $error if $error;
159 if ( $self->exp eq '' ) {
160 return "Expiration date required"
161 unless $self->payby =~ /^(CHEK|DCHK|WEST)$/;
164 if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
165 $self->exp("$1-$2-$3");
166 } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
167 if ( length($2) == 4 ) {
168 $self->exp("$2-$1-01");
169 } elsif ( $2 > 98 ) { #should pry change to check for "this year"
170 $self->exp("19$2-$1-01");
172 $self->exp("20$2-$1-01");
175 return "Illegal expiration date";
179 if ( $self->payname eq '' ) {
180 $self->payname( $self->first. " ". $self->getfield('last') );
182 $self->payname =~ /^([\w \,\.\-\']+)$/
183 or return "Illegal billing name";
187 #we have lots of old zips in there... don't hork up batch results cause of em
188 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
189 or return "Illegal zip: ". $self->zip;
192 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
195 #$error = $self->ut_zip('zip', $self->country);
196 #return $error if $error;
198 #check invnum, custnum, ?
205 Returns the customer (see L<FS::cust_main>) for this batched credit card
210 Returns the credit card expiration date in MMYY format. If this is a
211 CHEK payment, returns an empty string.
217 if ( $self->payby eq 'CARD' ) {
218 $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/;
219 return sprintf('%02u%02u', $2, ($1 % 100));
228 Returns the payment batch this payment belongs to (L<FS::pay_batch).
232 #you know what, screw this in the new world of events. we should be able to
233 #get the event defs to retry (remove once.pm condition, add every.pm) without
234 #mucking about with statuses of previous cust_event records. right?
238 #Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
239 #credit card payment as retriable. Useful if the corresponding financial
240 #institution account was declined for temporary reasons and/or a manual
243 #Implementation details: For the named customer's invoice, changes the
244 #statustext of the 'done' (without statustext) event to 'retriable.'
250 confess "deprecated method cust_pay_batch->retriable called; try removing ".
251 "the once condition and adding an every condition?";
255 =item approve OPTIONS
257 Approve this payment. This will replace the existing record with the
258 same paybatchnum, set its status to 'Approved', and generate a payment
259 record (L<FS::cust_pay>). This should only be called from the batch
262 OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
267 # to break up the Big Wall of Code that is import_results
270 my $paybatchnum = $new->paybatchnum;
271 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
272 or return "cannot approve, paybatchnum $paybatchnum not found";
273 # leave these restrictions in place until TD EFT is converted over
275 return "cannot approve paybatchnum $paybatchnum, already resolved ('".$old->status."')"
277 $new->status('Approved');
278 my $error = $new->replace($old);
280 return "error approving paybatchnum $paybatchnum: $error\n";
282 my $cust_pay = new FS::cust_pay ( {
283 'custnum' => $new->custnum,
284 'payby' => $new->payby,
285 'payinfo' => $new->payinfo || $old->payinfo,
286 'paid' => $new->paid,
287 '_date' => $new->_date,
288 'usernum' => $new->usernum,
289 'batchnum' => $new->batchnum,
290 'gatewaynum' => $opt{'gatewaynum'},
291 'processor' => $opt{'processor'},
292 'auth' => $opt{'auth'},
293 'order_number' => $opt{'order_number'}
296 $error = $cust_pay->insert;
298 return "error inserting payment for paybatchnum $paybatchnum: $error\n";
300 $cust_pay->cust_main->apply_payments;
304 =item decline [ REASON [ STATUS ] ]
306 Decline this payment. This will replace the existing record with the
307 same paybatchnum, set its status to 'Declined', and run collection events
308 as appropriate. This should only be called from the batch import process.
310 REASON is a string description of the decline reason, defaulting to
311 'Returned payment', and will go into the "error_message" field.
313 STATUS is a normalized failure status defined by L<Business::BatchPayment>,
314 and will go into the "failure_status" field.
320 my $reason = shift || 'Returned payment';
321 my $failure_status = shift || '';
322 #my $conf = new FS::Conf;
324 my $paybatchnum = $new->paybatchnum;
325 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
326 or return "cannot decline, paybatchnum $paybatchnum not found";
327 if ( $old->status ) {
328 # Handle the case where payments are rejected after the batch has been
329 # approved. FS::pay_batch::import_results won't allow results to be
330 # imported to a closed batch unless batch-manual_approval is enabled,
331 # so we don't check it here.
332 # if ( $conf->exists('batch-manual_approval') and
333 if ( lc($old->status) eq 'approved' ) {
335 my $cust_pay = qsearchs('cust_pay', {
336 custnum => $new->custnum,
337 batchnum => $new->batchnum
339 # these should all be migrated over, but if it's not found, look for
340 # batchnum in the 'paybatch' field also
341 $cust_pay ||= qsearchs('cust_pay', {
342 custnum => $new->custnum,
343 paybatch => $new->batchnum
346 # should never happen...
347 return "failed to revoke paybatchnum $paybatchnum, payment not found";
349 $cust_pay->void($reason);
352 # normal case: refuse to do anything
353 return "cannot decline paybatchnum $paybatchnum, already resolved ('".$old->status."')";
356 $new->status('Declined');
357 $new->error_message($reason);
358 $new->failure_status($failure_status);
359 my $error = $new->replace($old);
361 return "error declining paybatchnum $paybatchnum: $error\n";
363 my $due_cust_event = $new->cust_main->due_cust_event(
364 'eventtable' => 'cust_pay_batch',
365 'objects' => [ $new ],
367 if ( !ref($due_cust_event) ) {
368 return $due_cust_event;
370 # XXX breaks transaction integrity
371 foreach my $cust_event (@$due_cust_event) {
372 next unless $cust_event->test_conditions;
373 if ( my $error = $cust_event->do_event() ) {
380 =item request_item [ OPTIONS ]
382 Returns a L<Business::BatchPayment::Item> object for this batch payment
383 entry. This can be submitted to a processor.
385 OPTIONS can be a list of key/values to append to the attributes. The most
386 useful case of this is "process_date" to set a processing date based on the
387 date the batch is being submitted.
395 eval "use Business::BatchPayment;";
396 die "couldn't load Business::BatchPayment: $@" if $@;
398 my $cust_main = $self->cust_main;
399 my $location = $cust_main->bill_location;
400 my $pay_batch = $self->pay_batch;
403 $payment{payment_type} = FS::payby->payby2bop( $pay_batch->payby );
404 if ( $payment{payment_type} eq 'CC' ) {
405 $payment{card_number} = $self->payinfo,
406 $payment{expiration} = $self->expmmyy,
407 } elsif ( $payment{payment_type} eq 'ECHECK' ) {
408 $self->payinfo =~ /(\d+)@(\d+)/; # or else what?
409 $payment{account_number} = $1;
410 $payment{routing_code} = $2;
411 $payment{account_type} = $cust_main->paytype;
412 # XXX what if this isn't their regular payment method?
414 die "unsupported BatchPayment method: ".$pay_batch->payby;
417 Business::BatchPayment->create(Item =>
420 tid => $self->paybatchnum,
421 amount => $self->amount,
424 customer_id => $self->custnum,
425 first_name => $cust_main->first,
426 last_name => $cust_main->last,
427 company => $cust_main->company,
428 address => $location->address1,
429 ( map { $_ => $location->$_ } qw(address2 city state country zip) ),
431 invoice_number => $self->invnum,
436 =item process_unbatch_and_delete
438 L</unbatch_and_delete> run as a queued job, accepts I<$job> and I<$param>.
442 sub process_unbatch_and_delete {
443 my ($job, $param) = @_;
444 my $self = qsearchs('cust_pay_batch',{ 'paybatchnum' => scalar($param->{'paybatchnum'}) })
445 or die 'Could not find paybatchnum ' . $param->{'paybatchnum'};
446 my $error = $self->unbatch_and_delete;
447 die $error if $error;
451 =item unbatch_and_delete
453 May only be called on a record with an empty status and an associated
454 L<pay_batch> with a status of 'O' (not yet in transit.) Deletes all associated
455 records from L<cust_bill_pay_batch> and then deletes this record.
456 If there is an error, returns the error, otherwise returns false.
460 sub unbatch_and_delete {
463 return 'Cannot unbatch a cust_pay_batch with status ' . $self->status
466 my $pay_batch = qsearchs('pay_batch',{ 'batchnum' => $self->batchnum })
467 or return 'Cannot find associated pay_batch record';
469 return 'Cannot unbatch from a pay_batch with status ' . $pay_batch->status
470 if $pay_batch->status ne 'O';
472 local $SIG{HUP} = 'IGNORE';
473 local $SIG{INT} = 'IGNORE';
474 local $SIG{QUIT} = 'IGNORE';
475 local $SIG{TERM} = 'IGNORE';
476 local $SIG{TSTP} = 'IGNORE';
477 local $SIG{PIPE} = 'IGNORE';
479 my $oldAutoCommit = $FS::UID::AutoCommit;
480 local $FS::UID::AutoCommit = 0;
483 # have not generated actual payments yet, so should be safe to delete
484 foreach my $cust_bill_pay_batch (
485 qsearch('cust_bill_pay_batch',{ 'paybatchnum' => $self->paybatchnum })
487 my $error = $cust_bill_pay_batch->delete;
489 $dbh->rollback if $oldAutoCommit;
494 my $error = $self->delete;
496 $dbh->rollback if $oldAutoCommit;
500 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
507 Returns the invoice linked to this batched payment. Deprecated, will be
513 carp "FS::cust_pay_batch->cust_bill is deprecated";
515 $self->invnum ? qsearchs('cust_bill', { invnum => $self->invnum }) : '';
522 There should probably be a configuration file with a list of allowed credit
527 L<FS::cust_main>, L<FS::Record>