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::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
93 Creates a new record. To add the record to the database, see L<"insert">.
95 Note that this stores the hash reference, not a distinct copy of the hash it
96 points to. You can ask the object for a copy with the I<hash> method.
100 sub table { 'cust_pay_batch'; }
104 Adds this record to the database. If there is an error, returns the error,
105 otherwise returns false.
109 Delete this record from the database. If there is an error, returns the error,
110 otherwise returns false.
112 =item replace OLD_RECORD
114 Replaces the OLD_RECORD with this one in the database. If there is an error,
115 returns the error, otherwise returns false.
119 Checks all fields to make sure this is a valid transaction. If there is
120 an error, returns the error, otherwise returns false. Called by the insert
129 $self->ut_numbern('paybatchnum')
130 || $self->ut_numbern('trancode') #deprecated
131 || $self->ut_money('amount')
132 || $self->ut_number('invnum')
133 || $self->ut_number('custnum')
134 || $self->ut_text('address1')
135 || $self->ut_textn('address2')
136 || $self->ut_text('city')
137 || $self->ut_textn('state')
140 return $error if $error;
142 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
143 $self->setfield('last',$1);
145 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
148 $error = $self->payinfo_check();
149 return $error if $error;
151 if ( $self->exp eq '' ) {
152 return "Expiration date required"
153 unless $self->payby =~ /^(CHEK|DCHK|LECB|WEST)$/;
156 if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
157 $self->exp("$1-$2-$3");
158 } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
159 if ( length($2) == 4 ) {
160 $self->exp("$2-$1-01");
161 } elsif ( $2 > 98 ) { #should pry change to check for "this year"
162 $self->exp("19$2-$1-01");
164 $self->exp("20$2-$1-01");
167 return "Illegal expiration date";
171 if ( $self->payname eq '' ) {
172 $self->payname( $self->first. " ". $self->getfield('last') );
174 $self->payname =~ /^([\w \,\.\-\']+)$/
175 or return "Illegal billing name";
179 #we have lots of old zips in there... don't hork up batch results cause of em
180 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
181 or return "Illegal zip: ". $self->zip;
184 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
187 #$error = $self->ut_zip('zip', $self->country);
188 #return $error if $error;
190 #check invnum, custnum, ?
197 Returns the customer (see L<FS::cust_main>) for this batched credit card
204 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
209 Returns the credit card expiration date in MMYY format. If this is a
210 CHEK payment, returns an empty string.
216 if ( $self->payby eq 'CARD' ) {
217 $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/;
218 return sprintf('%02u%02u', $2, ($1 % 100));
227 Returns the payment batch this payment belongs to (L<FS::pay_batch).
233 FS::pay_batch->by_key($self->batchnum);
236 #you know what, screw this in the new world of events. we should be able to
237 #get the event defs to retry (remove once.pm condition, add every.pm) without
238 #mucking about with statuses of previous cust_event records. right?
242 #Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
243 #credit card payment as retriable. Useful if the corresponding financial
244 #institution account was declined for temporary reasons and/or a manual
247 #Implementation details: For the named customer's invoice, changes the
248 #statustext of the 'done' (without statustext) event to 'retriable.'
254 confess "deprecated method cust_pay_batch->retriable called; try removing ".
255 "the once condition and adding an every condition?";
259 local $SIG{HUP} = 'IGNORE'; #Hmm
260 local $SIG{INT} = 'IGNORE';
261 local $SIG{QUIT} = 'IGNORE';
262 local $SIG{TERM} = 'IGNORE';
263 local $SIG{TSTP} = 'IGNORE';
264 local $SIG{PIPE} = 'IGNORE';
266 my $oldAutoCommit = $FS::UID::AutoCommit;
267 local $FS::UID::AutoCommit = 0;
270 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
271 or return "event $self->eventnum references nonexistant invoice $self->invnum";
273 warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
274 my @cust_bill_event =
275 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
277 $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
278 && $_->status eq 'done'
281 $cust_bill->cust_bill_event;
282 # complain loudly if scalar(@cust_bill_event) > 1 ?
283 my $error = $cust_bill_event[0]->retriable;
285 # gah, even with transactions.
286 $dbh->commit if $oldAutoCommit; #well.
287 return "error marking invoice event retriable: $error";
292 =item approve PAYBATCH
294 Approve this payment. This will replace the existing record with the
295 same paybatchnum, set its status to 'Approved', and generate a payment
296 record (L<FS::cust_pay>). This should only be called from the batch
302 # to break up the Big Wall of Code that is import_results
304 my $paybatch = shift;
305 my $paybatchnum = $new->paybatchnum;
306 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
307 or return "paybatchnum $paybatchnum not found";
308 # leave these restrictions in place until TD EFT is converted over
310 return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
312 $new->status('Approved');
313 my $error = $new->replace($old);
315 return "error updating status of paybatchnum $paybatchnum: $error\n";
317 my $cust_pay = new FS::cust_pay ( {
318 'custnum' => $new->custnum,
319 'payby' => $new->payby,
320 'paybatch' => $paybatch,
321 'payinfo' => $new->payinfo || $old->payinfo,
322 'paid' => $new->paid,
323 '_date' => $new->_date,
324 'usernum' => $new->usernum,
325 'batchnum' => $new->batchnum,
327 $error = $cust_pay->insert;
329 return "error inserting payment for paybatchnum $paybatchnum: $error\n";
331 $cust_pay->cust_main->apply_payments;
335 =item decline [ REASON ]
337 Decline this payment. This will replace the existing record with the
338 same paybatchnum, set its status to 'Declined', and run collection events
339 as appropriate. This should only be called from the batch import process.
341 REASON is a string description of the decline reason, defaulting to
348 my $reason = shift || 'Returned payment';
349 #my $conf = new FS::Conf;
351 my $paybatchnum = $new->paybatchnum;
352 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
353 or return "paybatchnum $paybatchnum not found";
354 if ( $old->status ) {
355 # Handle the case where payments are rejected after the batch has been
356 # approved. FS::pay_batch::import_results won't allow results to be
357 # imported to a closed batch unless batch-manual_approval is enabled,
358 # so we don't check it here.
359 # if ( $conf->exists('batch-manual_approval') and
360 if ( lc($old->status) eq 'approved' ) {
362 my $cust_pay = qsearchs('cust_pay', {
363 custnum => $new->custnum,
364 paybatch => $new->batchnum
367 # should never happen...
368 return "failed to revoke paybatchnum $paybatchnum, payment not found";
370 $cust_pay->void($reason);
373 # normal case: refuse to do anything
374 return "paybatchnum $paybatchnum already resolved ('".$old->status."')";
377 $new->status('Declined');
378 my $error = $new->replace($old);
380 return "error updating status of 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} = $cust_main->paytype;
431 # XXX what if this isn't their regular payment method?
433 die "unsupported BatchPayment method: ".$pay_batch->payby;
436 Business::BatchPayment->create(Item =>
439 tid => $self->paybatchnum,
440 amount => $self->amount,
443 customer_id => $self->custnum,
444 first_name => $cust_main->first,
445 last_name => $cust_main->last,
446 company => $cust_main->company,
447 address => $location->address1,
448 ( map { $_ => $location->$_ } qw(address2 city state country zip) ),
450 invoice_number => $self->invnum,
459 There should probably be a configuration file with a list of allowed credit
464 L<FS::cust_main>, L<FS::Record>