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
98 Creates a new record. To add the record to the database, see L<"insert">.
100 Note that this stores the hash reference, not a distinct copy of the hash it
101 points to. You can ask the object for a copy with the I<hash> method.
105 sub table { 'cust_pay_batch'; }
109 Adds this record to the database. If there is an error, returns the error,
110 otherwise returns false.
114 Delete this record from the database. If there is an error, returns the error,
115 otherwise returns false.
117 =item replace OLD_RECORD
119 Replaces the OLD_RECORD with this one in the database. If there is an error,
120 returns the error, otherwise returns false.
124 Checks all fields to make sure this is a valid transaction. If there is
125 an error, returns the error, otherwise returns false. Called by the insert
134 $self->ut_numbern('paybatchnum')
135 || $self->ut_numbern('trancode') #deprecated
136 || $self->ut_money('amount')
137 || $self->ut_number('invnum')
138 || $self->ut_number('custnum')
139 || $self->ut_text('address1')
140 || $self->ut_textn('address2')
141 || $self->ut_text('city')
142 || $self->ut_textn('state')
145 return $error if $error;
147 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
148 $self->setfield('last',$1);
150 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
153 $error = $self->payinfo_check();
154 return $error if $error;
156 if ( $self->exp eq '' ) {
157 return "Expiration date required"
158 unless $self->payby =~ /^(CHEK|DCHK|LECB|WEST)$/;
161 if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
162 $self->exp("$1-$2-$3");
163 } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
164 if ( length($2) == 4 ) {
165 $self->exp("$2-$1-01");
166 } elsif ( $2 > 98 ) { #should pry change to check for "this year"
167 $self->exp("19$2-$1-01");
169 $self->exp("20$2-$1-01");
172 return "Illegal expiration date";
176 if ( $self->payname eq '' ) {
177 $self->payname( $self->first. " ". $self->getfield('last') );
179 $self->payname =~ /^([\w \,\.\-\']+)$/
180 or return "Illegal billing name";
184 #we have lots of old zips in there... don't hork up batch results cause of em
185 $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
186 or return "Illegal zip: ". $self->zip;
189 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
192 #$error = $self->ut_zip('zip', $self->country);
193 #return $error if $error;
195 #check invnum, custnum, ?
202 Returns the customer (see L<FS::cust_main>) for this batched credit card
209 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
214 Returns the payment batch this payment belongs to (L<FS::pay_batch).
220 FS::pay_batch->by_key($self->batchnum);
223 #you know what, screw this in the new world of events. we should be able to
224 #get the event defs to retry (remove once.pm condition, add every.pm) without
225 #mucking about with statuses of previous cust_event records. right?
229 #Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
230 #credit card payment as retriable. Useful if the corresponding financial
231 #institution account was declined for temporary reasons and/or a manual
234 #Implementation details: For the named customer's invoice, changes the
235 #statustext of the 'done' (without statustext) event to 'retriable.'
241 confess "deprecated method cust_pay_batch->retriable called; try removing ".
242 "the once condition and adding an every condition?";
246 local $SIG{HUP} = 'IGNORE'; #Hmm
247 local $SIG{INT} = 'IGNORE';
248 local $SIG{QUIT} = 'IGNORE';
249 local $SIG{TERM} = 'IGNORE';
250 local $SIG{TSTP} = 'IGNORE';
251 local $SIG{PIPE} = 'IGNORE';
253 my $oldAutoCommit = $FS::UID::AutoCommit;
254 local $FS::UID::AutoCommit = 0;
257 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
258 or return "event $self->eventnum references nonexistant invoice $self->invnum";
260 warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
261 my @cust_bill_event =
262 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
264 $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
265 && $_->status eq 'done'
268 $cust_bill->cust_bill_event;
269 # complain loudly if scalar(@cust_bill_event) > 1 ?
270 my $error = $cust_bill_event[0]->retriable;
272 # gah, even with transactions.
273 $dbh->commit if $oldAutoCommit; #well.
274 return "error marking invoice event retriable: $error";
279 =item approve PAYBATCH
281 Approve this payment. This will replace the existing record with the
282 same paybatchnum, set its status to 'Approved', and generate a payment
283 record (L<FS::cust_pay>). This should only be called from the batch
289 # to break up the Big Wall of Code that is import_results
291 my $paybatch = shift;
292 my $paybatchnum = $new->paybatchnum;
293 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
294 or return "paybatchnum $paybatchnum not found";
295 return "paybatchnum $paybatchnum already resolved ('".$old->status."')"
297 $new->status('Approved');
298 my $error = $new->replace($old);
300 return "error updating status of paybatchnum $paybatchnum: $error\n";
302 my $cust_pay = new FS::cust_pay ( {
303 'custnum' => $new->custnum,
304 'payby' => $new->payby,
305 'paybatch' => $paybatch,
306 'payinfo' => $new->payinfo || $old->payinfo,
307 'paid' => $new->paid,
308 '_date' => $new->_date,
309 'usernum' => $new->usernum,
310 'batchnum' => $new->batchnum,
312 $error = $cust_pay->insert;
314 return "error inserting payment for paybatchnum $paybatchnum: $error\n";
316 $cust_pay->cust_main->apply_payments;
320 =item decline [ REASON ]
322 Decline this payment. This will replace the existing record with the
323 same paybatchnum, set its status to 'Declined', and run collection events
324 as appropriate. This should only be called from the batch import process.
326 REASON is a string description of the decline reason, defaulting to
333 my $reason = shift || 'Returned payment';
334 #my $conf = new FS::Conf;
336 my $paybatchnum = $new->paybatchnum;
337 my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
338 or return "paybatchnum $paybatchnum not found";
339 if ( $old->status ) {
340 # Handle the case where payments are rejected after the batch has been
341 # approved. FS::pay_batch::import_results won't allow results to be
342 # imported to a closed batch unless batch-manual_approval is enabled,
343 # so we don't check it here.
344 # if ( $conf->exists('batch-manual_approval') and
345 if ( lc($old->status) eq 'approved' ) {
347 my $cust_pay = qsearchs('cust_pay', {
348 custnum => $new->custnum,
349 batchnum => $new->batchnum
352 $cust_pay ||= qsearchs('cust_pay', {
353 custnum => $new->custnum,
354 paybatch => $new->batchnum
357 # should never happen...
358 return "failed to revoke paybatchnum $paybatchnum, payment not found";
360 $cust_pay->void($reason);
363 # normal case: refuse to do anything
364 return "paybatchnum $paybatchnum already resolved ('".$old->status."')";
367 $new->status('Declined');
368 $new->error_message($reason);
369 my $error = $new->replace($old);
371 return "error updating status of paybatchnum $paybatchnum: $error\n";
373 my $due_cust_event = $new->cust_main->due_cust_event(
374 'eventtable' => 'cust_pay_batch',
375 'objects' => [ $new ],
377 if ( !ref($due_cust_event) ) {
378 return $due_cust_event;
380 # XXX breaks transaction integrity
381 foreach my $cust_event (@$due_cust_event) {
382 next unless $cust_event->test_conditions;
383 if ( my $error = $cust_event->do_event() ) {
394 There should probably be a configuration file with a list of allowed credit
399 L<FS::cust_main>, L<FS::Record>