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