1 package FS::cust_pay_batch;
4 use vars qw( @ISA $DEBUG );
5 use Carp qw( confess );
6 use Business::CreditCard 0.28;
7 use FS::Record qw(dbh qsearch qsearchs);
12 @ISA = qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
14 # 1 is mostly method/subroutine entry and options
15 # 2 traces progress of some operations
16 # 3 is even more information including possibly sensitive data
19 #@encrypted_fields = ('payinfo');
20 sub nohistory_fields { ('payinfo'); }
24 FS::cust_pay_batch - Object methods for batch cards
28 use FS::cust_pay_batch;
30 $record = new FS::cust_pay_batch \%hash;
31 $record = new FS::cust_pay_batch { 'column' => 'value' };
33 $error = $record->insert;
35 $error = $new_record->replace($old_record);
37 $error = $record->delete;
39 $error = $record->check;
41 #deprecated# $error = $record->retriable;
45 An FS::cust_pay_batch object represents a credit card transaction ready to be
46 batched (sent to a processor). FS::cust_pay_batch inherits from FS::Record.
47 Typically called by the collect method of an FS::cust_main object. The
48 following fields are currently supported:
52 =item paybatchnum - primary key (automatically assigned)
54 =item batchnum - indentifies group in batch
56 =item payby - CARD/CHEK/LECB/BILL/COMP
60 =item exp - card expiration
64 =item invnum - invoice
66 =item custnum - customer
68 =item payname - name on card
86 =item status - 'Approved' or 'Declined'
88 =item error_message - the error returned by the gateway if any
90 =item failure_status - the normalized L<Business::BatchPayment> failure
101 Creates a new record. To add the record to the database, see L<"insert">.
103 Note that this stores the hash reference, not a distinct copy of the hash it
104 points to. You can ask the object for a copy with the I<hash> method.
108 sub table { 'cust_pay_batch'; }
112 Adds this record to the database. If there is an error, returns the error,
113 otherwise returns false.
117 Delete this record from the database. If there is an error, returns the error,
118 otherwise returns false.
120 =item replace OLD_RECORD
122 Replaces the OLD_RECORD with this one in the database. If there is an error,
123 returns the error, otherwise returns false.
127 Checks all fields to make sure this is a valid transaction. If there is
128 an error, returns the error, otherwise returns false. Called by the insert
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 || $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|LECB|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
212 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
217 Returns the credit card expiration date in MMYY format. If this is a
218 CHEK payment, returns an empty string.
224 if ( $self->payby eq 'CARD' ) {
225 $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/;
226 return sprintf('%02u%02u', $2, ($1 % 100));
235 Returns the payment batch this payment belongs to (L<FS::pay_batch).
241 FS::pay_batch->by_key($self->batchnum);
244 #you know what, screw this in the new world of events. we should be able to
245 #get the event defs to retry (remove once.pm condition, add every.pm) without
246 #mucking about with statuses of previous cust_event records. right?
250 #Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
251 #credit card payment as retriable. Useful if the corresponding financial
252 #institution account was declined for temporary reasons and/or a manual
255 #Implementation details: For the named customer's invoice, changes the
256 #statustext of the 'done' (without statustext) event to 'retriable.'
262 confess "deprecated method cust_pay_batch->retriable called; try removing ".
263 "the once condition and adding an every condition?";
267 local $SIG{HUP} = 'IGNORE'; #Hmm
268 local $SIG{INT} = 'IGNORE';
269 local $SIG{QUIT} = 'IGNORE';
270 local $SIG{TERM} = 'IGNORE';
271 local $SIG{TSTP} = 'IGNORE';
272 local $SIG{PIPE} = 'IGNORE';
274 my $oldAutoCommit = $FS::UID::AutoCommit;
275 local $FS::UID::AutoCommit = 0;
278 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
279 or return "event $self->eventnum references nonexistant invoice $self->invnum";
281 warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
282 my @cust_bill_event =
283 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
285 $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
286 && $_->status eq 'done'
289 $cust_bill->cust_bill_event;
290 # complain loudly if scalar(@cust_bill_event) > 1 ?
291 my $error = $cust_bill_event[0]->retriable;
293 # gah, even with transactions.
294 $dbh->commit if $oldAutoCommit; #well.
295 return "error marking invoice event retriable: $error";
300 =item approve OPTIONS
302 Approve this payment. This will replace the existing record with the
303 same paybatchnum, set its status to 'Approved', and generate a payment
304 record (L<FS::cust_pay>). This should only be called from the batch
307 OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
312 # to break up the Big Wall of Code that is import_results
315 my $paybatchnum = $new->paybatchnum;
316 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
317 or return "paybatchnum $paybatchnum not found";
318 # leave these restrictions in place until TD EFT is converted over
320 return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
322 $new->status('Approved');
323 my $error = $new->replace($old);
325 return "error updating status of paybatchnum $paybatchnum: $error\n";
327 my $cust_pay = new FS::cust_pay ( {
328 'custnum' => $new->custnum,
329 'payby' => $new->payby,
330 'payinfo' => $new->payinfo || $old->payinfo,
331 'paid' => $new->paid,
332 '_date' => $new->_date,
333 'usernum' => $new->usernum,
334 'batchnum' => $new->batchnum,
335 'gatewaynum' => $opt{'gatewaynum'},
336 'processor' => $opt{'processor'},
337 'auth' => $opt{'auth'},
338 'order_number' => $opt{'order_number'}
341 $error = $cust_pay->insert;
343 return "error inserting payment for paybatchnum $paybatchnum: $error\n";
345 $cust_pay->cust_main->apply_payments;
349 =item decline [ REASON [ STATUS ] ]
351 Decline this payment. This will replace the existing record with the
352 same paybatchnum, set its status to 'Declined', and run collection events
353 as appropriate. This should only be called from the batch import process.
355 REASON is a string description of the decline reason, defaulting to
356 'Returned payment', and will go into the "error_message" field.
358 STATUS is a normalized failure status defined by L<Business::BatchPayment>,
359 and will go into the "failure_status" field.
365 my $reason = shift || 'Returned payment';
366 my $failure_status = shift || '';
367 #my $conf = new FS::Conf;
369 my $paybatchnum = $new->paybatchnum;
370 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
371 or return "paybatchnum $paybatchnum not found";
372 if ( $old->status ) {
373 # Handle the case where payments are rejected after the batch has been
374 # approved. FS::pay_batch::import_results won't allow results to be
375 # imported to a closed batch unless batch-manual_approval is enabled,
376 # so we don't check it here.
377 # if ( $conf->exists('batch-manual_approval') and
378 if ( lc($old->status) eq 'approved' ) {
380 my $cust_pay = qsearchs('cust_pay', {
381 custnum => $new->custnum,
382 batchnum => $new->batchnum
384 # these should all be migrated over, but if it's not found, look for
385 # batchnum in the 'paybatch' field also
386 $cust_pay ||= qsearchs('cust_pay', {
387 custnum => $new->custnum,
388 paybatch => $new->batchnum
391 # should never happen...
392 return "failed to revoke paybatchnum $paybatchnum, payment not found";
394 $cust_pay->void($reason);
397 # normal case: refuse to do anything
398 return "paybatchnum $paybatchnum already resolved ('".$old->status."')";
401 $new->status('Declined');
402 $new->error_message($reason);
403 $new->failure_status($failure_status);
404 my $error = $new->replace($old);
406 return "error updating status of paybatchnum $paybatchnum: $error\n";
408 my $due_cust_event = $new->cust_main->due_cust_event(
409 'eventtable' => 'cust_pay_batch',
410 'objects' => [ $new ],
412 if ( !ref($due_cust_event) ) {
413 return $due_cust_event;
415 # XXX breaks transaction integrity
416 foreach my $cust_event (@$due_cust_event) {
417 next unless $cust_event->test_conditions;
418 if ( my $error = $cust_event->do_event() ) {
425 =item request_item [ OPTIONS ]
427 Returns a L<Business::BatchPayment::Item> object for this batch payment
428 entry. This can be submitted to a processor.
430 OPTIONS can be a list of key/values to append to the attributes. The most
431 useful case of this is "process_date" to set a processing date based on the
432 date the batch is being submitted.
440 eval "use Business::BatchPayment;";
441 die "couldn't load Business::BatchPayment: $@" if $@;
443 my $cust_main = $self->cust_main;
444 my $location = $cust_main->bill_location;
445 my $pay_batch = $self->pay_batch;
448 $payment{payment_type} = FS::payby->payby2bop( $pay_batch->payby );
449 if ( $payment{payment_type} eq 'CC' ) {
450 $payment{card_number} = $self->payinfo,
451 $payment{expiration} = $self->expmmyy,
452 } elsif ( $payment{payment_type} eq 'ECHECK' ) {
453 $self->payinfo =~ /(\d+)@(\d+)/; # or else what?
454 $payment{account_number} = $1;
455 $payment{routing_code} = $2;
456 $payment{account_type} = $cust_main->paytype;
457 # XXX what if this isn't their regular payment method?
459 die "unsupported BatchPayment method: ".$pay_batch->payby;
462 Business::BatchPayment->create(Item =>
465 tid => $self->paybatchnum,
466 amount => $self->amount,
469 customer_id => $self->custnum,
470 first_name => $cust_main->first,
471 last_name => $cust_main->last,
472 company => $cust_main->company,
473 address => $location->address1,
474 ( map { $_ => $location->$_ } qw(address2 city state country zip) ),
476 invoice_number => $self->invnum,
485 There should probably be a configuration file with a list of allowed credit
490 L<FS::cust_main>, L<FS::Record>