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