0.03
[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.03';
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 if defined($content{'expiration'});
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'} ? 
160       ($content{'ship_first_name'}.' '.$content{'ship_last_name'}) : '';
161   $self->content(%content);
162 }
163
164 sub submit {
165     my($self) = @_;
166
167     $self->map_fields();
168
169     $self->remap_fields(%fields);
170
171     my %content = $self->content;
172     my $type = $content{'type'}; # what we call "action"
173     my $payment = $content{'payment'}; # what we call "type"
174     if ( $DEBUG >= 3  ) {
175       warn "content:$_ => $content{$_}\n" foreach keys %content;
176     }
177
178     my @required_fields = ( @{$required{'ALL'}} );
179     push @required_fields, @{$required{$type}} if exists($required{$type});
180     push @required_fields, @{$required{"$type:$payment"}} if exists($required{"$type:$payment"});
181
182     $self->required_fields(@required_fields);
183
184     my @allowed_fields = @required_fields;
185     push @allowed_fields, @{$optional{'ALL'}};
186     push @allowed_fields, @{$optional{$type}} if exists($optional{$type});
187     push @allowed_fields, @{$optional{"$type:$payment"}} if exists($required{"$type:$payment"});
188
189     my %post_data = $self->get_fields(@allowed_fields);
190
191     if ( $DEBUG ) {
192       warn "post_data:$_ => $post_data{$_}\n" foreach keys %post_data;
193     }
194
195     my($page,$server_response) = $self->https_post(\%post_data);
196     if ( $DEBUG ) {
197       warn "response page: $page\n";
198     }
199
200     my $response;
201     if ($server_response =~ /200/){
202       $response = {map { split '=', $_, 2 } split '&', $page};
203     }
204     else {
205       die "HTTPS error: '$server_response'\n";
206     }
207
208     $response->{$_} =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg
209       foreach keys %$response;
210
211     if ( $DEBUG ) {
212       warn "response:$_ => $response->{$_}\n" foreach keys %$response;
213     }
214
215     $self->is_success(0);
216     my $error;
217     if( $response->{response} == 1 ) {
218       $self->is_success(1);
219     }
220     elsif( $response->{response} == 2 ) {
221       $error = $response->{responsetext};
222       my $code = $response->{response_code};
223       $self->failure_status($failure_status{$code}) if exists($failure_status{$code});
224     }
225     elsif( $response->{response} == 3 ) {
226       $error = "Transaction error: '".$response->{responsetext}."'";
227     }
228     else {
229       $error = "Could not interpret server response: '$page'";
230     }
231     $self->order_number($response->{transactionid});
232     $self->authorization($response->{authcode});
233     $self->avs_code($response->{avsresponse});
234     $self->cvv2_response($response->{cvvresponse});
235     $self->result_code($response->{response_code});
236     $self->error_message($error);
237     $self->server_response($response);
238 }
239
240 1;
241 __END__
242
243 =head1 NAME
244
245 Business::OnlinePayment::NMI - Network Merchants backend for Business::OnlinePayment
246
247 =head1 SYNOPSIS
248
249   use Business::OnlinePayment;
250
251   my $tx = new Business::OnlinePayment("NMI");
252   $tx->content(
253       login          => 'mylogin',
254       password       => 'mypass',
255       action         => 'Normal Authorization',
256       description    => 'Business::OnlinePayment test',
257       amount         => '49.95',
258       invoice_number => '100100',
259       name           => 'Tofu Beast',
260       card_number    => '46464646464646',
261       expiration     => '11/08',
262       address        => '1234 Bean Curd Lane, San Francisco',
263       zip            => '94102',
264   );
265   $tx->submit();
266
267   if($tx->is_success()) {
268       print "Card processed successfully: ".$tx->authorization."\n";
269   } else {
270       print "Card was rejected: ".$tx->error_message."\n";
271   }
272
273 =head1 DESCRIPTION
274
275 For detailed information see L<Business::OnlinePayment>.
276
277 =head1 SUPPORTED TRANSACTION TYPES
278
279 =head2 Credit Card
280
281 Normal Authorization, Authorization Only, Post Authorization, Void, Credit.
282
283 =head2 Check
284
285 Normal Authorization, Void, Credit.
286
287 =head1 NOTES
288
289 Credit is handled using NMI's 'refund' action, which applies the credit against 
290 a specific payment.
291
292 Post Authorization, Void, and Credit require C<order_number> to be set with the 
293 transaction ID of the previous authorization.
294
295 =head1 COMPATIBILITY
296
297 This module implements the NMI Direct Post API, June 2007 revision.
298
299 =head1 AUTHOR
300
301 Mark Wells <mark@freeside.biz>
302
303 Based in part on Business::OnlinePayment::USAePay by Jeff Finucane 
304 <jeff@cmh.net>.
305
306 =head1 SEE ALSO
307
308 perl(1). L<Business::OnlinePayment>.
309
310 =head1 ADVERTISEMENT
311
312 Need a complete, open-source back-office and customer self-service solution?
313 The Freeside software includes support for credit card and electronic check
314 processing, integrated trouble ticketing, and customer signup and self-service
315 web interfaces.
316
317 http://freeside.biz/freeside/
318
319 =cut
320