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