1 package FS::cust_pay_batch;
2 use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
6 use Carp qw( 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
133 $self->ut_numbern('paybatchnum')
134 || $self->ut_numbern('trancode') #deprecated
135 || $self->ut_money('amount')
136 || $self->ut_number('invnum')
137 || $self->ut_number('custnum')
138 || $self->ut_text('address1')
139 || $self->ut_textn('address2')
140 || $self->ut_text('city')
141 || $self->ut_textn('state')
144 return $error if $error;
146 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
147 $self->setfield('last',$1);
149 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
152 $error = $self->payinfo_check();
153 return $error if $error;
155 if ( $self->exp eq '' ) {
156 return "Expiration date required"
157 unless $self->payby =~ /^(CHEK|DCHK|WEST)$/;
160 if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
161 $self->exp("$1-$2-$3");
162 } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
163 if ( length($2) == 4 ) {
164 $self->exp("$2-$1-01");
165 } elsif ( $2 > 98 ) { #should pry change to check for "this year"
166 $self->exp("19$2-$1-01");
168 $self->exp("20$2-$1-01");
171 return "Illegal expiration date";
175 if ( $self->payname eq '' ) {
176 $self->payname( $self->first. " ". $self->getfield('last') );
178 $self->payname =~ /^([\w \,\.\-\']+)$/
179 or return "Illegal billing name";
183 #we have lots of old zips in there... don't hork up batch results cause of em
184 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
185 or return "Illegal zip: ". $self->zip;
188 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
191 #$error = $self->ut_zip('zip', $self->country);
192 #return $error if $error;
194 #check invnum, custnum, ?
201 Returns the customer (see L<FS::cust_main>) for this batched credit card
206 Returns the credit card expiration date in MMYY format. If this is a
207 CHEK payment, returns an empty string.
213 if ( $self->payby eq 'CARD' ) {
214 $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/;
215 return sprintf('%02u%02u', $2, ($1 % 100));
224 Returns the payment batch this payment belongs to (L<FS::pay_batch).
228 #you know what, screw this in the new world of events. we should be able to
229 #get the event defs to retry (remove once.pm condition, add every.pm) without
230 #mucking about with statuses of previous cust_event records. right?
234 #Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
235 #credit card payment as retriable. Useful if the corresponding financial
236 #institution account was declined for temporary reasons and/or a manual
239 #Implementation details: For the named customer's invoice, changes the
240 #statustext of the 'done' (without statustext) event to 'retriable.'
246 confess "deprecated method cust_pay_batch->retriable called; try removing ".
247 "the once condition and adding an every condition?";
251 =item approve OPTIONS
253 Approve this payment. This will replace the existing record with the
254 same paybatchnum, set its status to 'Approved', and generate a payment
255 record (L<FS::cust_pay>). This should only be called from the batch
258 OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
263 # to break up the Big Wall of Code that is import_results
266 my $paybatchnum = $new->paybatchnum;
267 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
268 or return "paybatchnum $paybatchnum not found";
269 # leave these restrictions in place until TD EFT is converted over
271 return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
273 $new->status('Approved');
274 my $error = $new->replace($old);
276 return "error updating status of paybatchnum $paybatchnum: $error\n";
278 my $cust_pay = new FS::cust_pay ( {
279 'custnum' => $new->custnum,
280 'payby' => $new->payby,
281 'payinfo' => $new->payinfo || $old->payinfo,
282 'paid' => $new->paid,
283 '_date' => $new->_date,
284 'usernum' => $new->usernum,
285 'batchnum' => $new->batchnum,
286 'gatewaynum' => $opt{'gatewaynum'},
287 'processor' => $opt{'processor'},
288 'auth' => $opt{'auth'},
289 'order_number' => $opt{'order_number'}
292 $error = $cust_pay->insert;
294 return "error inserting payment for paybatchnum $paybatchnum: $error\n";
296 $cust_pay->cust_main->apply_payments;
300 =item decline [ REASON [ STATUS ] ]
302 Decline this payment. This will replace the existing record with the
303 same paybatchnum, set its status to 'Declined', and run collection events
304 as appropriate. This should only be called from the batch import process.
306 REASON is a string description of the decline reason, defaulting to
307 'Returned payment', and will go into the "error_message" field.
309 STATUS is a normalized failure status defined by L<Business::BatchPayment>,
310 and will go into the "failure_status" field.
316 my $reason = shift || 'Returned payment';
317 my $failure_status = shift || '';
318 #my $conf = new FS::Conf;
320 my $paybatchnum = $new->paybatchnum;
321 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
322 or return "paybatchnum $paybatchnum not found";
323 if ( $old->status ) {
324 # Handle the case where payments are rejected after the batch has been
325 # approved. FS::pay_batch::import_results won't allow results to be
326 # imported to a closed batch unless batch-manual_approval is enabled,
327 # so we don't check it here.
328 # if ( $conf->exists('batch-manual_approval') and
329 if ( lc($old->status) eq 'approved' ) {
331 my $cust_pay = qsearchs('cust_pay', {
332 custnum => $new->custnum,
333 batchnum => $new->batchnum
335 # these should all be migrated over, but if it's not found, look for
336 # batchnum in the 'paybatch' field also
337 $cust_pay ||= qsearchs('cust_pay', {
338 custnum => $new->custnum,
339 paybatch => $new->batchnum
342 # should never happen...
343 return "failed to revoke paybatchnum $paybatchnum, payment not found";
345 $cust_pay->void($reason);
348 # normal case: refuse to do anything
349 return "paybatchnum $paybatchnum already resolved ('".$old->status."')";
352 $new->status('Declined');
353 $new->error_message($reason);
354 $new->failure_status($failure_status);
355 my $error = $new->replace($old);
357 return "error updating status of paybatchnum $paybatchnum: $error\n";
359 my $due_cust_event = $new->cust_main->due_cust_event(
360 'eventtable' => 'cust_pay_batch',
361 'objects' => [ $new ],
363 if ( !ref($due_cust_event) ) {
364 return $due_cust_event;
366 # XXX breaks transaction integrity
367 foreach my $cust_event (@$due_cust_event) {
368 next unless $cust_event->test_conditions;
369 if ( my $error = $cust_event->do_event() ) {
376 =item request_item [ OPTIONS ]
378 Returns a L<Business::BatchPayment::Item> object for this batch payment
379 entry. This can be submitted to a processor.
381 OPTIONS can be a list of key/values to append to the attributes. The most
382 useful case of this is "process_date" to set a processing date based on the
383 date the batch is being submitted.
391 eval "use Business::BatchPayment;";
392 die "couldn't load Business::BatchPayment: $@" if $@;
394 my $cust_main = $self->cust_main;
395 my $location = $cust_main->bill_location;
396 my $pay_batch = $self->pay_batch;
399 $payment{payment_type} = FS::payby->payby2bop( $pay_batch->payby );
400 if ( $payment{payment_type} eq 'CC' ) {
401 $payment{card_number} = $self->payinfo,
402 $payment{expiration} = $self->expmmyy,
403 } elsif ( $payment{payment_type} eq 'ECHECK' ) {
404 $self->payinfo =~ /(\d+)@(\d+)/; # or else what?
405 $payment{account_number} = $1;
406 $payment{routing_code} = $2;
407 $payment{account_type} = $cust_main->paytype;
408 # XXX what if this isn't their regular payment method?
410 die "unsupported BatchPayment method: ".$pay_batch->payby;
413 Business::BatchPayment->create(Item =>
416 tid => $self->paybatchnum,
417 amount => $self->amount,
420 customer_id => $self->custnum,
421 first_name => $cust_main->first,
422 last_name => $cust_main->last,
423 company => $cust_main->company,
424 address => $location->address1,
425 ( map { $_ => $location->$_ } qw(address2 city state country zip) ),
427 invoice_number => $self->invnum,
436 There should probably be a configuration file with a list of allowed credit
441 L<FS::cust_main>, L<FS::Record>