When IPPay returns a non-sensical response, don't include it in the error message...
[Business-OnlinePayment-IPPay.git] / IPPay.pm
1 package Business::OnlinePayment::IPPay;
2
3 use strict;
4 use Carp;
5 use Tie::IxHash;
6 use XML::Simple;
7 use XML::Writer;
8 use Locale::Country;
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11 use vars qw($VERSION $DEBUG @ISA $me);
12
13 @ISA = qw(Business::OnlinePayment::HTTPS);
14 $VERSION = '0.09_02';
15 $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
16
17 $DEBUG = 0;
18 $me = 'Business::OnlinePayment::IPPay';
19
20 sub _info {
21   {
22     'info_version'          => '0.01',
23     'module_version'        => $VERSION,
24     'supported_types'       => [ qw( CC ECHECK ) ],
25     'supported_actions'     => { 'CC' => [
26                                      'Normal Authorization',
27                                      'Authorization Only',
28                                      'Post Authorization',
29                                      'Void',
30                                      'Credit',
31                                      'Reverse Authorization',
32                                    ],
33                                    'ECHECK' => [
34                                      'Normal Authorization',
35                                      'Void',
36                                      'Credit',
37                                    ],
38                                  },
39     'CC_void_requires_card' => 1,
40     'ECHECK_void_requires_account' => 1,
41   };
42 }
43
44 sub set_defaults {
45     my $self = shift;
46     my %opts = @_;
47
48     # standard B::OP methods/data
49     $self->server('gtwy.ippay.com') unless $self->server;
50     $self->port('443') unless $self->port;
51     $self->path('/ippay') unless $self->path;
52
53     $self->build_subs(qw( order_number avs_code cvv2_response
54                           response_page response_code response_headers
55                      ));
56
57     $DEBUG = exists($opts{debug}) ? $opts{debug} : 0;
58
59     # module specific data
60     my %_defaults = ();
61     foreach my $key (keys %opts) {
62       $key =~ /^default_(\w*)$/ or next;
63       $_defaults{$1} = $opts{$key};
64       delete $opts{$key};
65     }
66     $self->{_defaults} = \%_defaults;
67 }
68
69 sub map_fields {
70     my($self) = @_;
71
72     my %content = $self->content();
73
74     # TYPE MAP
75     my %types = ( 'visa'               => 'CC',
76                   'mastercard'         => 'CC',
77                   'american express'   => 'CC',
78                   'discover'           => 'CC',
79                   'check'              => 'ECHECK',
80                 );
81     $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
82     $self->transaction_type($content{'type'});
83     
84     # ACTION MAP 
85     my $action = lc($content{'action'});
86     my %actions =
87       ( 'normal authorization'            => 'SALE',
88         'authorization only'              => 'AUTHONLY',
89         'post authorization'              => 'CAPT',
90         'reverse authorization'           => 'REVERSEAUTH',
91         'void'                            => 'VOID',
92         'credit'                          => 'CREDIT',
93       );
94     my %check_actions =
95       ( 'normal authorization'            => 'CHECK',
96         'void'                            => 'VOIDACH',
97         'credit'                          => 'REVERSAL',
98       );
99
100     if ($self->transaction_type eq 'CC') {
101       $content{'TransactionType'} = $actions{$action} || $action;
102     } elsif ($self->transaction_type eq 'ECHECK') {
103
104       $content{'TransactionType'} = $check_actions{$action} || $action;
105
106       # ACCOUNT TYPE MAP
107       my %account_types = ('personal checking'   => 'CHECKING',
108                            'personal savings'    => 'SAVINGS',
109                            'business checking'   => 'CHECKING',
110                            'business savings'    => 'SAVINGS',
111                            #not technically B:OP valid i guess?
112                            'checking'            => 'CHECKING',
113                            'savings'             => 'SAVINGS',
114                           );
115       $content{'account_type'} = $account_types{lc($content{'account_type'})}
116                                  || $content{'account_type'};
117     }
118
119     $content{Origin} = 'RECURRING' 
120       if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
121
122     # stuff it back into %content
123     $self->content(%content);
124
125 }
126
127 sub expdate_month {
128   my ($self, $exp) = (shift, shift);
129   my $month;
130   if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
131     $month  = sprintf( "%02d", $1 );
132   }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
133     $month  = sprintf( "%02d", $1 );
134   }
135   return $month;
136 }
137
138 sub expdate_year {
139   my ($self, $exp) = (shift, shift);
140   my $year;
141   if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
142     $year  = sprintf( "%02d", $1 );
143   }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
144     $year  = sprintf( "%02d", $1 );
145   }
146   return $year;
147 }
148
149 sub revmap_fields {
150   my $self = shift;
151   tie my(%map), 'Tie::IxHash', @_;
152   my %content = $self->content();
153   map {
154         my $value;
155         if ( ref( $map{$_} ) eq 'HASH' ) {
156           $value = $map{$_} if ( keys %{ $map{$_} } );
157         }elsif( ref( $map{$_} ) ) {
158           $value = ${ $map{$_} };
159         }elsif( exists( $content{ $map{$_} } ) ) {
160           $value = $content{ $map{$_} };
161         }
162
163         if (defined($value)) {
164           ($_ => $value);
165         }else{
166           ();
167         }
168       } (keys %map);
169 }
170
171 sub submit {
172   my($self) = @_;
173
174   $self->is_success(0);
175   $self->map_fields();
176
177   my @required_fields = qw(action login password type);
178
179   my $action = lc($self->{_content}->{action});
180   my $type = $self->transaction_type();
181   if ( $action eq 'normal authorization'
182     || $action eq 'credit'
183     || $action eq 'authorization only' && $type eq 'CC')
184   {
185     push @required_fields, qw( amount );
186
187     push @required_fields, qw( card_number expiration )
188       if ($type eq "CC"); 
189         
190     push @required_fields,
191       qw( routing_code account_number name ) # account_type
192       if ($type eq "ECHECK");
193         
194   }elsif ( $action eq 'post authorization' && $type eq 'CC') {
195     push @required_fields, qw( order_number );
196   }elsif ( $action eq 'reverse authorization' && $type eq 'CC') {
197     push @required_fields, qw( order_number card_number expiration amount );
198   }elsif ( $action eq 'void') {
199     push @required_fields, qw( order_number amount );
200
201     push @required_fields, qw( authorization card_number )
202       if ($type eq "CC");
203
204     push @required_fields,
205       qw( routing_code account_number name ) # account_type
206       if ($type eq "ECHECK");
207
208   }else{
209     croak "$me can't handle transaction type: ".
210       $self->{_content}->{action}. " for ".
211       $self->transaction_type();
212   }
213
214   my %content = $self->content();
215   foreach ( keys ( %{($self->{_defaults})} ) ) {
216     $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
217   }
218   if ($self->test_transaction()) {
219     $content{'login'} = 'TESTTERMINAL';
220   }
221   $self->content(%content);
222
223   $self->required_fields(@required_fields);
224
225   #quick validation because ippay dumps an error indecipherable to the end user
226   if (grep { /^routing_code$/ } @required_fields) {
227     unless( $content{routing_code} =~ /^\d{9}$/ ) {
228       $self->_error_response('Invalid routing code');
229       return;
230     }
231   }
232
233   my $transaction_id = $content{'order_number'};
234   unless ($transaction_id) {
235     my ($page, $server_response, %headers) = $self->https_get('dummy' => 1);
236     warn "fetched transaction id: (HTTPS response: $server_response) ".
237          "(HTTPS headers: ".
238          join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
239          "(Raw HTTPS content: $page)"
240       if $DEBUG > 1;
241     return unless $server_response=~ /^200/;
242     $transaction_id = $page;
243   }
244
245   my $cardexpmonth = $self->expdate_month($content{expiration});
246   my $cardexpyear  = $self->expdate_year($content{expiration});
247   my $cardstartmonth = $self->expdate_month($content{card_start});
248   my $cardstartyear  = $self->expdate_year($content{card_start});
249  
250   my $amount;
251   if (defined($content{amount})) {
252     $amount = sprintf("%.2f", $content{amount});
253     $amount =~ s/\.//;
254   }
255
256   my $check_number = $content{check_number} || "100"  # make one up
257     if($content{account_number});
258
259   my $terminalid = $content{login} if $type eq 'CC';
260   my $merchantid = $content{login} if $type eq 'ECHECK';
261
262   my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
263   $country  = country_code2code( $content{country},
264                                  LOCALE_CODE_ALPHA_2,
265                                  LOCALE_CODE_ALPHA_3
266                                )
267     unless $country;
268   $country = $content{country}
269     unless $country;
270   $country = uc($country) if $country;
271
272   my $ship_country =
273     country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
274   $ship_country  = country_code2code( $content{ship_country},
275                                  LOCALE_CODE_ALPHA_2,
276                                  LOCALE_CODE_ALPHA_3
277                                )
278     unless $ship_country;
279   $ship_country = $content{ship_country}
280     unless $ship_country;
281   $ship_country = uc($ship_country) if $ship_country;
282
283   tie my %ach, 'Tie::IxHash',
284     $self->revmap_fields(
285                           #wtf, this is a "Type"" attribute of the ACH element,
286                           # not a child element like the others
287                           #AccountType         => 'account_type',
288                           AccountNumber       => 'account_number',
289                           ABA                 => 'routing_code',
290                           CheckNumber         => \$check_number,
291                         );
292
293   tie my %industryinfo, 'Tie::IxHash',
294     $self->revmap_fields(
295                           Type                => 'IndustryInfo',
296                         );
297
298   tie my %shippingaddr, 'Tie::IxHash',
299     $self->revmap_fields(
300                           Address             => 'ship_address',
301                           City                => 'ship_city',
302                           StateProv           => 'ship_state',
303                           Country             => \$ship_country,
304                           Phone               => 'ship_phone',
305                         );
306
307   unless ( $type ne 'CC' || keys %shippingaddr ) {
308     tie %shippingaddr, 'Tie::IxHash',
309       $self->revmap_fields(
310                             Address             => 'address',
311                             City                => 'city',
312                             StateProv           => 'state',
313                             Country             => \$country,
314                             Phone               => 'phone',
315                           );
316   }
317   delete $shippingaddr{Country} unless $shippingaddr{Country};
318
319   tie my %shippinginfo, 'Tie::IxHash',
320     $self->revmap_fields(
321                           CustomerPO          => 'CustomerPO',
322                           ShippingMethod      => 'ShippingMethod',
323                           ShippingName        => 'ship_name',
324                           ShippingAddr        => \%shippingaddr,
325                         );
326
327   tie my %req, 'Tie::IxHash',
328     $self->revmap_fields(
329                           TransactionType     => 'TransactionType',
330                           TerminalID          => 'login',
331 #                          TerminalID          => \$terminalid,
332 #                          MerchantID          => \$merchantid,
333                           TransactionID       => \$transaction_id,
334                           RoutingCode         => 'RoutingCode',
335                           Approval            => 'authorization',
336                           BatchID             => 'BatchID',
337                           Origin              => 'Origin',
338                           Password            => 'password',
339                           OrderNumber         => 'invoice_number',
340                           CardNum             => 'card_number',
341                           CVV2                => 'cvv2',
342                           Issue               => 'issue_number',
343                           CardExpMonth        => \$cardexpmonth,
344                           CardExpYear         => \$cardexpyear,
345                           CardStartMonth      => \$cardstartmonth,
346                           CardStartYear       => \$cardstartyear,
347                           Track1              => 'track1',
348                           Track2              => 'track2',
349                           ACH                 => \%ach,
350                           CardName            => 'name',
351                           DispositionType     => 'DispositionType',
352                           TotalAmount         => \$amount,
353                           FeeAmount           => 'FeeAmount',
354                           TaxAmount           => 'TaxAmount',
355                           BillingAddress      => 'address',
356                           BillingCity         => 'city',
357                           BillingStateProv    => 'state',
358                           BillingPostalCode   => 'zip',
359                           BillingCountry      => \$country,
360                           BillingPhone        => 'phone',
361                           Email               => 'email',
362                           UserIPAddr          => 'customer_ip',
363                           UserHost            => 'UserHost',
364                           UDField1            => 'UDField1',
365                           UDField2            => 'UDField2',
366                           UDField3            => \"$me $VERSION", #'UDField3',
367                           ActionCode          => 'ActionCode',
368                           IndustryInfo        => \%industryinfo,
369                           ShippingInfo        => \%shippinginfo,
370                         );
371   delete $req{BillingCountry} unless $req{BillingCountry};
372
373   my $post_data;
374   my $writer = new XML::Writer( OUTPUT      => \$post_data,
375                                 DATA_MODE   => 1,
376                                 DATA_INDENT => 1,
377                                 ENCODING    => 'us-ascii',
378                               );
379   $writer->xmlDecl();
380   $writer->startTag('JetPay');
381   foreach ( keys ( %req ) ) {
382     $self->_xmlwrite($writer, $_, $req{$_});
383   }
384   $writer->endTag('JetPay');
385   $writer->end();
386
387   warn "$post_data\n" if $DEBUG > 1;
388
389   my ($page,$server_response,%headers) = $self->https_post($post_data);
390
391   warn "$page\n" if $DEBUG > 1;
392
393   my $response = {};
394   if ($server_response =~ /^200/){
395     $response = XMLin($page);
396     if (  exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
397       $self->error_message($response->{ResponseText});
398     }else{
399       $self->error_message($response->{ErrMsg});
400     }
401 #  }else{
402 #    $self->error_message("Server Failed");
403   }
404
405   $self->result_code($response->{ActionCode} || '');
406   $self->order_number($response->{TransactionID} || '');
407   $self->authorization($response->{Approval} || '');
408   $self->cvv2_response($response->{CVV2} || '');
409   $self->avs_code($response->{AVS} || '');
410
411   $self->is_success($self->result_code() eq '000' ? 1 : 0);
412
413   unless ($self->is_success()) {
414     unless ( $self->error_message() ) {
415       if ( $DEBUG ) {
416         #additional logging information, possibly too sensitive for an error msg
417         # (IPPay seems to have a failure mode where they return the full
418         #  original request including card number)
419         $self->error_message(
420           "(HTTPS response: $server_response) ".
421           "(HTTPS headers: ".
422             join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
423           "(Raw HTTPS content: $page)"
424         );
425       } else {
426         $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)');
427       }
428     }
429   }
430
431 }
432
433 sub _error_response {
434   my ($self, $error_message) = (shift, shift);
435   $self->result_code('');
436   $self->order_number('');
437   $self->authorization('');
438   $self->cvv2_response('');
439   $self->avs_code('');
440   $self->is_success( 0);
441   $self->error_message($error_message);
442 }
443
444 sub _xmlwrite {
445   my ($self, $writer, $item, $value) = @_;
446
447   my %att = ();
448   if ( $item eq 'ACH' ) {
449     $att{'Type'} = $self->{_content}->{'account_type'}
450       if $self->{_content}->{'account_type'}; #necessary so we don't pass empty?
451     $att{'SEC'}  = 'PPD';
452   }
453
454   $writer->startTag($item, %att);
455
456   if ( ref( $value ) eq 'HASH' ) {
457     foreach ( keys ( %$value ) ) {
458       $self->_xmlwrite($writer, $_, $value->{$_});
459     }
460   }else{
461     $writer->characters($value);
462   }
463
464   $writer->endTag($item);
465 }
466
467 1;
468
469 __END__
470
471 =head1 NAME
472
473 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
474
475 =head1 SYNOPSIS
476
477   use Business::OnlinePayment;
478
479   my $tx =
480     new Business::OnlinePayment( "IPPay",
481                                  'default_Origin' => 'PHONE ORDER',
482                                );
483   $tx->content(
484       type           => 'VISA',
485       login          => 'testdrive',
486       password       => '', #password 
487       action         => 'Normal Authorization',
488       description    => 'Business::OnlinePayment test',
489       amount         => '49.95',
490       customer_id    => 'tfb',
491       name           => 'Tofu Beast',
492       address        => '123 Anystreet',
493       city           => 'Anywhere',
494       state          => 'UT',
495       zip            => '84058',
496       card_number    => '4007000000027',
497       expiration     => '09/02',
498       cvv2           => '1234', #optional
499   );
500   $tx->submit();
501
502   if($tx->is_success()) {
503       print "Card processed successfully: ".$tx->authorization."\n";
504   } else {
505       print "Card was rejected: ".$tx->error_message."\n";
506   }
507
508 =head1 SUPPORTED TRANSACTION TYPES
509
510 =head2 CC, Visa, MasterCard, American Express, Discover
511
512 Content required: type, login, action, amount, card_number, expiration.
513
514 =head2 Check
515
516 Content required: type, login, action, amount, name, account_number, routing_code.
517
518 =head1 DESCRIPTION
519
520 For detailed information see L<Business::OnlinePayment>.
521
522 =head1 METHODS AND FUNCTIONS
523
524 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
525
526 =head2 result_code
527
528 Returns the response error code.
529
530 =head2 error_message
531
532 Returns the response error description text.
533
534 =head2 server_response
535
536 Returns the complete response from the server.
537
538 =head1 Handling of content(%content) data:
539
540 =head2 action
541
542 The following actions are valid
543
544   normal authorization
545   authorization only
546   reverse authorization
547   post authorization
548   credit
549   void
550
551 =head1 Setting IPPay parameters from content(%content)
552
553 The following rules are applied to map data to IPPay parameters
554 from content(%content):
555
556       # param => $content{<key>}
557       TransactionType     => 'TransactionType',
558       TerminalID          => 'login',
559       TransactionID       => 'order_number',
560       RoutingCode         => 'RoutingCode',
561       Approval            => 'authorization',
562       BatchID             => 'BatchID',
563       Origin              => 'Origin',
564       Password            => 'password',
565       OrderNumber         => 'invoice_number',
566       CardNum             => 'card_number',
567       CVV2                => 'cvv2',
568       Issue               => 'issue_number',
569       CardExpMonth        => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
570       CardExpYear         => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
571       CardStartMonth      => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
572       CardStartYear       => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
573       Track1              => 'track1',
574       Track2              => 'track2',
575       ACH
576         AccountNumber       => 'account_number',
577         ABA                 => 'routing_code',
578         CheckNumber         => 'check_number',
579       CardName            => 'name',
580       DispositionType     => 'DispositionType',
581       TotalAmount         => 'amount' reformatted into cents
582       FeeAmount           => 'FeeAmount',
583       TaxAmount           => 'TaxAmount',
584       BillingAddress      => 'address',
585       BillingCity         => 'city',
586       BillingStateProv    => 'state',
587       BillingPostalCode   => 'zip',
588       BillingCountry      => 'country',           # forced to ISO-3166-alpha-3
589       BillingPhone        => 'phone',
590       Email               => 'email',
591       UserIPAddr          => 'customer_ip',
592       UserHost            => 'UserHost',
593       UDField1            => 'UDField1',
594       UDField2            => 'UDField2',
595       ActionCode          => 'ActionCode',
596       IndustryInfo
597         Type                => 'IndustryInfo',
598       ShippingInfo
599         CustomerPO          => 'CustomerPO',
600         ShippingMethod      => 'ShippingMethod',
601         ShippingName        => 'ship_name',
602         ShippingAddr
603           Address             => 'ship_address',
604           City                => 'ship_city',
605           StateProv           => 'ship_state',
606           Country             => 'ship_country',  # forced to ISO-3166-alpha-3
607           Phone               => 'ship_phone',
608
609 =head1 NOTE
610
611 =head1 COMPATIBILITY
612
613 Version 0.07 changes the server name and path for IPPay's late 2012 update.
614
615 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
616 1.1.2.
617
618 See http://www.ippay.com/ for more information.
619
620 =head1 AUTHORS
621
622 Original author: Jeff Finucane
623
624 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
625
626 Reverse Authorization patch from dougforpres
627
628 =head1 SEE ALSO
629
630 perl(1). L<Business::OnlinePayment>.
631
632 =cut
633