first crack at module
[Business-OnlinePayment-IATSPayments.git] / lib / Business / OnlinePayment / IATSPayments.pm
1 package Business::OnlinePayment::IATSPayments;
2 use base qw( Business::OnlinePayment );
3
4 use warnings;
5 use strict;
6 use Data::Dumper;
7 use Business::CreditCard;
8 use SOAP::Lite;
9 #SOAP::Lite->import(+trace=>'debug');
10
11 our $VERSION = '0.01';
12 $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
13
14 sub _info {
15   {
16     'info_compat'       => '0.01',
17     'gateway_name'      => 'IATS Payments',
18     'gateway_url'       => 'http://home.iatspayments.com/',
19     'module_version'    => $VERSION,
20     'supported_types'   => [ 'CC', 'ECHECK' ],
21     #'token_support'     => 1,
22     'test_transaction'  => 1,
23
24     'supported_actions' => [ 'Normal Authorization',
25                              'Credit',
26                            ],
27   };
28 }
29
30 sub map_fields {
31     my($self) = @_;
32
33     my %content = $self->content();
34
35     # TYPE MAP
36     my %types = ( 'visa'               => 'CC',
37                   'mastercard'         => 'CC',
38                   'american express'   => 'CC',
39                   'discover'           => 'CC',
40                   'check'              => 'ECHECK',
41                 );
42     $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
43     $self->transaction_type($content{'type'});
44     
45     # ACTION MAP 
46     my $action = lc($content{'action'});
47     my %actions =
48       ( 'normal authorization'  => 'ProcessCreditCardV1',
49         'credit'                => 'ProcessCreditCardRefundWithTransactionIdV1',
50       );
51     my %check_actions =
52       ( 'normal authorization'  => 'ProcessACHEFTV1',
53         'credit'                => 'ProcessACHEFTRefundWithTransactionIdV1',
54       );
55
56     if ($self->transaction_type eq 'CC') {
57       $content{'action'} = $actions{$action} || $action;
58     } elsif ($self->transaction_type eq 'ECHECK') {
59
60       $content{'action'} = $check_actions{$action} || $action;
61
62       # ACCOUNT TYPE MAP
63       my %account_types = ('personal checking'   => 'CHECKING',
64                            'personal savings'    => 'SAVINGS',
65                            'business checking'   => 'CHECKING',
66                            'business savings'    => 'SAVINGS',
67                            #not technically B:OP valid i guess?
68                            'checking'            => 'CHECKING',
69                            'savings'             => 'SAVINGS',
70                           );
71       $content{'account_type'} = $account_types{lc($content{'account_type'})}
72                                  || $content{'account_type'};
73     }
74
75     # stuff it back into %content
76     $self->content(%content);
77
78 }
79
80 sub remap_fields {
81     my($self,%map) = @_;
82
83     my %content = $self->content();
84     foreach(keys %map) {
85         $content{$map{$_}} = $content{$_};
86     }
87     $self->content(%content);
88 }
89
90 # NA: VISA, MC, AMX, DSC
91 # UK: VISA, MC, AMX, MAESTR
92 our %mop = (
93   'VISA card'             => 'VISA',
94   'MasterCard'            => 'MC',
95   'Discover card'         => 'DSC',
96   'American Express card' => 'AMEX',
97   'Switch'                => 'MAESTR',
98   'Solo'                  => 'MAESTR',
99 );
100
101 #https://www.iatspayments.com/english/help/rejects.html
102 our %reject = (
103   '1' => 'Agent code has not been set up on the authorization system. Please call iATS at 1-888-955-5455.',
104   '2' => 'Unable to process transaction. Verify and re-enter credit card information.',
105   '3' => 'Invalid Customer Code.',
106   '4' => 'Incorrect expiration date.',
107   '5' => 'Invalid transaction. Verify and re-enter credit card information.',
108   '6' => 'Please have cardholder call the number on the back of the card.',
109   '7' => 'Lost or stolen card.',
110   '8' => 'Invalid card status.',
111   '9' => 'Restricted card status. Usually on corporate cards restricted to specific sales.',
112   '10' => 'Error. Please verify and re-enter credit card information.',
113   '11' => 'General decline code. Please have client call the number on the back of credit card',
114   '12' => 'Incorrect CVV2 or Expiry date',
115   '14' => 'The card is over the limit.',
116   '15' => 'General decline code. Please have client call the number on the back of credit card',
117   '16' => 'Invalid charge card number. Verify and re-enter credit card information.',
118   '17' => 'Unable to authorize transaction. Authorizer needs more information for approval.',
119   '18' => 'Card not supported by institution.',
120   '19' => 'Incorrect CVV2 security code',
121   '22' => 'Bank timeout. Bank lines may be down or busy. Re-try transaction later.',
122   '23' => 'System error. Re-try transaction later.',
123   '24' => 'Charge card expired.',
124   '25' => 'Capture card. Reported lost or stolen.',
125   '26' => 'Invalid transaction, invalid expiry date. Please confirm and retry transaction.',
126   '27' => 'Please have cardholder call the number on the back of the card.',
127   '32' => 'Invalid charge card number.',
128   '39' => 'Contact IATS 1-888-955-5455.',
129   '40' => 'Invalid card number. Card not supported by IATS.',
130   '41' => 'Invalid Expiry date.',
131   '42' => 'CVV2 required.',
132   '43' => 'Incorrect AVS.',
133   '45' => 'Credit card name blocked. Call iATS at 1-888-955-5455.',
134   '46' => 'Card tumbling. Call iATS at 1-888-955-5455.',
135   '47' => 'Name tumbling. Call iATS at 1-888-955-5455.',
136   '48' => 'IP blocked. Call iATS at 1-888-955-5455.',
137   '49' => 'Velocity 1 – IP block. Call iATS at 1-888-955-5455.',
138   '50' => 'Velocity 2 – IP block. Call iATS at 1-888-955-5455.',
139   '51' => 'Velocity 3 – IP block. Call iATS at 1-888-955-5455.',
140   '52' => 'Credit card BIN country blocked. Call iATS at 1-888-955-5455.',
141   '100' => 'DO NOT REPROCESS. Call iATS at 1-888-955-5455.',
142   #Timeout      The system has not responded in the time allotted. Call iATS at 1-888-955-5455.
143 );
144
145 our %failure_status = (
146   '7'  => 'stolen',
147   '8'  => 'inactive',
148   '9'  => 'inactive',
149   '14' => 'nsf',
150   '24' => 'expired',
151   '25' => 'stolen',
152   '45' => 'blacklisted',
153   '48' => 'blacklisted',
154   '49' => 'blacklisted',
155   '50' => 'blacklisted',
156   '51' => 'blacklisted',
157   '52' => 'blacklisted',
158   #'100' => # it sounds serious.  but why?  it says nothing specific
159 );
160
161 sub submit {
162   my($self) = @_;
163
164   $self->map_fields;
165
166   $self->remap_fields(
167         login             => 'agentCode',
168         password          => 'password',
169
170         description       => 'comment',
171         amount            => 'total',
172         invoice_number    => 'invoiceNum',
173         customer_ip       => 'customerIPAddress',
174
175         last_name         => 'lastName',
176         first_name        => 'firstName',
177         address           => 'address',
178         city              => 'city',
179         state             => 'state',
180         zip               => 'zipCode',
181         #country           => 'x_Country',
182
183         card_number       => 'creditCardNum',
184         expiration        => 'creditCardExpiry',
185         cvv2              => 'cvv2',
186
187         authorization     => 'transactionId',
188
189         account_type      => 'accountType',
190
191   );
192
193   my %content = $self->content();
194
195   $content{'mop'} = $mop{ cardtype($content{creditCardNum}) }
196     if $content{'type'} eq 'CC';
197
198   if ( $self->test_transaction ) {
199     $content{agentCode} = 'TEST88';
200     $content{password}  = 'TEST88';
201   }
202
203   my $base_uri =
204     ( ! $content{currency} || $content{currency} =~ /^(USD|CAD)$/i )
205       ? 'https://www.iatspayments.com/NetGate/'
206       : 'https://www.uk.iatspayments.com/NetGate/';
207
208   my $action = $content{action};
209
210   my $uri = $base_uri. "ProcessLink.asmx?op=$action";
211
212   my %data = map { $_ => $content{$_} } (qw(
213     agentCode
214     password
215     comment
216     total
217     customerIPAddress
218   ));
219
220   if ( $action =~ /RefundWithTransacdtionIdV[\d\.]+$/ ) {
221
222     $data{ $_ } = $content{$_} for qw(
223       transactionId
224     );
225
226   } else {
227
228     $data{ $_ } = $content{$_} for qw(
229       invoiceNum
230       lastName
231       firstName
232       address
233       city
234       state
235       zipCode
236     );
237
238     if ( $content{'type'} eq 'CC' ) {
239
240       $data{$_} = $content{$_}
241         for qw( creditCardNum creditCardExpiry cvv2 mop );
242
243     } elsif ( $content{'type'} eq 'ECHECK' ) {
244
245       $data{'accountNum'}= $content{'routing_code'}. $content{'account_number'};
246
247       $data{$_} = $content{$_}
248         for qw( accountType );
249
250     }
251
252   }
253
254   my @opts = map { SOAP::Data->name($_)->value( $data{$_} ) }
255                keys %data;
256
257   my $result = SOAP::Lite
258                  ->proxy($uri)
259                  ->default_ns($base_uri)
260                  #->on_action( sub { join '/', @_ } )
261                  ->on_action( sub { join '', @_ } )
262                  ->autotype(0)
263
264                  ->$action( @opts )
265
266                  ->result();
267
268   my $iatsresponse = $result->{IATSRESPONSE};
269
270   if ( $iatsresponse->{STATUS} eq 'Failure' && $iatsresponse->{ERRORS} ) {
271     die 'iATS Payments error: '. $iatsresponse->{ERRORS}. "\n";
272   } elsif ( $iatsresponse->{STATUS} ne 'Success' ) {
273     die "Couldn't parse iATS Payments response: ". Dumper($result);
274   }
275
276   my $processresult = $iatsresponse->{PROCESSRESULT};
277
278   $self->authorization($processresult->{TRANSACTIONID} || '');
279
280   if ( $processresult->{AUTHORIZATIONRESULT} =~ /^\s*OK(:\s*\d+)?(:(\w))?\s*$/i ) {
281     $self->is_success(1);
282     $self->avs_code($3); #avs_code?  sure looks like one
283
284   } elsif ( $processresult->{AUTHORIZATIONRESULT} =~ /^\s*Timeout\s*$/i ) {
285     $self->is_success(0);
286     $self->error_message('The system has not responded in the time allotted. '.
287                          'Call iATS at 1-888-955-5455.');
288
289   } elsif ( $processresult->{AUTHORIZATIONRESULT}
290               =~ /^\s*REJ(ECT)?:\s*(\d+)\s*$/i
291           )
292   {
293     $self->is_success(0);
294     $self->error_message( $reject{$2} || $processresult->{AUTHORIZATIONRESULT});
295     $self->failure_status( $failure_status{$2} || 'decline' );
296
297   } else {
298     die "No/Unknown AUTHORIZATIONRESULT iATS Payments response: ".
299           Dumper($processresult);
300   }
301
302 }
303
304 1;
305
306 __END__
307
308 =head1 NAME
309
310 Business::OnlinePayment::IATSPayments - IATS Payments backend for Business::OnlinePayment
311
312 =head1 SYNOPSIS
313
314   use Business::OnlinePayment;
315
316   my $tx =
317     new Business::OnlinePayment( 'IATSPayments' );
318
319   $tx->content(
320       login          => 'TEST88', # agentCode
321       password       => 'TEST88', #password 
322
323       type           => 'CC',
324       action         => 'Normal Authorization',
325       amount         => '1.00',
326
327       first_name     => 'Tofu',
328       last_name      => 'Beast',
329       address        => '123 Anystreet',
330       city           => 'Anywhere',
331       state          => 'UT',
332       zip            => '84058',
333
334       card_number    => '4111111111111111',
335       expiration     => '09/20',
336       cvv2           => '124',
337
338       #optional
339       description    => 'Business::OnlinePayment test',
340       customer_ip    => '1.2.3.4',
341       invoice_num    => 54,
342   );
343   $tx->submit();
344
345   if($tx->is_success()) {
346       print "Card processed successfully: ".$tx->authorization."\n";
347   } else {
348       print "Card was rejected: ".$tx->error_message."\n";
349   }
350
351 =head1 SUPPORTED TRANSACTION TYPES
352
353 =head2 CC, Visa, MasterCard, American Express, Discover
354
355 Content required: type, login, action, amount, card_number, expiration.
356
357 =head2 Check
358
359 Content required: type, login, action, amount, name, account_number, routing_code.
360
361 =head1 DESCRIPTION
362
363 For detailed information see L<Business::OnlinePayment>.
364
365 =head1 METHODS AND FUNCTIONS
366
367 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
368
369 =head2 result_code
370
371 Returns the response error code.
372
373 =head2 error_message
374
375 Returns the response error number.
376
377 =head2 action
378
379 The following actions are valid
380
381   Normal Authorization
382   Credit
383
384 =head1 COMPATIBILITY
385
386 Business::OnlinePayment::IATSPayments uses iATS WebServices ProcessLink 4.0
387 and (for tokenization support) iATS WebServices CustomerLink 4.0.
388
389 =head1 AUTHORS
390
391 Ivan Kohler <ivan-iatspayments@freeside.biz>
392
393 =head1 SEE ALSO
394
395 perl(1). L<Business::OnlinePayment>.
396
397 =cut
398