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
21 FS::cust_pay_batch - Object methods for batch cards
25 use FS::cust_pay_batch;
27 $record = new FS::cust_pay_batch \%hash;
28 $record = new FS::cust_pay_batch { 'column' => 'value' };
30 $error = $record->insert;
32 $error = $new_record->replace($old_record);
34 $error = $record->delete;
36 $error = $record->check;
38 #deprecated# $error = $record->retriable;
42 An FS::cust_pay_batch object represents a credit card transaction ready to be
43 batched (sent to a processor). FS::cust_pay_batch inherits from FS::Record.
44 Typically called by the collect method of an FS::cust_main object. The
45 following fields are currently supported:
49 =item paybatchnum - primary key (automatically assigned)
51 =item batchnum - indentifies group in batch
53 =item payby - CARD/CHEK/LECB/BILL/COMP
57 =item exp - card expiration
61 =item invnum - invoice
63 =item custnum - customer
65 =item payname - name on card
83 =item status - 'Approved' or 'Declined'
85 =item error_message - the error returned by the gateway if any
95 Creates a new record. To add the record to the database, see L<"insert">.
97 Note that this stores the hash reference, not a distinct copy of the hash it
98 points to. You can ask the object for a copy with the I<hash> method.
102 sub table { 'cust_pay_batch'; }
106 Adds this record to the database. If there is an error, returns the error,
107 otherwise returns false.
111 Delete this record from the database. If there is an error, returns the error,
112 otherwise returns false.
114 =item replace OLD_RECORD
116 Replaces the OLD_RECORD with this one in the database. If there is an error,
117 returns the error, otherwise returns false.
121 Checks all fields to make sure this is a valid transaction. If there is
122 an error, returns the error, otherwise returns false. Called by the insert
131 $self->ut_numbern('paybatchnum')
132 || $self->ut_numbern('trancode') #deprecated
133 || $self->ut_money('amount')
134 || $self->ut_number('invnum')
135 || $self->ut_number('custnum')
136 || $self->ut_text('address1')
137 || $self->ut_textn('address2')
138 || $self->ut_text('city')
139 || $self->ut_textn('state')
142 return $error if $error;
144 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
145 $self->setfield('last',$1);
147 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
150 $error = $self->payinfo_check();
151 return $error if $error;
153 if ( $self->exp eq '' ) {
154 return "Expiration date required"
155 unless $self->payby =~ /^(CHEK|DCHK|LECB|WEST)$/;
158 if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
159 $self->exp("$1-$2-$3");
160 } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
161 if ( length($2) == 4 ) {
162 $self->exp("$2-$1-01");
163 } elsif ( $2 > 98 ) { #should pry change to check for "this year"
164 $self->exp("19$2-$1-01");
166 $self->exp("20$2-$1-01");
169 return "Illegal expiration date";
173 if ( $self->payname eq '' ) {
174 $self->payname( $self->first. " ". $self->getfield('last') );
176 $self->payname =~ /^([\w \,\.\-\']+)$/
177 or return "Illegal billing name";
181 #we have lots of old zips in there... don't hork up batch results cause of em
182 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
183 or return "Illegal zip: ". $self->zip;
186 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
189 #$error = $self->ut_zip('zip', $self->country);
190 #return $error if $error;
192 #check invnum, custnum, ?
199 Returns the customer (see L<FS::cust_main>) for this batched credit card
206 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
211 Returns the credit card expiration date in MMYY format. If this is a
212 CHEK payment, returns an empty string.
218 if ( $self->payby eq 'CARD' ) {
219 $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/;
220 return sprintf('%02u%02u', $2, ($1 % 100));
229 Returns the payment batch this payment belongs to (L<FS::pay_batch).
235 FS::pay_batch->by_key($self->batchnum);
238 #you know what, screw this in the new world of events. we should be able to
239 #get the event defs to retry (remove once.pm condition, add every.pm) without
240 #mucking about with statuses of previous cust_event records. right?
244 #Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
245 #credit card payment as retriable. Useful if the corresponding financial
246 #institution account was declined for temporary reasons and/or a manual
249 #Implementation details: For the named customer's invoice, changes the
250 #statustext of the 'done' (without statustext) event to 'retriable.'
256 confess "deprecated method cust_pay_batch->retriable called; try removing ".
257 "the once condition and adding an every condition?";
261 local $SIG{HUP} = 'IGNORE'; #Hmm
262 local $SIG{INT} = 'IGNORE';
263 local $SIG{QUIT} = 'IGNORE';
264 local $SIG{TERM} = 'IGNORE';
265 local $SIG{TSTP} = 'IGNORE';
266 local $SIG{PIPE} = 'IGNORE';
268 my $oldAutoCommit = $FS::UID::AutoCommit;
269 local $FS::UID::AutoCommit = 0;
272 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
273 or return "event $self->eventnum references nonexistant invoice $self->invnum";
275 warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
276 my @cust_bill_event =
277 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
279 $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
280 && $_->status eq 'done'
283 $cust_bill->cust_bill_event;
284 # complain loudly if scalar(@cust_bill_event) > 1 ?
285 my $error = $cust_bill_event[0]->retriable;
287 # gah, even with transactions.
288 $dbh->commit if $oldAutoCommit; #well.
289 return "error marking invoice event retriable: $error";
294 =item approve OPTIONS
296 Approve this payment. This will replace the existing record with the
297 same paybatchnum, set its status to 'Approved', and generate a payment
298 record (L<FS::cust_pay>). This should only be called from the batch
301 OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
306 # to break up the Big Wall of Code that is import_results
309 my $paybatchnum = $new->paybatchnum;
310 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
311 or return "paybatchnum $paybatchnum not found";
312 # leave these restrictions in place until TD EFT is converted over
314 return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
316 $new->status('Approved');
317 my $error = $new->replace($old);
319 return "error updating status of paybatchnum $paybatchnum: $error\n";
321 my $cust_pay = new FS::cust_pay ( {
322 'custnum' => $new->custnum,
323 'payby' => $new->payby,
324 'payinfo' => $new->payinfo || $old->payinfo,
325 'paid' => $new->paid,
326 '_date' => $new->_date,
327 'usernum' => $new->usernum,
328 'batchnum' => $new->batchnum,
329 'gatewaynum' => $opt{'gatewaynum'},
330 'processor' => $opt{'processor'},
331 'auth' => $opt{'auth'},
332 'order_number' => $opt{'order_number'}
335 $error = $cust_pay->insert;
337 return "error inserting payment for paybatchnum $paybatchnum: $error\n";
339 $cust_pay->cust_main->apply_payments;
343 =item decline [ REASON ]
345 Decline this payment. This will replace the existing record with the
346 same paybatchnum, set its status to 'Declined', and run collection events
347 as appropriate. This should only be called from the batch import process.
349 REASON is a string description of the decline reason, defaulting to
356 my $reason = shift || 'Returned payment';
357 #my $conf = new FS::Conf;
359 my $paybatchnum = $new->paybatchnum;
360 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
361 or return "paybatchnum $paybatchnum not found";
362 if ( $old->status ) {
363 # Handle the case where payments are rejected after the batch has been
364 # approved. FS::pay_batch::import_results won't allow results to be
365 # imported to a closed batch unless batch-manual_approval is enabled,
366 # so we don't check it here.
367 # if ( $conf->exists('batch-manual_approval') and
368 if ( lc($old->status) eq 'approved' ) {
370 my $cust_pay = qsearchs('cust_pay', {
371 custnum => $new->custnum,
372 batchnum => $new->batchnum
374 # these should all be migrated over, but if it's not found, look for
375 # batchnum in the 'paybatch' field also
376 $cust_pay ||= qsearchs('cust_pay', {
377 custnum => $new->custnum,
378 paybatch => $new->batchnum
381 # should never happen...
382 return "failed to revoke paybatchnum $paybatchnum, payment not found";
384 $cust_pay->void($reason);
387 # normal case: refuse to do anything
388 return "paybatchnum $paybatchnum already resolved ('".$old->status."')";
391 $new->status('Declined');
392 $new->error_message($reason);
393 my $error = $new->replace($old);
395 return "error updating status of paybatchnum $paybatchnum: $error\n";
397 my $due_cust_event = $new->cust_main->due_cust_event(
398 'eventtable' => 'cust_pay_batch',
399 'objects' => [ $new ],
401 if ( !ref($due_cust_event) ) {
402 return $due_cust_event;
404 # XXX breaks transaction integrity
405 foreach my $cust_event (@$due_cust_event) {
406 next unless $cust_event->test_conditions;
407 if ( my $error = $cust_event->do_event() ) {
414 =item request_item [ OPTIONS ]
416 Returns a L<Business::BatchPayment::Item> object for this batch payment
417 entry. This can be submitted to a processor.
419 OPTIONS can be a list of key/values to append to the attributes. The most
420 useful case of this is "process_date" to set a processing date based on the
421 date the batch is being submitted.
429 eval "use Business::BatchPayment;";
430 die "couldn't load Business::BatchPayment: $@" if $@;
432 my $cust_main = $self->cust_main;
433 my $location = $cust_main->bill_location;
434 my $pay_batch = $self->pay_batch;
437 $payment{payment_type} = FS::payby->payby2bop( $pay_batch->payby );
438 if ( $payment{payment_type} eq 'CC' ) {
439 $payment{card_number} = $self->payinfo,
440 $payment{expiration} = $self->expmmyy,
441 } elsif ( $payment{payment_type} eq 'ECHECK' ) {
442 $self->payinfo =~ /(\d+)@(\d+)/; # or else what?
443 $payment{account_number} = $1;
444 $payment{routing_code} = $2;
445 $payment{account_type} = $cust_main->paytype;
446 # XXX what if this isn't their regular payment method?
448 die "unsupported BatchPayment method: ".$pay_batch->payby;
451 Business::BatchPayment->create(Item =>
454 tid => $self->paybatchnum,
455 amount => $self->amount,
458 customer_id => $self->custnum,
459 first_name => $cust_main->first,
460 last_name => $cust_main->last,
461 company => $cust_main->company,
462 address => $location->address1,
463 ( map { $_ => $location->$_ } qw(address2 city state country zip) ),
465 invoice_number => $self->invnum,
474 There should probably be a configuration file with a list of allowed credit
479 L<FS::cust_main>, L<FS::Record>