typo
[Business-OnlinePayment-NMI.git] / NMI.pm
1 package Business::OnlinePayment::NMI;
2
3 use strict;
4 use Carp;
5 use Business::OnlinePayment 3;
6 use Business::OnlinePayment::HTTPS;
7 use Digest::MD5 qw(md5_hex);
8 use URI::Escape;
9 use vars qw($VERSION @ISA $DEBUG);
10
11 @ISA = qw(Business::OnlinePayment::HTTPS);
12 $VERSION = '0.01';
13
14 $DEBUG = 0;
15
16 sub _info {
17   {
18     'info_compat'           => '0.01',
19     'gateway_name'          => 'Network Merchants',
20     'gateway_url'           => 'https://www.nmi.com',
21     'module_version'        => $VERSION,
22     'supported_types'       => [ 'CC', 'ECHECK' ],
23     'supported_actions'     => {
24                                   CC => [
25                                     'Normal Authorization',
26                                     'Authorization Only',
27                                     'Post Authorization',
28                                     'Credit',
29                                     'Void',
30                                     ],
31                                   ECHECK => [
32                                     'Normal Authorization',
33                                     'Credit',
34                                     'Void',
35                                     ],
36     },
37   };
38 }
39
40 my %actions = (
41   'normal authorization' => 'sale',
42   'authorization only'   => 'auth',
43   'post authorization'   => 'capture',
44   'credit'               => 'refund',
45   'void'                 => 'void',
46 );
47 my %types = (
48   'cc'  => 'creditcard',
49   'echeck' => 'check',
50 );
51
52 my %fields = (
53 # NMI Direct Post API, June 2007
54   action          => 'type', # special
55   login           => 'username',
56   password        => 'password',
57   card_number     => 'ccnumber',
58   expiration      => 'ccexp',
59   name            => 'checkname',
60   routing_code    => 'checkaba',
61   account_number  => 'checkaccount',
62   account_holder_type => 'account_holder_type',
63   account_type    => 'account_type',
64   amount          => 'amount',
65   cvv2            => 'cvv',
66   payment         => 'payment', # special
67   description     => 'orderdescription',
68   invoice_number  => 'orderid',
69   customer_ip     => 'ipaddress',
70   tax             => 'tax',
71   freight         => 'shipping',
72   po_number       => 'ponumber',
73   first_name      => 'firstname',
74   last_name       => 'lastname',
75   company         => 'company',
76   address         => 'address1',
77   city            => 'city',
78   state           => 'state',
79   zip             => 'zip',
80   country         => 'country',
81   order_number    => 'transactionid', # used for capture/void/refund
82 );
83
84 $fields{"ship_$_"} = 'shipping_'.$fields{$_} 
85   foreach(qw(first_name last_name company address city state zip country)) ;
86
87 my %required = (
88 'ALL'             => [ qw( type username password payment ) ],
89 'sale'            => [ 'amount' ],
90 'sale:creditcard' => [ 'ccnumber', 'ccexp' ],
91 'sale:check'      => [ qw( checkname checkaba checkaccount account_holder_type account_type ) ],
92 'auth:creditcard' => [ qw( amount ccnumber ccexp ) ],
93 'capture'         => [ 'amount', 'transactionid' ],
94 'refund'          => [ 'amount', 'transactionid' ],
95 'void'            => [ 'transactionid' ],
96 # not supported: update
97 ),
98
99 my %optional = (
100 'ALL'             => [],
101 'sale'            => [ qw( orderdescription orderid ipaddress tax 
102                            shipping ponumber firstname lastname company 
103                            address1 city state zip country phone fax email 
104                            shipping_firstname shipping_lastname
105                            shipping_company shipping_address1 shipping_city 
106                            shipping_state shipping_zip shipping_country 
107                            ) ],
108 'sale:creditcard' => [ 'cvv' ],
109 'sale:check' => [],
110 'auth:creditcard' => [ qw( orderdescription orderid ipaddress tax 
111                            shipping ponumber firstname lastname company 
112                            address1 city state zip country phone fax email 
113                            shipping_firstname shipping_lastname
114                            shipping_company shipping_address1 shipping_city 
115                            shipping_state shipping_zip shipping_country 
116                            cvv ) ],
117 'capture'         => [ 'orderid' ],
118 'refund'          => [ 'amount' ],
119 );
120
121 my %failure_status = (
122 200 => 'decline',
123 201 => 'decline',
124 202 => 'nsf',
125 203 => 'nsf',
126 223 => 'expired',
127 250 => 'pickup',
128 252 => 'stolen',
129 # add others here as needed; very little code uses failure_status at present
130 );
131
132 sub set_defaults {
133     my $self = shift;
134     $self->server('secure.networkmerchants.com');
135     $self->port('443');
136     $self->path('/api/transact.php');
137     $self->build_subs(qw(avs_code cvv2_response failure_status));
138 }
139
140 sub map_fields {
141   my($self) = shift;
142
143   my %content = $self->content();
144
145   if($self->test_transaction) {
146     # Public test account.
147     $content{'login'} = 'demo';
148     $content{'password'} = 'password';
149   }
150
151   $content{'payment'} = $types{lc($content{'type'})} or die "Payment method '$content{type}' not supported.\n";
152   $content{'action'} = $actions{lc($content{'action'})} or die "Transaction type '$content{action}' not supported.\n";
153
154   $content{'expiration'} =~ s/\D//g;
155
156   $content{'account_type'} ||= 'personal checking';
157   @content{'account_holder_type', 'account_type'} = 
158     map {lc} split /\s/, $content{'account_type'};
159   $content{'ship_name'} = $content{'ship_first_name'}.' '.$content{'ship_last_name'};
160   $self->content(%content);
161 }
162
163 sub submit {
164     my($self) = @_;
165
166     $self->map_fields();
167
168     $self->remap_fields(%fields);
169
170     my %content = $self->content;
171     my $type = $content{'type'}; # what we call "action"
172     my $payment = $content{'payment'}; # what we call "type"
173     if ( $DEBUG >= 3  ) {
174       warn "content:$_ => $content{$_}\n" foreach keys %content;
175     }
176
177     my @required_fields = ( @{$required{'ALL'}} );
178     push @required_fields, @{$required{$type}} if exists($required{$type});
179     push @required_fields, @{$required{"$type:$payment"}} if exists($required{"$type:$payment"});
180
181     $self->required_fields(@required_fields);
182
183     my @allowed_fields = @required_fields;
184     push @allowed_fields, @{$optional{'ALL'}};
185     push @allowed_fields, @{$optional{$type}} if exists($optional{$type});
186     push @allowed_fields, @{$optional{"$type:$payment"}} if exists($required{"$type:$payment"});
187
188     my %post_data = $self->get_fields(@allowed_fields);
189
190     if ( $DEBUG ) {
191       warn "post_data:$_ => $post_data{$_}\n" foreach keys %post_data;
192     }
193
194     my($page,$server_response) = $self->https_post(\%post_data);
195     if ( $DEBUG ) {
196       warn "response page: $page\n";
197     }
198
199     my $response;
200     if ($server_response =~ /200/){
201       $response = {map { split '=', $_, 2 } split '&', $page};
202     }
203     else {
204       die "HTTPS error: '$server_response'\n";
205     }
206
207     $response->{$_} =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg
208       foreach keys %$response;
209
210     if ( $DEBUG ) {
211       warn "response:$_ => $response->{$_}\n" foreach keys %$response;
212     }
213
214     $self->is_success(0);
215     my $error;
216     if( $response->{response} == 1 ) {
217       $self->is_success(1);
218     }
219     elsif( $response->{response} == 2 ) {
220       $error = $response->{responsetext};
221       my $code = $response->{response_code};
222       $self->failure_status($failure_status{$code}) if exists($failure_status{$code});
223     }
224     elsif( $response->{response} == 3 ) {
225       $error = "Transaction error: '".$response->{responsetext}."'";
226     }
227     else {
228       $error = "Could not interpret server response: '$page'";
229     }
230     $self->order_number($response->{transactionid});
231     $self->authorization($response->{authcode});
232     $self->avs_code($response->{avsresponse});
233     $self->cvv2_response($response->{cvvresponse});
234     $self->result_code($response->{response_code});
235     $self->error_message($error);
236     $self->server_response($response);
237 }
238
239 1;
240 __END__
241
242 =head1 NAME
243
244 Business::OnlinePayment::NMI - Network Merchants backend for Business::OnlinePayment
245
246 =head1 SYNOPSIS
247
248   use Business::OnlinePayment;
249
250   my $tx = new Business::OnlinePayment("NMI");
251   $tx->content(
252       login          => 'mylogin',
253       password       => 'mypass',
254       action         => 'Normal Authorization',
255       description    => 'Business::OnlinePayment test',
256       amount         => '49.95',
257       invoice_number => '100100',
258       name           => 'Tofu Beast',
259       card_number    => '46464646464646',
260       expiration     => '11/08',
261       address        => '1234 Bean Curd Lane, San Francisco',
262       zip            => '94102',
263   );
264   $tx->submit();
265
266   if($tx->is_success()) {
267       print "Card processed successfully: ".$tx->authorization."\n";
268   } else {
269       print "Card was rejected: ".$tx->error_message."\n";
270   }
271
272 =head1 DESCRIPTION
273
274 For detailed information see L<Business::OnlinePayment>.
275
276 =head1 SUPPORTED TRANSACTION TYPES
277
278 =head2 Credit Card
279
280 Normal Authorization, Authorization Only, Post Authorization, Void, Credit.
281
282 =head2 Check
283
284 Normal Authorization, Void, Credit.
285
286 =head1 NOTES
287
288 Credit is handled using NMI's 'refund' action, which applies the credit against 
289 a specific payment.
290
291 Post Authorization, Void, and Credit require C<order_number> to be set with the 
292 transaction ID of the previous authorization.
293
294 =head1 COMPATIBILITY
295
296 This module implements the NMI Direct Post API, June 2007 revision.
297
298 =head1 AUTHOR
299
300 Mark Wells <mark@freeside.biz>
301
302 Based in part on Business::OnlinePayment::USAePay by Jeff Finucane 
303 <jeff@cmh.net>.
304
305 =head1 SEE ALSO
306
307 perl(1). L<Business::OnlinePayment>.
308
309 =cut
310