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',
31 my $result = $processor->submit(@items);
35 Requires L<Net::SFTP::Foreign> and ssh (for file transfer) and the zip and
36 unzip programs. Unlikely to work on non-Unix systems.
38 =head2 PROCESSOR ATTRIBUTES
42 =item login - the username to use for SFTP, and in the "userID" tag
44 =item password - the password for SFTP, and for creating zip files
46 =item merchantID - your 6- or 12-digit Paymentech merchant ID
48 =item bin - your BIN: 000001 or 000002
50 =item terminalID - your 3-digit terminal ID
52 =item industryType - your 2-letter industry type code
58 use File::Temp qw(tempdir);
65 with 'Business::BatchPayment::Processor';
66 with 'Business::BatchPayment::TestMode';
70 # could have some validation on all of these
71 has [ qw(merchantID terminalID bin industryType login password) ] => (
77 has 'fileDateTime' => (
81 DateTime->now->strftime('%Y%m%d%H%M%S')
86 'personal checking' => 'C',
87 'personal savings' => 'S',
88 'business checking' => 'X',
89 'business savings' => 'X',
92 my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK );
94 sub default_transport {
96 Business::BatchPayment::Paymentech::Transport->new(
97 login => $self->login,
98 password => $self->password,
99 debug => $self->debug,
100 test_mode => $self->test_mode,
109 my $xml = XML::Writer->new(
115 $self->format_header($batch, $xml);
117 foreach my $item ( @{ $batch->items } ) {
119 $self->format_item($item, $batch, $xml, $count);
122 $self->format_error($item, $_);
125 $self->format_trailer($batch, $xml, $count);
130 my ($self, $batch, $xml) = @_;
131 my $num_items = $batch->count;
134 $xml->startTag('transRequest', RequestCount => $num_items + 1);
135 $xml->startTag('batchFileID');
136 $xml->dataElement(userID => $self->login);
137 $xml->dataElement(fileDateTime => $self->fileDateTime);
138 $xml->dataElement(fileID => sprintf('%06d-', $batch->batch_id) .
139 $self->fileDateTime);
140 $xml->endTag('batchFileID');
144 my ($self, $item, $batch, $xml, $count) = @_;
145 if ( $item->action eq 'payment' ) {
146 $xml->startTag('newOrder', BatchRequestNo => $count);
148 industryType => $self->industryType,
151 merchantID => $self->merchantID,
152 terminalID => $self->terminalID,
154 if ($item->payment_type eq 'CC') {
155 my $expiration = $item->expiration;
156 $expiration =~ s/\D//g;
158 ccAccountNum => $item->card_number,
159 ccExp => $expiration,
161 } elsif ( $item->payment_type eq 'ECHECK' ) {
164 ecpCheckRT => $item->routing_code,
165 ecpCheckDDA => $item->account_number,
166 ecpBankAcctType => $BankAcctType{ $item->account_type },
167 ecpDelvMethod => 'A',
170 die "payment type ".$item->type." not supported";
172 if ( $item->recurring_billing eq 'F' ) {
173 push @order, ( recurringInd => 'RF' );
174 } elsif ( $item->recurring_billing eq 'S' ) {
175 push @order, ( recurringInd => 'RS' );
176 } # else don't send recurringInd at all
179 avsZip => $item->zip,
180 avsAddress1 => bytes_substr($item->address, 0, 30),
181 avsAddress2 => bytes_substr($item->address2, 0, 30),
182 avsCity => bytes_substr($item->city, 0, 20),
183 avsState => bytes_substr($item->state, 0, 2),
184 avsName => bytes_substr($item->first_name. ' '. $item->last_name, 0, 30),
185 ( $paymentech_countries{ $item->country }
186 ? ( avsCountryCode => $item->country )
189 orderID => $item->tid,
190 amount => int( $item->amount * 100 ),
193 my $key = shift @order;
194 my $value = shift @order;
195 $xml->dataElement($key, $value);
197 $xml->endTag('newOrder');
198 } # if action eq 'payment'
200 die "action ".$item->action." not supported";
206 my ($self, $batch, $xml, $count) = @_;
207 $xml->startTag('endOfDay', 'BatchRequestNo', $count);
208 $xml->dataElement('bin' => $self->bin);
209 $xml->dataElement('merchantID' => $self->merchantID);
210 $xml->dataElement('terminalID' => $self->terminalID);
211 $xml->endTag('endOfDay');
212 $xml->endTag('transRequest');
218 my $batch = Business::BatchPayment->create('Batch');
220 my $tree = XML::Simple::XMLin($input, KeepRoot => 1);
221 my $newOrderResp = $tree->{transResponse}->{newOrderResp};
222 die "can't find <transResponse><newOrderResp> in input"
223 unless defined $newOrderResp;
225 $newOrderResp = [ $newOrderResp ] if ref($newOrderResp) ne 'ARRAY';
226 foreach my $resp (@$newOrderResp) {
228 $batch->push( $self->parse_item($resp) );
230 # parse_error needs a string representation of the
231 # input data...and if it 's failing because it wasn't valid
232 # XML, we wouldn't get this far.
233 $self->parse_error(XML::Simple::XMLout($resp), $_);
240 my ($self, $resp) = @_;
242 my ($mon, $day, $year, $hour, $min, $sec) =
243 $resp->{respDateTime} =~ /^(..)(..)(....)(..)(..)(..)$/;
244 my $dt = DateTime->new(
253 my %failure_status = (
254 # API version 2.6, April 2013
255 '00' => undef, # Approved
262 'B7' => 'blacklisted', # Fraud
263 'B9' => 'blacklisted', # On Negative File
264 'BB' => 'stolen', # Possible Compromise
265 'BG' => 'blacklisted', # Blocked Account
266 'BQ' => 'blacklisted', # Issuer has Flagged Account as Suspected Fraud
267 'C4' => 'nsf', # Over Credit Limit
268 'D5' => 'blacklisted', # On Negative File
269 'D7' => 'nsf', # Insufficient Funds
270 'F3' => 'inactive', # Account Closed
272 ); # all others are "decline"
274 my $failure_status = undef;
277 if ( $resp->{procStatus} ) {
278 $error_message = $resp->{procStatusMessage};
279 } elsif ( $resp->{respCode} ) {
280 $error_message = $resp->{respCodeMessage};
281 $failure_status = $failure_status{ $resp->{respCode} } || 'decline';
286 my $item = Business::BatchPayment->create(Item =>
287 tid => $resp->{orderID},
289 authorization => $resp->{authorizationCode},
290 order_number => $resp->{txRefNum},
291 approved => ($resp->{approvalStatus} == 1),
292 error_message => $error_message,
293 failure_status => $failure_status,
301 my ($string, $offset, $length, $repl) = @_;
303 Encode::encode('utf8', $string || ''),
306 Encode::encode('utf8', $repl || '')
308 return Encode::decode('utf8', $bytes, Encode::FB_QUIET);
312 package Business::BatchPayment::Paymentech::Transport;
314 use File::Temp qw( tempdir );
315 use File::Slurp qw( read_file write_file );
317 use Moose::Util::TypeConstraints;
318 extends 'Business::BatchPayment::Transport::SFTP';
319 with 'Business::BatchPayment::TestMode';
324 $self->test_mode ? 'orbitalbatchvar.paymentech.net'
325 : 'orbitalbatch.paymentech.net'
332 where { !defined($_) or ( -d $_ and -w $_ ) },
333 message { "can't write to '$_'" };
335 has 'archive_to' => (
340 # batch content passed as an argument
346 my $tmpdir = tempdir( CLEANUP => 1 );
347 $content =~ /<fileID>(.*)<\/fileID>/;
349 my $archive_dir = $self->archive_to;
351 warn "Writing temp file to $tmpdir/$filename.xml.\n" if $self->debug;
352 write_file("$tmpdir/$filename.xml", $content);
354 warn "Creating zip file.\n" if $self->debug;
359 "$tmpdir/$filename.zip",
360 "$tmpdir/$filename.xml",
362 unshift @args, '-q' unless $self->debug;
363 system('zip', @args);
364 die "failed to create zip file" if (! -f "$tmpdir/$filename.zip");
366 warn "Uploading.\n" if $self->debug;
367 $self->put("$tmpdir/$filename.zip", "$filename.zip");
374 my $tmpdir = tempdir( CLEANUP => 1 );
375 my $ls_info = $self->ls('.', wanted => qr/_resp\.zip$/);
376 my $archive_dir = $self->archive_to;
378 foreach (@$ls_info) {
379 my $filename = $_->{filename}; # still ends in _resp
380 $filename =~ s/\.zip$//;
381 warn "Retrieving $filename.zip\n" if $self->debug;
382 $self->get("$filename.zip", "$tmpdir/$filename.zip");
387 "$tmpdir/$filename.zip",
391 unshift @args, '-q' unless $self->debug;
392 system('unzip', @args);
393 if (! -f "$tmpdir/$filename.xml") {
394 warn "failed to extract $filename.xml from $filename.zip\n";
397 my $content = read_file("$tmpdir/$filename.xml");
398 if ( $archive_dir ) {
399 warn "Copying $tmpdir/$filename.xml to archive dir $archive_dir\n";
400 write_file("$archive_dir/$filename.xml", $content);
402 push @batches, $content;
409 'info_compat' => '0.01',
410 'gateway_name' => 'Paymentech',
411 'gateway_url' => 'http://www.chasepaymentech.com/',
412 'module_version' => $VERSION,
413 'supported_types' => [ qw( CC ECHECK ) ],
414 'token_support' => 0,
415 'test_transaction' => 1,
416 'supported_actions' => [ 'Payment' ],
422 Mark Wells, C<< <mark at freeside.biz> >>
426 Relying on external zip/unzip is awkward.
430 You can find documentation for this module with the perldoc command.
432 perldoc Business::BatchPayment::Paymentech
434 Commercial support is available from Freeside Internet Services, Inc.
436 L<http://www.freeside.biz>
438 =head1 ACKNOWLEDGEMENTS
440 =head1 LICENSE AND COPYRIGHT
442 Copyright 2012 Mark Wells.
444 This program is free software; you can redistribute it and/or modify it
445 under the terms of either: the GNU General Public License as published
446 by the Free Software Foundation; or the Artistic License.
448 See http://dev.perl.org/licenses/ for more information.
453 1; # End of Business::BatchPayment::Paymentech