1 package Business::OnlinePayment::PaymenTech;
5 use Business::OnlinePayment::HTTPS;
8 use vars qw($VERSION $DEBUG @ISA $me);
10 @ISA = qw(Business::OnlinePayment::HTTPS);
13 $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
16 $me='Business::OnlinePayment::PaymenTech';
18 my %request_header = (
19 'MIME-VERSION' => '1.0',
20 'Content-Transfer-Encoding' => 'text',
21 'Request-Number' => 1,
22 'Document-Type' => 'Request',
23 'Interface-Version' => "$me $VERSION",
24 ); # Content-Type has to be passed separately
26 tie my %new_order, 'Tie::IxHash', (
27 OrbitalConnectionUsername => [ ':login', 32 ],
28 OrbitalConnectionPassword => [ ':password', 32 ],
29 IndustryType => [ 'EC', 2 ],
30 MessageType => [ ':message_type', 2 ],
32 MerchantID => [ ':merchant_id', 12 ],
33 TerminalID => [ ':terminal_id', 3 ],
34 CardBrand => [ '', 2 ],
35 AccountNum => [ ':card_number', 19 ],
36 Exp => [ ':expiration', 4 ],
37 CurrencyCode => [ ':currency_code', 3 ],
38 CurrencyExponent => [ ':currency_exp', 6 ],
39 CardSecValInd => [ ':cvvind', 1 ],
40 CardSecVal => [ ':cvv2', 4 ],
41 AVSzip => [ ':zip', 10 ],
42 AVSaddress1 => [ ':address', 30 ],
43 AVScity => [ ':city', 20 ],
44 AVSstate => [ ':state', 2 ],
45 AVScountryCode => [ ':country', 2 ],
46 OrderID => [ ':invoice_number', 22 ],
47 Amount => [ ':amount', 12 ],
48 Comments => [ ':email', 64 ],
49 TxRefNum => [ ':order_number', 40 ],# used only for Refund
52 tie my %mark_for_capture, 'Tie::IxHash', (
53 OrbitalConnectionUsername => [ ':login', 32 ],
54 OrbitalConnectionPassword => [ ':password', 32 ],
55 OrderID => [ ':invoice_number', 22 ],
56 Amount => [ ':amount', 12 ],
58 MerchantID => [ ':merchant_id', 12 ],
59 TerminalID => [ ':terminal_id', 3 ],
60 TxRefNum => [ ':order_number', 40 ],
63 tie my %reversal, 'Tie::IxHash', (
64 OrbitalConnectionUsername => [ ':login', 32 ],
65 OrbitalConnectionPassword => [ ':password', 32 ],
66 TxRefNum => [ ':order_number', 40 ],
67 TxRefIdx => [ '0', 4 ],
68 OrderID => [ ':invoice_number', 22 ],
70 MerchantID => [ ':merchant_id', 12 ],
71 TerminalID => [ ':terminal_id', 3 ],
72 OnlineReversalInd => [ 'Y', 1 ],
73 # Always attempt to reverse authorization.
94 # Per ISO 4217. Add to this as needed.
100 my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK );
101 my %failure_status = (
102 # values of the RespCode element
103 # in theory RespMsg should be set to a descriptive message, but it looks
104 # like that's not reliable
105 # XXX we should have a way to indicate other actions required by the
106 # processor, such as "honor with identification", "call for instructions",
108 '00' => undef, # Approved
109 '04' => 'pickup', # Pickup
110 '33' => 'expired', # Card is Expired
111 '41' => 'stolen', # Lost/Stolen
112 '42' => 'inactive', # Account Not Active
113 '43' => 'stolen', # Lost/Stolen Card
114 '44' => 'inactive', # Account Not Active
115 #'45' duplicate transaction, should also have its own status
116 'B7' => 'blacklisted', # Fraud
117 'B9' => 'blacklisted', # On Negative File
118 'BB' => 'stolen', # Possible Compromise
119 'BG' => 'blacklisted', # Blocked Account
120 'BQ' => 'blacklisted', # Issuer has Flagged Account as Suspected Fraud
121 'C4' => 'nsf', # Over Credit Limit
122 'D5' => 'blacklisted', # On Negative File
123 'D7' => 'nsf', # Insufficient Funds
124 'F3' => 'inactive', # Account Closed
131 $self->server('orbitalvar1.chasepaymentech.com') unless $self->server; # this is the test server.
132 $self->port('443') unless $self->port;
133 $self->path('/authorize') unless $self->path;
135 $self->build_subs(qw(
139 #leaking gateway-specific anmes? need to be mapped to B:OP standards :)
152 my %content = $self->content();
154 tie my %data, 'Tie::IxHash';
155 ref($skel) eq 'HASH' or die 'Tried to build non-hash';
156 foreach my $k (keys(%$skel)) {
159 ($v, $l) = @$v if(ref $v eq 'ARRAY');
161 # Get the content field with that name.
162 $data{$k} = $content{$1};
167 # Ruthlessly enforce field length.
168 $data{$k} = substr($data{$k}, 0, $l) if($data{$k} and $l);
176 my %content = $self->content();
177 foreach(qw(merchant_id terminal_id currency)) {
178 $content{$_} = $self->{$_} if exists($self->{$_});
181 $self->required_fields('action');
183 ('normal authorization' => 'AC',
184 'authorization only' => 'A',
187 'post authorization' => 'MFC', # for our use, doesn't go in the request
189 $content{'message_type'} = $message_type{lc($content{'action'})}
190 or die "unsupported action: '".$content{'action'}."'";
192 foreach (keys(%defaults) ) {
193 $content{$_} = $defaults{$_} if !defined($content{$_});
195 if(length($content{merchant_id}) == 12) {
196 $content{bin} = '000002' # PNS
198 elsif(length($content{merchant_id}) == 6) {
199 $content{bin} = '000001' # Salem
202 die "invalid merchant ID: '".$content{merchant_id}."'";
205 @content{qw(currency_code currency_exp)} = @{$currency_code{$content{currency}}}
206 if $content{currency};
208 if($content{card_number} =~ /^(4|6011)/) { # Matches Visa and Discover transactions
209 if(defined($content{cvv2})) {
210 $content{cvvind} = 1; # "Value is present"
213 $content{cvvind} = 9; # "Value is not available"
216 $content{amount} = int($content{amount}*100);
217 $content{name} = $content{first_name} . ' ' . $content{last_name};
218 # According to the spec, the first 8 characters of this have to be unique.
219 # The test server doesn't enforce this, but we comply anyway to the extent possible.
220 if(! $content{invoice_number}) {
221 # Choose one arbitrarily
222 $content{invoice_number} ||= sprintf("%04x%04x",time % 2**16,int(rand() * 2**16));
225 # Always send as MMYY
226 $content{expiration} =~ s/\D//g;
227 $content{expiration} = sprintf('%04d',$content{expiration});
229 $content{country} ||= 'US';
230 $content{country} = ( $paymentech_countries{ $content{country} }
235 $self->content(%content);
241 $DB::single = $DEBUG;
244 my %content = $self->content;
246 my @required_fields = @required;
249 if( $content{'message_type'} eq 'MFC' ) {
250 $request = { MarkForCapture => $self->build(\%mark_for_capture) };
251 push @required_fields, 'order_number';
253 elsif( $content{'message_type'} eq 'V' ) {
254 $request = { Reversal => $self->build(\%reversal) };
257 $request = { NewOrder => $self->build(\%new_order) };
258 push @required_fields, qw(
268 $self->required_fields(@required_fields);
270 my $post_data = XMLout({ Request => $request }, KeepRoot => 1, NoAttr => 1, NoSort => 1);
272 if (!$self->test_transaction()) {
273 $self->server('orbital1.chasepaymentech.com');
276 warn $post_data if $DEBUG;
277 $DB::single = $DEBUG;
278 my($page,$server_response,%headers) =
279 $self->https_post( { 'Content-Type' => 'application/PTI47',
280 'headers' => \%request_header } ,
283 warn $page if $DEBUG;
285 my $response = XMLin($page, KeepRoot => 0);
286 #$self->Response($response);
289 #warn Dumper($response) if $DEBUG;
291 my ($r) = values(%$response);
292 #foreach(qw(ProcStatus RespCode AuthCode AVSRespCode CVV2RespCode)) {
293 # if(exists($r->{$_}) and
295 # $self->$_($r->{$_});
301 #turn empty hashrefs into the empty string
302 $r->{$_} = '' if ref($r->{$_}) && ! keys %{ $r->{$_} };
304 #turn hashrefs with content into scalars
305 $r->{$_} = $r->{$_}{'content'}
306 if ref($r->{$_}) && exists($r->{$_}{'content'});
309 if ($server_response !~ /^200/) {
311 #$self->is_success(0);
312 my $error = "Server error: '$server_response'";
313 $error .= " / Transaction error: '".
314 ($r->{'ProcStatusMsg'} || $r->{'StatusMsg'}) . "'"
315 if $r->{'ProcStatus'} != 0;
316 #$self->error_message($error);
317 #overzealous? are there "normal decline" transaction errors being returned?
322 die "Unable to parse response_page\n" if !exists($r->{'ProcStatus'});
324 if ( $r->{'ProcStatus'} != 0 or
325 # NewOrders get ApprovalStatus, Reversals don't.
326 ( exists($r->{'ApprovalStatus'}) ?
327 $r->{'ApprovalStatus'} != 1 :
328 $r->{'StatusMsg'} ne 'Approved' )
332 $self->failure_status( $failure_status{ $r->{RespCode} } || 'decline' );
333 $self->is_success(0);
334 $self->error_message( "Transaction error: '".
335 ($r->{'ProcStatusMsg'} || $r->{'StatusMsg'}) . "'"
340 $self->is_success(1);
341 # For credits, AuthCode is empty and gets converted to a hashref.
342 $self->authorization($r->{'AuthCode'}) if !ref($r->{'AuthCode'});
343 $self->order_number($r->{'TxRefNum'});
355 Business::OnlinePayment::PaymenTech - Chase Paymentech backend for Business::OnlinePayment
359 $trans = new Business::OnlinePayment('PaymenTech',
360 merchant_id => "000111222333",
361 terminal_id => "001",
362 currency => "USD", # CAD, MXN
367 password => "password",
369 card_number => "5500000000000004",
370 expiration => "0211",
371 address => "123 Anystreet",
372 city => "Sacramento",
374 action => "Normal Authorization",
379 if($trans->is_approved) {
380 print "Approved: ".$trans->authorization;
382 print "Failed: ".$trans->error_message;
387 Electronic check processing and recurring billing are not yet supported.
391 Mark Wells, mark@freeside.biz
395 perl(1). L<Business::OnlinePayment>.