290607930d86c4038332a549a571ae79d9993d86
[Business-OnlinePayment-AuthorizeNet.git] / AuthorizeNet / AIM.pm
1 package Business::OnlinePayment::AuthorizeNet::AIM;
2
3 use strict;
4 use Carp;
5 use Business::OnlinePayment::AuthorizeNet;
6 use Net::SSLeay qw/make_form post_https make_headers/;
7 use Text::CSV_XS;
8 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
9
10 require Exporter;
11
12 @ISA = qw(Exporter Business::OnlinePayment::AuthorizeNet);
13 @EXPORT = qw();
14 @EXPORT_OK = qw();
15 $VERSION = '3.21';
16
17 sub set_defaults {
18     my $self = shift;
19
20     $self->server('secure.authorize.net') unless $self->server;
21     $self->port('443') unless $self->port;
22     $self->path('/gateway/transact.dll') unless $self->path;
23
24     $self->build_subs(qw( order_number md5 avs_code cvv2_response
25                           cavv_response
26                      ));
27 }
28
29 sub map_fields {
30     my($self) = @_;
31
32     my %content = $self->content();
33
34     # ACTION MAP
35     my %actions = ('normal authorization' => 'AUTH_CAPTURE',
36                    'authorization only'   => 'AUTH_ONLY',
37                    'credit'               => 'CREDIT',
38                    'post authorization'   => 'PRIOR_AUTH_CAPTURE',
39                    'void'                 => 'VOID',
40                   );
41     $content{'action'} = $actions{lc($content{'action'})} || $content{'action'};
42
43     # TYPE MAP
44     my %types = ('visa'               => 'CC',
45                  'mastercard'         => 'CC',
46                  'american express'   => 'CC',
47                  'discover'           => 'CC',
48                  'check'              => 'ECHECK',
49                 );
50     $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
51     $self->transaction_type($content{'type'});
52
53     # ACCOUNT TYPE MAP
54     my %account_types = ('personal checking'   => 'CHECKING',
55                          'personal savings'    => 'SAVINGS',
56                          'business checking'   => 'CHECKING',
57                          'business savings'    => 'SAVINGS',
58                         );
59     $content{'account_type'} = $account_types{lc($content{'account_type'})}
60                                || $content{'account_type'};
61
62     $content{'referer'} = defined( $content{'referer'} )
63                             ? make_headers( 'Referer' => $content{'referer'} )
64                             : "";
65
66     if (length $content{'password'} == 15) {
67         $content{'transaction_key'} = delete $content{'password'};
68     }
69
70     # stuff it back into %content
71     $self->content(%content);
72 }
73
74 sub remap_fields {
75     my($self,%map) = @_;
76
77     my %content = $self->content();
78     foreach(keys %map) {
79         $content{$map{$_}} = $content{$_};
80     }
81     $self->content(%content);
82 }
83
84 sub get_fields {
85     my($self,@fields) = @_;
86
87     my %content = $self->content();
88     my %new = ();
89     foreach( grep defined $content{$_}, @fields) { $new{$_} = $content{$_}; }
90     return %new;
91 }
92
93 sub submit {
94     my($self) = @_;
95
96     $self->map_fields();
97     $self->remap_fields(
98         type              => 'x_Method',
99         login             => 'x_Login',
100         password          => 'x_Password',
101         transaction_key   => 'x_Tran_Key',
102         action            => 'x_Type',
103         description       => 'x_Description',
104         amount            => 'x_Amount',
105         currency          => 'x_Currency_Code',
106         invoice_number    => 'x_Invoice_Num',
107         order_number      => 'x_Trans_ID',
108         auth_code         => 'x_Auth_Code',
109         customer_id       => 'x_Cust_ID',
110         customer_ip       => 'x_Customer_IP',
111         last_name         => 'x_Last_Name',
112         first_name        => 'x_First_Name',
113         company           => 'x_Company',
114         address           => 'x_Address',
115         city              => 'x_City',
116         state             => 'x_State',
117         zip               => 'x_Zip',
118         country           => 'x_Country',
119         ship_last_name    => 'x_Ship_To_Last_Name',
120         ship_first_name   => 'x_Ship_To_First_Name',
121         ship_company      => 'x_Ship_To_Company',
122         ship_address      => 'x_Ship_To_Address',
123         ship_city         => 'x_Ship_To_City',
124         ship_state        => 'x_Ship_To_State',
125         ship_zip          => 'x_Ship_To_Zip',
126         ship_country      => 'x_Ship_To_Country',
127         tax               => 'x_Tax',
128         freight           => 'x_Freight',
129         duty              => 'x_Duty',
130         tax_exempt        => 'x_Tax_Exempt',
131         po_number         => 'x_Po_Num',
132         phone             => 'x_Phone',
133         fax               => 'x_Fax',
134         email             => 'x_Email',
135         email_customer    => 'x_Email_Customer',
136         card_number       => 'x_Card_Num',
137         expiration        => 'x_Exp_Date',
138         cvv2              => 'x_Card_Code',
139         check_type        => 'x_Echeck_Type',
140         account_name      => 'x_Bank_Acct_Name',
141         account_number    => 'x_Bank_Acct_Num',
142         account_type      => 'x_Bank_Acct_Type',
143         bank_name         => 'x_Bank_Name',
144         routing_code      => 'x_Bank_ABA_Code',
145         check_number      => 'x_Bank_Check_Number',
146         customer_org      => 'x_Customer_Organization_Type', 
147         customer_ssn      => 'x_Customer_Tax_ID',
148         license_num       => 'x_Drivers_License_Num',
149         license_state     => 'x_Drivers_License_State',
150         license_dob       => 'x_Drivers_License_DOB',
151         recurring_billing => 'x_Recurring_Billing',
152         duplicate_window  => 'x_Duplicate_Window',
153         track1            => 'x_Track1',
154         track2            => 'x_Track2',
155     );
156
157     my $auth_type = $self->{_content}->{transaction_key}
158                       ? 'transaction_key'
159                       : 'password';
160
161     my @required_fields = ( qw(type action login), $auth_type );
162
163     unless ( $self->{_content}->{action} eq 'VOID' ) {
164
165       if ($self->transaction_type() eq "ECHECK") {
166
167         push @required_fields, qw(
168           amount routing_code account_number account_type bank_name
169           account_name
170         );
171
172         if (defined $self->{_content}->{customer_org} and
173             length  $self->{_content}->{customer_org}
174         ) {
175           push @required_fields, qw( customer_org customer_ssn );
176         } else {
177           push @required_fields, qw(license_num license_state license_dob);
178         }
179
180       } elsif ($self->transaction_type() eq 'CC' ) {
181
182         if ( $self->{_content}->{action} eq 'PRIOR_AUTH_CAPTURE' ) {
183           if ( $self->{_content}->{order_number} ) {
184             push @required_fields, qw( amount order_number );
185           } else {
186             push @required_fields, qw( amount card_number expiration );
187           }
188         } elsif ( $self->{_content}->{action} eq 'CREDIT' ) {
189           push @required_fields, qw( amount order_number card_number );
190         } else {
191           push @required_fields, qw(
192             amount last_name first_name card_number expiration
193           );
194         }
195       } else {
196         Carp::croak( "AuthorizeNet can't handle transaction type: ".
197                      $self->transaction_type() );
198       }
199
200     }
201
202     $self->required_fields(@required_fields);
203
204     my %post_data = $self->get_fields(qw/
205         x_Login x_Password x_Tran_Key x_Invoice_Num
206         x_Description x_Amount x_Cust_ID x_Method x_Type x_Card_Num x_Exp_Date
207         x_Card_Code x_Auth_Code x_Echeck_Type x_Bank_Acct_Num
208         x_Bank_Account_Name x_Bank_ABA_Code x_Bank_Name x_Bank_Acct_Type
209         x_Bank_Check_Number
210         x_Customer_Organization_Type x_Customer_Tax_ID x_Customer_IP
211         x_Drivers_License_Num x_Drivers_License_State x_Drivers_License_DOB
212         x_Last_Name x_First_Name x_Company
213         x_Address x_City x_State x_Zip
214         x_Country
215         x_Ship_To_Last_Name x_Ship_To_First_Name x_Ship_To_Company
216         x_Ship_To_Address x_Ship_To_City x_Ship_To_State x_Ship_To_Zip
217         x_Ship_To_Country
218         x_Tax x_Freight x_Duty x_Tax_Exempt x_Po_Num
219         x_Phone x_Fax x_Email x_Email_Customer x_Country
220         x_Currency_Code x_Trans_ID x_Duplicate_Window x_Track1 x_Track2/);
221
222     $post_data{'x_Test_Request'} = $self->test_transaction() ? 'TRUE' : 'FALSE';
223
224     #deal with perl-style bool
225     if (    $post_data{'x_Email_Customer'}
226          && $post_data{'x_Email_Customer'} !~ /^FALSE$/i ) {
227       $post_data{'x_Email_Customer'} = 'TRUE';
228     } elsif ( exists $post_data{'x_Email_Customer'} ) {
229       $post_data{'x_Email_Customer'} = 'FALSE';
230     }
231
232     my $data_string = join("", values %post_data);
233
234     my $encap_character;
235     # The first set of characters here are recommended by authorize.net in their
236     #   encapsulating character example.
237     # The second set we made up hoping they will work if the first fail.
238     # The third chr(31) is the binary 'unit separator' and is our final last
239     #   ditch effort to find something not in the input.
240     foreach my $char( qw( | " ' : ; / \ - * ), '#', qw( ^ + < > [ ] ~), chr(31) ){
241       if( index($data_string, $char) == -1 ){ # found one.
242         $encap_character = $char;
243         last;
244       }
245     }
246
247     if(!$encap_character){
248       $self->is_success(0);
249       $self->error_message(
250                            "DEBUG: Input contains all encapsulating characters."
251                            . " Please remove | or ^ from your input if possible."
252                           );
253       return;
254     }
255
256     $post_data{'x_ADC_Delim_Data'} = 'TRUE';
257     $post_data{'x_delim_char'} = ',';
258     $post_data{'x_encap_char'} = $encap_character;
259     $post_data{'x_ADC_URL'} = 'FALSE';
260     $post_data{'x_Version'} = '3.1';
261
262     my $pd = make_form(%post_data);
263     my $s = $self->server();
264     my $p = $self->port();
265     my $t = $self->path();
266     my $r = $self->{_content}->{referer};
267     my($page,$server_response,%headers) = post_https($s,$p,$t,$r,$pd);
268     #escape NULL (binary 0x00) values
269     $page =~ s/\x00/\^0/g;
270
271     #trim 'ip_addr="1.2.3.4"' added by eProcessingNetwork Authorize.Net compat
272     $page =~ s/,ip_addr="[\d\.]+"$//;
273
274     my $csv = new Text::CSV_XS({ binary=>1, escape_char=>'', quote_char => $encap_character });
275     $csv->parse($page);
276     my @col = $csv->fields();
277
278     $self->server_response($page);
279     $self->avs_code($col[5]);
280     $self->order_number($col[6]);
281     $self->md5($col[37]);
282     $self->cvv2_response($col[38]);
283     $self->cavv_response($col[39]);
284
285     if($col[0] eq "1" ) { # Authorized/Pending/Test
286         $self->is_success(1);
287         $self->result_code($col[0]);
288         if ($col[4] =~ /^(.*)\s+(\d+)$/) { #eProcessingNetwork extra bits..
289           $self->authorization($2);
290         } else {
291           $self->authorization($col[4]);
292         }
293     } else {
294         $self->is_success(0);
295         $self->result_code($col[2]);
296         $self->error_message($col[3]);
297         unless ( $self->result_code() ) { #additional logging information
298           #$page =~ s/\x00/\^0/g;
299           $self->error_message($col[3].
300             " DEBUG: No x_response_code from server, ".
301             "(HTTPS response: $server_response) ".
302             "(HTTPS headers: ".
303               join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
304             "(Raw HTTPS content: $page)"
305           );
306         }
307     }
308 }
309
310 1;
311 __END__
312
313 =head1 NAME
314
315 Business::OnlinePayment::AuthorizeNet::AIM - AuthorizeNet AIM backend for Business::OnlinePayment
316
317 =head1 AUTHOR
318
319 Jason Kohles, jason@mediabang.com
320
321 Ivan Kohler <ivan-authorizenet@420.am> updated it for Authorize.Net protocol
322 3.0/3.1 and is the current maintainer.  Please send patches as unified diffs
323 (diff -u).
324
325 Jason Spence <jspence@lightconsulting.com> contributed support for separate
326 Authorization Only and Post Authorization steps and wrote some docs.
327 OST <services@ostel.com> paid for it.
328
329 T.J. Mather <tjmather@maxmind.com> sent a number of CVV2 patches.
330
331 Mike Barry <mbarry@cos.com> sent in a patch for the referer field.
332
333 Yuri V. Mkrtumyan <yuramk@novosoft.ru> sent in a patch to add the void action.
334
335 Paul Zimmer <AuthorizeNetpm@pzimmer.box.bepress.com> sent in a patch for
336 card-less post authorizations.
337
338 Daemmon Hughes <daemmon@daemmonhughes.com> sent in a patch for "transaction
339 key" authentication as well support for the recurring_billing flag and the md5
340 method that returns the MD5 hash which is returned by the gateway.
341
342 Steve Simitzis contributed a patch for better compatibility with
343 eProcessingNetwork's AuthorizeNet compatability mode.
344
345 =head1 SEE ALSO
346
347 perl(1). L<Business::OnlinePayment> L<Business::OnlinePayment::AuthorizeNet>.
348
349 =cut
350