82b50d3d06222014d79bb25fd6c29a497d287d92
[Business-OnlinePayment-PaymenTech.git] / lib / Business / OnlinePayment / PaymenTech.pm
1 package Business::OnlinePayment::PaymenTech;
2
3 use strict;
4 use Carp;
5 use Business::OnlinePayment::HTTPS;
6 use XML::Simple;
7 use Tie::IxHash;
8 use vars qw($VERSION $DEBUG @ISA $me);
9
10 @ISA = qw(Business::OnlinePayment::HTTPS);
11 $VERSION = '2.02';
12 $DEBUG = 0;
13 $me='Business::OnlinePayment::PaymenTech';
14
15 my %request_header = (
16   'MIME-VERSION'    =>    '1.0',
17   'Content-Transfer-Encoding' => 'text',
18   'Request-Number'  =>    1,
19   'Document-Type'   =>    'Request',
20   'Interface-Version' =>  "$me $VERSION",
21 ); # Content-Type has to be passed separately
22
23 tie my %new_order, 'Tie::IxHash', (
24   OrbitalConnectionUsername => ':login',
25   OrbitalConnectionPassword => ':password',
26   IndustryType              => 'EC', # Assume industry = Ecommerce
27   MessageType               => ':message_type',
28   BIN                       => ':bin',
29   MerchantID                => ':merchant_id',
30   TerminalID                => ':terminal_id',
31   CardBrand                 => '',
32   AccountNum                => ':card_number',
33   Exp                       => ':expiration',
34   CurrencyCode              => ':currency_code',
35   CurrencyExponent          => ':currency_exp',
36   CardSecValInd             => ':cvvind',
37   CardSecVal                => ':cvv2',
38   AVSzip                    => ':zip',
39   AVSaddress1               => ':address',
40   AVScity                   => ':city',
41   AVSstate                  => ':state',
42   OrderID                   => ':invoice_number',
43   Amount                    => ':amount',
44   Comments                  => ':email', # as per B:OP:WesternACH
45   TxRefNum                  => ':order_number', # used only for Refund
46 );
47
48 tie my %mark_for_capture, 'Tie::IxHash', (
49   OrbitalConnectionUsername => ':login',
50   OrbitalConnectionPassword => ':password',
51   OrderID                   => ':invoice_number',
52   Amount                    => ':amount',
53   BIN                       => ':bin',
54   MerchantID                => ':merchant_id',
55   TerminalID                => ':terminal_id',
56   TxRefNum                  => ':order_number',
57 );
58
59 tie my %reversal, 'Tie::IxHash', (
60   OrbitalConnectionUsername => ':login',
61   OrbitalConnectionPassword => ':password',
62   TxRefNum                  => ':order_number',
63   TxRefIdx                  => 0,
64   OrderID                   => ':invoice_number',
65   BIN                       => ':bin',
66   MerchantID                => ':merchant_id',
67   TerminalID                => ':terminal_id',
68 # Always attempt to reverse authorization.
69   OnlineReversalInd         => 'Y',
70 );
71
72 my %defaults = (
73   terminal_id => '001',
74   currency    => 'USD',
75   cvvind      => '',
76 );
77
78 my @required = ( qw(
79   login
80   password
81   action
82   bin
83   merchant_id
84   invoice_number
85   amount
86   )
87 );
88
89 my %currency_code = (
90 # Per ISO 4217.  Add to this as needed.
91   USD => [840, 2],
92   CAD => [124, 2],
93   MXN => [484, 2],
94 );
95
96 sub set_defaults {
97     my $self = shift;
98
99     $self->server('orbitalvar1.paymentech.net') unless $self->server; # this is the test server.
100     $self->port('443') unless $self->port;
101     $self->path('/authorize') unless $self->path;
102
103     $self->build_subs(qw( 
104       order_number
105       ProcStatus 
106       ApprovalStatus 
107       StatusMsg 
108       Response
109       RespCode
110       AuthCode
111       AVSRespCode
112       CVV2RespCode
113      ));
114
115 }
116
117 sub build {
118   my $self = shift;
119   my %content = $self->content();
120   my $skel = shift;
121   tie my %data, 'Tie::IxHash';
122   ref($skel) eq 'HASH' or die 'Tried to build non-hash';
123   foreach my $k (keys(%$skel)) {
124     my $v = $skel->{$k};
125     # Not recursive like B:OP:WesternACH; Paymentech requests are only one layer deep.
126     if($v =~ /^:(.*)/) {
127       # Get the content field with that name.
128       $data{$k} = $content{$1};
129     }
130     else {
131       $data{$k} = $v;
132     }
133   }
134   return \%data;
135 }
136
137 sub map_fields {
138     my($self) = @_;
139
140     my %content = $self->content();
141     foreach(qw(merchant_id terminal_id currency)) {
142       $content{$_} = $self->{$_} if exists($self->{$_});
143     }
144
145     $self->required_fields('action');
146     my %message_type = 
147                   ('normal authorization' => 'AC',
148                    'authorization only'   => 'A',
149                    'credit'               => 'R',
150                    'void'                 => 'V',
151                    'post authorization'   => 'MFC', # for our use, doesn't go in the request
152                    ); 
153     $content{'message_type'} = $message_type{lc($content{'action'})} 
154       or die "unsupported action: '".$content{'action'}."'";
155
156     foreach (keys(%defaults) ) {
157       $content{$_} = $defaults{$_} if !defined($content{$_});
158     }
159     if(length($content{merchant_id}) == 12) {
160       $content{bin} = '000002' # PNS
161     }
162     elsif(length($content{merchant_id}) == 6) {
163       $content{bin} = '000001' # Salem
164     }
165     else {
166       die "invalid merchant ID: '".$content{merchant_id}."'";
167     }
168
169     @content{qw(currency_code currency_exp)} = @{$currency_code{$content{currency}}}
170       if $content{currency};
171
172     if($content{card_number} =~ /^(4|6011)/) { # Matches Visa and Discover transactions
173       if(defined($content{cvv2})) {
174         $content{cvvind} = 1; # "Value is present"
175       }
176       else {
177         $content{cvvind} = 9; # "Value is not available"
178       }
179     }
180     $content{amount} = int($content{amount}*100);
181     $content{name} = $content{first_name} . ' ' . $content{last_name};
182 # According to the spec, the first 8 characters of this have to be unique.
183 # The test server doesn't enforce this, but we comply anyway to the extent possible.
184     if(! $content{invoice_number}) {
185       # Choose one arbitrarily
186       $content{invoice_number} ||= sprintf("%04x%04x",time % 2**16,int(rand() * 2**16));
187     }
188
189     $content{expiration} =~ s/\D//g; # Because Freeside sends it as mm/yy, not mmyy.
190
191     $self->content(%content);
192     return;
193 }
194
195 sub submit {
196   my($self) = @_;
197   $DB::single = $DEBUG;
198
199   $self->map_fields();
200   my %content = $self->content;
201
202   my @required_fields = @required;
203
204   my $request;
205   if( $content{'message_type'} eq 'MFC' ) {
206     $request = { MarkForCapture => $self->build(\%mark_for_capture) };
207     push @required_fields, 'order_number';
208   }
209   elsif( $content{'message_type'} eq 'V' ) {
210     $request = { Reversal => $self->build(\%reversal) };
211   }
212   else { 
213     $request = { NewOrder => $self->build(\%new_order) }; 
214     push @required_fields, qw(
215       card_number
216       expiration
217       currency
218       address
219       city
220       zip
221       );
222   }
223
224   $self->required_fields(@required_fields);
225
226   my $post_data = XMLout({ Request => $request }, KeepRoot => 1, NoAttr => 1, NoSort => 1);
227
228   if (!$self->test_transaction()) {
229     $self->server('orbital1.paymentech.net');
230   }
231
232   warn $post_data if $DEBUG;
233   $DB::single = $DEBUG;
234   my($page,$server_response,%headers) =
235     $self->https_post( { 'Content-Type' => 'application/PTI47', 
236                          'headers' => \%request_header } ,
237                           $post_data);
238
239   warn $page if $DEBUG;
240
241   my $response;
242   my $error = '';
243   if ($server_response =~ /200/){
244     $response = XMLin($page, KeepRoot => 0);
245     $self->Response($response);
246     my ($r) = values(%$response);
247     foreach(qw(ProcStatus RespCode AuthCode AVSRespCode CVV2RespCode)) {
248       if(exists($r->{$_}) and
249          !ref($r->{$_})) {
250         $self->$_($r->{$_});
251       }
252     }
253     if(!exists($r->{'ProcStatus'})) {
254       $error = "Malformed response: '$page'";
255       $self->is_success(0);
256     }
257     elsif( $r->{'ProcStatus'} != 0 or 
258           # NewOrders get ApprovalStatus, Reversals don't.
259           ( exists($r->{'ApprovalStatus'}) ?
260             $r->{'ApprovalStatus'} != 1 :
261             $r->{'StatusMsg'} ne 'Approved' )
262           ) {
263       $error = "Transaction error: '". ($r->{'ProcStatusMsg'} || $r->{'StatusMsg'}) . "'";
264       $self->is_success(0);
265     }
266     else {
267       # success!
268       $self->is_success(1);
269       # For credits, AuthCode is empty and gets converted to a hashref.
270       $self->authorization($r->{'AuthCode'}) if !ref($r->{'AuthCode'});
271       $self->order_number($r->{'TxRefNum'});
272     }
273   } else {
274     $error = "Server error: '$server_response'";
275   }
276   $self->error_message($error);
277 }
278
279 1;
280 __END__
281
282 =head1 NAME
283
284 Business::OnlinePayment::PaymenTech - Chase Paymentech backend for Business::OnlinePayment
285
286 =head1 SYNOPSIS
287
288 $trans = new Business::OnlinePayment('PaymenTech');
289 $trans->content(
290   login           => "login",
291   password        => "password",
292   merchant_id     => "000111222333",
293   terminal_id     => "001",
294   type            => "CC",
295   card_number     => "5500000000000004",
296   expiration      => "0211",
297   address         => "123 Anystreet",
298   city            => "Sacramento",
299   zip             => "95824",
300   action          => "Normal Authorization",
301   amount          => "24.99",
302
303 );
304
305 $trans->submit;
306 if($trans->is_approved) {
307   print "Approved: ".$trans->authorization;
308
309 } else {
310   print "Failed: ".$trans->error_message;
311
312 }
313
314 =head1 NOTES
315
316 The only supported transaction types are Normal Authorization and Credit.  Paymentech 
317 supports separate Authorize and Capture actions as well as recurring billing, but 
318 those are not yet implemented.
319
320 Electronic check processing is not yet supported.
321
322 =head1 AUTHOR
323
324 Mark Wells, mark@freeside.biz
325
326 =head1 SEE ALSO
327
328 perl(1). L<Business::OnlinePayment>.
329
330 =cut
331