1 package FS::cust_pay_batch;
5 use FS::Record qw(dbh qsearchs);
6 use Business::CreditCard;
8 @ISA = qw( FS::Record );
12 FS::cust_pay_batch - Object methods for batch cards
16 use FS::cust_pay_batch;
18 $record = new FS::cust_pay_batch \%hash;
19 $record = new FS::cust_pay_batch { 'column' => 'value' };
21 $error = $record->insert;
23 $error = $new_record->replace($old_record);
25 $error = $record->delete;
27 $error = $record->check;
31 An FS::cust_pay_batch object represents a credit card transaction ready to be
32 batched (sent to a processor). FS::cust_pay_batch inherits from FS::Record.
33 Typically called by the collect method of an FS::cust_main object. The
34 following fields are currently supported:
38 =item paybatchnum - primary key (automatically assigned)
42 =item exp - card expiration
46 =item invnum - invoice
48 =item custnum - customer
50 =item payname - name on card
76 Creates a new record. To add the record to the database, see L<"insert">.
78 Note that this stores the hash reference, not a distinct copy of the hash it
79 points to. You can ask the object for a copy with the I<hash> method.
83 sub table { 'cust_pay_batch'; }
87 Adds this record to the database. If there is an error, returns the error,
88 otherwise returns false.
92 Delete this record from the database. If there is an error, returns the error,
93 otherwise returns false.
95 =item replace OLD_RECORD
99 #Replaces the OLD_RECORD with this one in the database. If there is an error,
100 #returns the error, otherwise returns false.
105 return "Can't (yet?) replace batched transactions!";
110 Checks all fields to make sure this is a valid transaction. If there is
111 an error, returns the error, otherwise returns false. Called by the insert
120 $self->ut_numbern('paybatchnum')
121 || $self->ut_numbern('trancode') #depriciated
122 || $self->ut_number('payinfo')
123 || $self->ut_money('amount')
124 || $self->ut_number('invnum')
125 || $self->ut_number('custnum')
126 || $self->ut_text('address1')
127 || $self->ut_textn('address2')
128 || $self->ut_text('city')
129 || $self->ut_textn('state')
132 return $error if $error;
134 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
135 $self->setfield('last',$1);
137 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
141 # there is no point in false laziness here
142 # we will effectively set "check_payinfo to 0"
143 # we can change that when we finish the refactor
145 #my $cardnum = $self->cardnum;
146 #$cardnum =~ s/\D//g;
147 #$cardnum =~ /^(\d{13,16})$/
148 # or return "Illegal credit card number";
150 #$self->cardnum($cardnum);
151 #validate($cardnum) or return "Illegal credit card number";
152 #return "Unknown card type" if cardtype($cardnum) eq "Unknown";
154 if ( $self->exp eq '' ) {
155 return "Expiration date required"; #unless
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 #$self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
182 # or return "Illegal zip: ". $self->zip;
185 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
188 $error = $self->ut_zip('zip', $self->country);
189 return $error if $error;
191 #check invnum, custnum, ?
198 Returns the customer (see L<FS::cust_main>) for this batched credit card
205 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
221 eval "use Text::CSV_XS;";
225 my $fh = $param->{'filehandle'};
226 my $format = $param->{'format'};
227 my $paybatch = $param->{'paybatch'};
233 my $approved_condition;
234 my $declined_condition;
236 if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) {
239 'paybatchnum', # Reference#: Invoice number of the transaction
240 'paid', # Amount: Amount of the transaction. Dollars and cents
241 # with no decimal entered.
242 '', # Card Type: 0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover,
243 # 4 - Insignia, 5 - Diners/EnRoute, 6 - JCB
244 '_date', # Transaction Date: Date the Transaction was processed
245 'time', # Transaction Time: Time the transaction was processed
246 'payinfo', # Card Number: Card number for the transaction
247 '', # Expiry Date: Expiry date of the card
248 '', # Auth#: Authorization number entered for force post
250 'type', # Transaction Type: 0 - purchase, 40 - refund,
252 'result', # Processing Result: 3 - Approval,
253 # 4 - Declined/Amount over limit,
254 # 5 - Invalid/Expired/stolen card,
256 '', # Terminal ID: Terminal ID used to process the transaction
259 $end_condition = sub {
261 $hash->{'type'} eq '0BC';
265 my( $hash, $total) = @_;
266 $total = sprintf("%.2f", $total);
267 my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 );
268 return "Our total $total does not match bank total $batch_total!"
269 if $total != $batch_total;
275 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
276 $hash->{'_date'} = timelocal( substr($hash->{'time'}, 4, 2),
277 substr($hash->{'time'}, 2, 2),
278 substr($hash->{'time'}, 0, 2),
279 substr($hash->{'_date'}, 6, 2),
280 substr($hash->{'_date'}, 4, 2)-1,
281 substr($hash->{'_date'}, 0, 4)-1900, );
284 $approved_condition = sub {
286 $hash->{'type'} eq '0' && $hash->{'result'} == 3;
289 $declined_condition = sub {
291 $hash->{'type'} eq '0' && ( $hash->{'result'} == 4
292 || $hash->{'result'} == 5 );
297 return "Unknown format $format";
300 my $csv = new Text::CSV_XS;
302 local $SIG{HUP} = 'IGNORE';
303 local $SIG{INT} = 'IGNORE';
304 local $SIG{QUIT} = 'IGNORE';
305 local $SIG{TERM} = 'IGNORE';
306 local $SIG{TSTP} = 'IGNORE';
307 local $SIG{PIPE} = 'IGNORE';
309 my $oldAutoCommit = $FS::UID::AutoCommit;
310 local $FS::UID::AutoCommit = 0;
313 my $pay_batch = qsearchs('pay_batch',{'batchnum'=> $paybatch});
314 unless ($pay_batch && $pay_batch->status eq 'I') {
315 $dbh->rollback if $oldAutoCommit;
316 return "batch $paybatch is not in transit";
319 my %batchhash = $pay_batch->hash;
320 $batchhash{'status'} = 'R'; # Resolved
321 my $newbatch = new FS::pay_batch ( \%batchhash );
322 my $error = $newbatch->replace($paybatch);
324 $dbh->rollback if $oldAutoCommit;
330 while ( defined($line=<$fh>) ) {
332 next if $line =~ /^\s*$/; #skip blank lines
334 $csv->parse($line) or do {
335 $dbh->rollback if $oldAutoCommit;
336 return "can't parse: ". $csv->error_input();
339 my @values = $csv->fields();
341 foreach my $field ( @fields ) {
342 my $value = shift @values;
344 $hash{$field} = $value;
347 if ( &{$end_condition}(\%hash) ) {
348 my $error = &{$end_hook}(\%hash, $total);
350 $dbh->rollback if $oldAutoCommit;
357 qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'} } );
358 unless ( $cust_pay_batch ) {
359 $dbh->rollback if $oldAutoCommit;
360 return "unknown paybatchnum $hash{'paybatchnum'}\n";
362 my $custnum = $cust_pay_batch->custnum,
364 my $error = $cust_pay_batch->delete;
366 $dbh->rollback if $oldAutoCommit;
367 return "error removing paybatchnum $hash{'paybatchnum'}: $error\n";
372 if ( &{$approved_condition}(\%hash) ) {
374 my $cust_pay = new FS::cust_pay ( {
375 'custnum' => $custnum,
377 'paybatch' => $paybatch,
378 map { $_ => $hash{$_} } (qw( paid _date payinfo )),
380 $error = $cust_pay->insert;
382 $dbh->rollback if $oldAutoCommit;
383 return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
385 $total += $hash{'paid'};
387 $cust_pay->cust_main->apply_payments;
389 } elsif ( &{$declined_condition}(\%hash) ) {
391 #this should be configurable... if anybody else ever uses batches
392 $cust_pay_batch->cust_main->suspend;
398 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
407 There should probably be a configuration file with a list of allowed credit
412 L<FS::cust_main>, L<FS::Record>