1 package Business::BatchPayment::Paymentech;
10 Business::BatchPayment::Paymentech - Chase Paymentech XML batch format.
14 See L<Business::BatchPayment> for general usage notes.
18 use Business::BatchPayment;
20 my @items = Business::BatchPayment::Item->new( ... );
22 my $processor = Business::BatchPayment->processor('Paymentech',
23 merchantID => '123456',
29 with_recurringInd => 1,
32 my $result = $processor->submit(@items);
36 Requires L<Net::SFTP::Foreign> and ssh (for file transfer) and the zip and
37 unzip programs. Unlikely to work on non-Unix systems.
39 =head2 PROCESSOR ATTRIBUTES
43 =item login - the username to use for SFTP, and in the "userID" tag
45 =item password - the password for SFTP, and for creating zip files
47 =item merchantID - your 6- or 12-digit Paymentech merchant ID
49 =item bin - your BIN: 000001 or 000002
51 =item terminalID - your 3-digit terminal ID
53 =item industryType - your 2-letter industry type code
55 =item with_recurringInd - enable the recurring charge indicator field
61 use File::Temp qw(tempdir);
68 with 'Business::BatchPayment::Processor';
69 with 'Business::BatchPayment::TestMode';
73 # could have some validation on all of these
74 has [ qw(merchantID terminalID bin industryType login password) ] => (
80 has 'with_recurringInd' => (
86 has 'fileDateTime' => (
90 DateTime->now->strftime('%Y%m%d%H%M%S')
95 'personal checking' => 'C',
96 'personal savings' => 'S',
97 'business checking' => 'X',
98 'business savings' => 'X',
101 my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK );
103 sub default_transport {
105 Business::BatchPayment::Paymentech::Transport->new(
106 login => $self->login,
107 password => $self->password,
108 debug => $self->debug,
109 test_mode => $self->test_mode,
118 my $xml = XML::Writer->new(
124 $self->format_header($batch, $xml);
126 foreach my $item ( @{ $batch->items } ) {
128 $self->format_item($item, $batch, $xml, $count);
131 $self->format_error($item, $_);
134 $self->format_trailer($batch, $xml, $count);
139 my ($self, $batch, $xml) = @_;
140 my $num_items = $batch->count;
143 $xml->startTag('transRequest', RequestCount => $num_items + 1);
144 $xml->startTag('batchFileID');
145 $xml->dataElement(userID => $self->login);
146 $xml->dataElement(fileDateTime => $self->fileDateTime);
147 $xml->dataElement(fileID => sprintf('%06d-', $batch->batch_id) .
148 $self->fileDateTime);
149 $xml->endTag('batchFileID');
153 my ($self, $item, $batch, $xml, $count) = @_;
154 if ( $item->action eq 'payment' ) {
155 $xml->startTag('newOrder', BatchRequestNo => $count);
157 industryType => $self->industryType,
160 merchantID => $self->merchantID,
161 terminalID => $self->terminalID,
163 if ($item->payment_type eq 'CC') {
164 my $expiration = $item->expiration;
165 $expiration =~ s/\D//g;
167 ccAccountNum => $item->card_number,
168 ccExp => $expiration,
170 } elsif ( $item->payment_type eq 'ECHECK' ) {
173 ecpCheckRT => $item->routing_code,
174 ecpCheckDDA => $item->account_number,
175 ecpBankAcctType => $BankAcctType{ $item->account_type },
176 ecpDelvMethod => 'A',
179 die "payment type ".$item->type." not supported";
181 if ( $self->with_recurringInd ) {
182 if ( $item->recurring_billing eq 'F' ) {
183 push @order, ( recurringInd => 'RF' );
184 } elsif ( $item->recurring_billing eq 'S' ) {
185 push @order, ( recurringInd => 'RS' );
187 } # else don't send recurringInd at all
190 avsZip => $item->zip,
191 avsAddress1 => bytes_substr($item->address, 0, 30),
192 avsAddress2 => bytes_substr($item->address2, 0, 30),
193 avsCity => bytes_substr($item->city, 0, 20),
194 avsState => bytes_substr($item->state, 0, 2),
195 avsName => bytes_substr($item->first_name. ' '. $item->last_name, 0, 30),
196 ( $paymentech_countries{ $item->country }
197 ? ( avsCountryCode => $item->country )
200 orderID => $item->tid,
201 amount => int( $item->amount * 100 ),
204 my $key = shift @order;
205 my $value = shift @order;
206 $xml->dataElement($key, $value);
208 $xml->endTag('newOrder');
209 } # if action eq 'payment'
211 die "action ".$item->action." not supported";
217 my ($self, $batch, $xml, $count) = @_;
218 $xml->startTag('endOfDay', 'BatchRequestNo', $count);
219 $xml->dataElement('bin' => $self->bin);
220 $xml->dataElement('merchantID' => $self->merchantID);
221 $xml->dataElement('terminalID' => $self->terminalID);
222 $xml->endTag('endOfDay');
223 $xml->endTag('transRequest');
229 my $batch = Business::BatchPayment->create('Batch');
231 my $tree = XML::Simple::XMLin($input, KeepRoot => 1);
232 my $newOrderResp = $tree->{transResponse}->{newOrderResp};
233 die "can't find <transResponse><newOrderResp> in input"
234 unless defined $newOrderResp;
236 $newOrderResp = [ $newOrderResp ] if ref($newOrderResp) ne 'ARRAY';
237 foreach my $resp (@$newOrderResp) {
239 $batch->push( $self->parse_item($resp) );
241 # parse_error needs a string representation of the
242 # input data...and if it 's failing because it wasn't valid
243 # XML, we wouldn't get this far.
244 $self->parse_error(XML::Simple::XMLout($resp), $_);
251 my ($self, $resp) = @_;
253 my ($mon, $day, $year, $hour, $min, $sec) =
254 $resp->{respDateTime} =~ /^(..)(..)(....)(..)(..)(..)$/;
255 my $dt = DateTime->new(
264 my %failure_status = (
265 # API version 2.6, April 2013
266 '00' => undef, # Approved
273 'B7' => 'blacklisted', # Fraud
274 'B9' => 'blacklisted', # On Negative File
275 'BB' => 'stolen', # Possible Compromise
276 'BG' => 'blacklisted', # Blocked Account
277 'BQ' => 'blacklisted', # Issuer has Flagged Account as Suspected Fraud
278 'C4' => 'nsf', # Over Credit Limit
279 'D5' => 'blacklisted', # On Negative File
280 'D7' => 'nsf', # Insufficient Funds
281 'F3' => 'inactive', # Account Closed
283 ); # all others are "decline"
285 my $failure_status = undef;
288 if ( $resp->{procStatus} ) {
289 $error_message = $resp->{procStatusMessage};
290 } elsif ( $resp->{respCode} ) {
291 $error_message = $resp->{respCodeMessage};
292 $failure_status = $failure_status{ $resp->{respCode} } || 'decline';
297 my $item = Business::BatchPayment->create(Item =>
298 tid => $resp->{orderID},
300 authorization => $resp->{authorizationCode},
301 order_number => $resp->{txRefNum},
302 approved => ($resp->{approvalStatus} == 1),
303 error_message => $error_message,
304 failure_status => $failure_status,
312 my ($string, $offset, $length, $repl) = @_;
314 Encode::encode('utf8', $string || ''),
317 Encode::encode('utf8', $repl || '')
319 return Encode::decode('utf8', $bytes, Encode::FB_QUIET);
323 package Business::BatchPayment::Paymentech::Transport;
325 use File::Temp qw( tempdir );
326 use File::Slurp qw( read_file write_file );
328 use Moose::Util::TypeConstraints;
329 extends 'Business::BatchPayment::Transport::SFTP';
330 with 'Business::BatchPayment::TestMode';
335 $self->test_mode ? 'orbitalbatchvar.paymentech.net'
336 : 'orbitalbatch.paymentech.net'
343 where { !defined($_) or ( -d $_ and -w $_ ) },
344 message { "can't write to '$_'" };
346 has 'archive_to' => (
351 # batch content passed as an argument
357 my $tmpdir = tempdir( CLEANUP => 1 );
358 $content =~ /<fileID>(.*)<\/fileID>/;
360 my $archive_dir = $self->archive_to;
362 warn "Writing temp file to $tmpdir/$filename.xml.\n" if $self->debug;
363 write_file("$tmpdir/$filename.xml", $content);
365 warn "Creating zip file.\n" if $self->debug;
370 "$tmpdir/$filename.zip",
371 "$tmpdir/$filename.xml",
373 unshift @args, '-q' unless $self->debug;
374 system('zip', @args);
375 die "failed to create zip file" if (! -f "$tmpdir/$filename.zip");
377 warn "Uploading.\n" if $self->debug;
378 $self->put("$tmpdir/$filename.zip", "$filename.zip");
385 my $tmpdir = tempdir( CLEANUP => 1 );
386 my $ls_info = $self->ls('.', wanted => qr/_resp\.zip$/);
387 my $archive_dir = $self->archive_to;
389 foreach (@$ls_info) {
390 my $filename = $_->{filename}; # still ends in _resp
391 $filename =~ s/\.zip$//;
392 warn "Retrieving $filename.zip\n" if $self->debug;
393 $self->get("$filename.zip", "$tmpdir/$filename.zip");
398 "$tmpdir/$filename.zip",
402 unshift @args, '-q' unless $self->debug;
403 system('unzip', @args);
404 if (! -f "$tmpdir/$filename.xml") {
405 warn "failed to extract $filename.xml from $filename.zip\n";
408 my $content = read_file("$tmpdir/$filename.xml");
409 if ( $archive_dir ) {
410 warn "Copying $tmpdir/$filename.xml to archive dir $archive_dir\n";
411 write_file("$archive_dir/$filename.xml", $content);
413 push @batches, $content;
420 'info_compat' => '0.01',
421 'gateway_name' => 'Paymentech',
422 'gateway_url' => 'http://www.chasepaymentech.com/',
423 'module_version' => $VERSION,
424 'supported_types' => [ qw( CC ECHECK ) ],
425 'token_support' => 0,
426 'test_transaction' => 1,
427 'supported_actions' => [ 'Payment' ],
433 Mark Wells, C<< <mark at freeside.biz> >>
437 Relying on external zip/unzip is awkward.
441 You can find documentation for this module with the perldoc command.
443 perldoc Business::BatchPayment::Paymentech
445 Commercial support is available from Freeside Internet Services, Inc.
447 L<http://www.freeside.biz>
449 =head1 ACKNOWLEDGEMENTS
451 =head1 LICENSE AND COPYRIGHT
453 Copyright 2012 Mark Wells.
455 This program is free software; you can redistribute it and/or modify it
456 under the terms of either: the GNU General Public License as published
457 by the Free Software Foundation; or the Artistic License.
459 See http://dev.perl.org/licenses/ for more information.
464 1; # End of Business::BatchPayment::Paymentech