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('cardnum')
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";
140 my $cardnum = $self->cardnum;
142 $cardnum =~ /^(\d{13,16})$/
143 or return "Illegal credit card number";
145 $self->cardnum($cardnum);
146 validate($cardnum) or return "Illegal credit card number";
147 return "Unknown card type" if cardtype($cardnum) eq "Unknown";
149 if ( $self->exp eq '' ) {
150 return "Expriation date required"; #unless
153 if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
154 $self->exp("$1-$2-$3");
155 } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
156 if ( length($2) == 4 ) {
157 $self->exp("$2-$1-01");
158 } elsif ( $2 > 98 ) { #should pry change to check for "this year"
159 $self->exp("19$2-$1-01");
161 $self->exp("20$2-$1-01");
164 return "Illegal expiration date";
168 if ( $self->payname eq '' ) {
169 $self->payname( $self->first. " ". $self->getfield('last') );
171 $self->payname =~ /^([\w \,\.\-\']+)$/
172 or return "Illegal billing name";
176 #$self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
177 # or return "Illegal zip: ". $self->zip;
180 $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
183 $error = $self->ut_zip('zip', $self->country);
184 return $error if $error;
186 #check invnum, custnum, ?
193 Returns the customer (see L<FS::cust_main>) for this batched credit card
200 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
216 eval "use Text::CSV_XS;";
220 my $fh = $param->{'filehandle'};
221 my $format = $param->{'format'};
222 my $paybatch = $param->{'paybatch'};
228 my $approved_condition;
229 my $declined_condition;
231 if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) {
234 'paybatchnum', # Reference#: Invoice number of the transaction
235 'paid', # Amount: Amount of the transaction. Dollars and cents
236 # with no decimal entered.
237 '', # Card Type: 0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover,
238 # 4 - Insignia, 5 - Diners/EnRoute, 6 - JCB
239 '_date', # Transaction Date: Date the Transaction was processed
240 'time', # Transaction Time: Time the transaction was processed
241 'payinfo', # Card Number: Card number for the transaction
242 '', # Expiry Date: Expiry date of the card
243 '', # Auth#: Authorization number entered for force post
245 'type', # Transaction Type: 0 - purchase, 40 - refund,
247 'result', # Processing Result: 3 - Approval,
248 # 4 - Declined/Amount over limit,
249 # 5 - Invalid/Expired/stolen card,
251 '', # Terminal ID: Terminal ID used to process the transaction
254 $end_condition = sub {
256 $hash->{'type'} eq '0BC';
260 my( $hash, $total) = @_;
261 $total = sprintf("%.2f", $total);
262 my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 );
263 return "Our total $total does not match bank total $batch_total!"
264 if $total != $batch_total;
270 $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
271 $hash->{'_date'} = timelocal( substr($hash->{'time'}, 4, 2),
272 substr($hash->{'time'}, 2, 2),
273 substr($hash->{'time'}, 0, 2),
274 substr($hash->{'_date'}, 6, 2),
275 substr($hash->{'_date'}, 4, 2)-1,
276 substr($hash->{'_date'}, 0, 4)-1900, );
279 $approved_condition = sub {
281 $hash->{'type'} eq '0' && $hash->{'result'} == 3;
284 $declined_condition = sub {
286 $hash->{'type'} eq '0' && ( $hash->{'result'} == 4
287 || $hash->{'result'} == 5 );
292 return "Unknown format $format";
295 my $csv = new Text::CSV_XS;
297 local $SIG{HUP} = 'IGNORE';
298 local $SIG{INT} = 'IGNORE';
299 local $SIG{QUIT} = 'IGNORE';
300 local $SIG{TERM} = 'IGNORE';
301 local $SIG{TSTP} = 'IGNORE';
302 local $SIG{PIPE} = 'IGNORE';
304 my $oldAutoCommit = $FS::UID::AutoCommit;
305 local $FS::UID::AutoCommit = 0;
310 while ( defined($line=<$fh>) ) {
312 next if $line =~ /^\s*$/; #skip blank lines
314 $csv->parse($line) or do {
315 $dbh->rollback if $oldAutoCommit;
316 return "can't parse: ". $csv->error_input();
319 my @values = $csv->fields();
321 foreach my $field ( @fields ) {
322 my $value = shift @values;
324 $hash{$field} = $value;
327 if ( &{$end_condition}(\%hash) ) {
328 my $error = &{$end_hook}(\%hash, $total);
330 $dbh->rollback if $oldAutoCommit;
337 qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'} } );
338 unless ( $cust_pay_batch ) {
339 $dbh->rollback if $oldAutoCommit;
340 return "unknown paybatchnum $hash{'paybatchnum'}\n";
342 my $custnum = $cust_pay_batch->custnum,
344 my $error = $cust_pay_batch->delete;
346 $dbh->rollback if $oldAutoCommit;
347 return "error removing paybatchnum $hash{'paybatchnum'}: $error\n";
352 if ( &{$approved_condition}(\%hash) ) {
354 my $cust_pay = new FS::cust_pay ( {
355 'custnum' => $custnum,
357 'paybatch' => $paybatch,
358 map { $_ => $hash{$_} } (qw( paid _date payinfo )),
360 $error = $cust_pay->insert;
362 $dbh->rollback if $oldAutoCommit;
363 return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
365 $total += $hash{'paid'};
367 $cust_pay->cust_main->apply_payments;
369 } elsif ( &{$declined_condition}(\%hash) ) {
371 #this should be configurable... if anybody else ever uses batches
372 $cust_pay_batch->cust_main->suspend;
378 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
387 There should probably be a configuration file with a list of allowed credit
392 L<FS::cust_main>, L<FS::Record>