s/JetPay/ippay/ per new spec
[Business-OnlinePayment-IPPay.git] / IPPay.pm
index 65815f0..4d9dbd5 100644 (file)
--- a/IPPay.pm
+++ b/IPPay.pm
@@ -5,34 +5,58 @@ use Carp;
 use Tie::IxHash;
 use XML::Simple;
 use XML::Writer;
+use Locale::Country;
 use Business::OnlinePayment;
 use Business::OnlinePayment::HTTPS;
 use vars qw($VERSION $DEBUG @ISA $me);
 
 @ISA = qw(Business::OnlinePayment::HTTPS);
-$VERSION = '0.02';
-$DEBUG = 1;
+$VERSION = '0.11_01';
+$VERSION = eval $VERSION; # modperlstyle: convert the string into a number
+
+$DEBUG = 0;
 $me = 'Business::OnlinePayment::IPPay';
 
+sub _info {
+  {
+    'info_compat'           => '0.01',
+    'module_version'        => $VERSION,
+    'supported_types'       => [ qw( CC ECHECK ) ],
+    'supported_actions'     => { 'CC' => [
+                                     'Normal Authorization',
+                                     'Authorization Only',
+                                     'Post Authorization',
+                                     'Void',
+                                     'Credit',
+                                     'Reverse Authorization',
+                                   ],
+                                   'ECHECK' => [
+                                     'Normal Authorization',
+                                     'Void',
+                                     'Credit',
+                                   ],
+                                 },
+    'CC_void_requires_card' => 1,
+    'ECHECK_void_requires_account' => 1,
+  };
+}
+
 sub set_defaults {
     my $self = shift;
     my %opts = @_;
 
     # standard B::OP methods/data
-    $self->server('gateway17.jetpay.com') unless $self->server;
+    $self->server('gtwy.ippay.com') unless $self->server;
     $self->port('443') unless $self->port;
-    $self->path('/jetpay') unless $self->path;
+    $self->path('/ippay') unless $self->path;
 
     $self->build_subs(qw( order_number avs_code cvv2_response
                           response_page response_code response_headers
                      ));
 
-    # module specific data
-    if ( $opts{debug} ) {
-        $self->debug( $opts{debug} );
-        delete $opts{debug};
-    }
+    $DEBUG = exists($opts{debug}) ? $opts{debug} : 0;
 
+    # module specific data
     my %_defaults = ();
     foreach my $key (keys %opts) {
       $key =~ /^default_(\w*)$/ or next;
@@ -63,6 +87,7 @@ sub map_fields {
       ( 'normal authorization'            => 'SALE',
         'authorization only'              => 'AUTHONLY',
         'post authorization'              => 'CAPT',
+        'reverse authorization'           => 'REVERSEAUTH',
         'void'                            => 'VOID',
         'credit'                          => 'CREDIT',
       );
@@ -71,20 +96,25 @@ sub map_fields {
         'void'                            => 'VOIDACH',
         'credit'                          => 'REVERSAL',
       );
+
     if ($self->transaction_type eq 'CC') {
       $content{'TransactionType'} = $actions{$action} || $action;
-    }elsif ($self->transaction_type eq 'ECHECK') {
-      $content{'TransactionType'} = $check_actions{$action} || $action;
-    }
+    } elsif ($self->transaction_type eq 'ECHECK') {
 
+      $content{'TransactionType'} = $check_actions{$action} || $action;
 
-    # ACCOUNT TYPE MAP
-    my %account_types = ('personal checking'   => 'Checking',
-                         'personal savings'    => 'Savings',
-                         'business checking'   => 'BusinessCk',
-                        );
-    $content{'account_type'} = $account_types{lc($content{'account_type'})}
-                               || $content{'account_type'};
+      # ACCOUNT TYPE MAP
+      my %account_types = ('personal checking'   => 'CHECKING',
+                           'personal savings'    => 'SAVINGS',
+                           'business checking'   => 'CHECKING',
+                           'business savings'    => 'SAVINGS',
+                           #not technically B:OP valid i guess?
+                           'checking'            => 'CHECKING',
+                           'savings'             => 'SAVINGS',
+                          );
+      $content{'account_type'} = $account_types{lc($content{'account_type'})}
+                                 || $content{'account_type'};
+    }
 
     $content{Origin} = 'RECURRING' 
       if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
@@ -144,7 +174,7 @@ sub submit {
   $self->is_success(0);
   $self->map_fields();
 
-  my @required_fields = qw(action login type);
+  my @required_fields = qw(action login password type);
 
   my $action = lc($self->{_content}->{action});
   my $type = $self->transaction_type();
@@ -163,6 +193,8 @@ sub submit {
         
   }elsif ( $action eq 'post authorization' && $type eq 'CC') {
     push @required_fields, qw( order_number );
+  }elsif ( $action eq 'reverse authorization' && $type eq 'CC') {
+    push @required_fields, qw( order_number card_number expiration amount );
   }elsif ( $action eq 'void') {
     push @required_fields, qw( order_number amount );
 
@@ -183,14 +215,20 @@ sub submit {
   foreach ( keys ( %{($self->{_defaults})} ) ) {
     $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
   }
+  if ($self->test_transaction()) {
+    $content{'login'} = 'TESTTERMINAL';
+    $self->server('testgtwy.ippay.com') if $self->server eq 'gtwy.ippay.com';
+  }
   $self->content(%content);
 
   $self->required_fields(@required_fields);
 
-  if ($self->test_transaction()) {
-    $self->server('test1.jetpay.com');
-    $self->port('443');
-    $self->path('/jetpay');
+  #quick validation because ippay dumps an error indecipherable to the end user
+  if (grep { /^routing_code$/ } @required_fields) {
+    unless( $content{routing_code} =~ /^\d{9}$/ ) {
+      $self->_error_response('Invalid routing code');
+      return;
+    }
   }
 
   my $transaction_id = $content{'order_number'};
@@ -200,7 +238,7 @@ sub submit {
          "(HTTPS headers: ".
          join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
          "(Raw HTTPS content: $page)"
-      if $DEBUG;
+      if $DEBUG > 1;
     return unless $server_response=~ /^200/;
     $transaction_id = $page;
   }
@@ -222,8 +260,31 @@ sub submit {
   my $terminalid = $content{login} if $type eq 'CC';
   my $merchantid = $content{login} if $type eq 'ECHECK';
 
+  my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
+  $country  = country_code2code( $content{country},
+                                 LOCALE_CODE_ALPHA_2,
+                                 LOCALE_CODE_ALPHA_3
+                               )
+    unless $country;
+  $country = $content{country}
+    unless $country;
+  $country = uc($country) if $country;
+
+  my $ship_country =
+    country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
+  $ship_country  = country_code2code( $content{ship_country},
+                                 LOCALE_CODE_ALPHA_2,
+                                 LOCALE_CODE_ALPHA_3
+                               )
+    unless $ship_country;
+  $ship_country = $content{ship_country}
+    unless $ship_country;
+  $ship_country = uc($ship_country) if $ship_country;
+
   tie my %ach, 'Tie::IxHash',
     $self->revmap_fields(
+                          #wtf, this is a "Type"" attribute of the ACH element,
+                          # not a child element like the others
                           #AccountType         => 'account_type',
                           AccountNumber       => 'account_number',
                           ABA                 => 'routing_code',
@@ -240,7 +301,7 @@ sub submit {
                           Address             => 'ship_address',
                           City                => 'ship_city',
                           StateProv           => 'ship_state',
-                          Country             => 'ship_country',
+                          Country             => \$ship_country,
                           Phone               => 'ship_phone',
                         );
 
@@ -250,10 +311,11 @@ sub submit {
                             Address             => 'address',
                             City                => 'city',
                             StateProv           => 'state',
-                            Country             => 'country',
+                            Country             => \$country,
                             Phone               => 'phone',
                           );
   }
+  delete $shippingaddr{Country} unless $shippingaddr{Country};
 
   tie my %shippinginfo, 'Tie::IxHash',
     $self->revmap_fields(
@@ -295,18 +357,19 @@ sub submit {
                           BillingCity         => 'city',
                           BillingStateProv    => 'state',
                           BillingPostalCode   => 'zip',
-                          BillingCountry      => 'country',
+                          BillingCountry      => \$country,
                           BillingPhone        => 'phone',
                           Email               => 'email',
-                          UserIPAddr          => 'customer_ip',
+                          UserIPAddress       => 'customer_ip',
                           UserHost            => 'UserHost',
                           UDField1            => 'UDField1',
                           UDField2            => 'UDField2',
-                          UDField3            => 'UDField3',
+                          UDField3            => \"$me $VERSION", #'UDField3',
                           ActionCode          => 'ActionCode',
                           IndustryInfo        => \%industryinfo,
                           ShippingInfo        => \%shippinginfo,
                         );
+  delete $req{BillingCountry} unless $req{BillingCountry};
 
   my $post_data;
   my $writer = new XML::Writer( OUTPUT      => \$post_data,
@@ -315,18 +378,18 @@ sub submit {
                                 ENCODING    => 'us-ascii',
                               );
   $writer->xmlDecl();
-  $writer->startTag('JetPay');
+  $writer->startTag('ippay');
   foreach ( keys ( %req ) ) {
     $self->_xmlwrite($writer, $_, $req{$_});
   }
-  $writer->endTag('JetPay');
+  $writer->endTag('ippay');
   $writer->end();
 
-  warn "$post_data\n" if $DEBUG;
+  warn "$post_data\n" if $DEBUG > 1;
 
   my ($page,$server_response,%headers) = $self->https_post($post_data);
 
-  warn "$page\n" if $DEBUG;
+  warn "$page\n" if $DEBUG > 1;
 
   my $response = {};
   if ($server_response =~ /^200/){
@@ -334,7 +397,7 @@ sub submit {
     if (  exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
       $self->error_message($response->{ResponseText});
     }else{
-      $self->error_message($response->{Errmsg});
+      $self->error_message($response->{ErrMsg});
     }
 #  }else{
 #    $self->error_message("Server Failed");
@@ -349,21 +412,49 @@ sub submit {
   $self->is_success($self->result_code() eq '000' ? 1 : 0);
 
   unless ($self->is_success()) {
-    unless ( $self->error_message() ) { #additional logging information
-      $self->error_message(
-        "(HTTPS response: $server_response) ".
-        "(HTTPS headers: ".
-          join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
-        "(Raw HTTPS content: $page)"
-      );
+    unless ( $self->error_message() ) {
+      if ( $DEBUG ) {
+        #additional logging information, possibly too sensitive for an error msg
+        # (IPPay seems to have a failure mode where they return the full
+        #  original request including card number)
+        $self->error_message(
+          "(HTTPS response: $server_response) ".
+          "(HTTPS headers: ".
+            join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
+          "(Raw HTTPS content: $page)"
+        );
+      } else {
+        $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)');
+      }
     }
   }
 
 }
 
+sub _error_response {
+  my ($self, $error_message) = (shift, shift);
+  $self->result_code('');
+  $self->order_number('');
+  $self->authorization('');
+  $self->cvv2_response('');
+  $self->avs_code('');
+  $self->is_success( 0);
+  $self->error_message($error_message);
+}
+
 sub _xmlwrite {
   my ($self, $writer, $item, $value) = @_;
-  $writer->startTag($item);
+
+  my %att = ();
+  if ( $item eq 'ACH' ) {
+    $att{'Type'} = $self->{_content}->{'account_type'}
+      if $self->{_content}->{'account_type'}; #necessary so we don't pass empty?
+    $att{'SEC'}  = $self->{_content}->{'nacha_sec_code'}
+                 || ( $att{'Type'} =~ /business/i ? 'CCD' : 'PPD' );
+  }
+
+  $writer->startTag($item, %att);
+
   if ( ref( $value ) eq 'HASH' ) {
     foreach ( keys ( %$value ) ) {
       $self->_xmlwrite($writer, $_, $value->{$_});
@@ -371,10 +462,12 @@ sub _xmlwrite {
   }else{
     $writer->characters($value);
   }
+
   $writer->endTag($item);
 }
 
 1;
+
 __END__
 
 =head1 NAME
@@ -452,6 +545,7 @@ The following actions are valid
 
   normal authorization
   authorization only
+  reverse authorization
   post authorization
   credit
   void
@@ -493,14 +587,13 @@ from content(%content):
       BillingCity         => 'city',
       BillingStateProv    => 'state',
       BillingPostalCode   => 'zip',
-      BillingCountry      => 'country',
+      BillingCountry      => 'country',           # forced to ISO-3166-alpha-3
       BillingPhone        => 'phone',
       Email               => 'email',
-      UserIPAddr          => 'customer_ip',
+      UserIPAddress        => 'customer_ip',
       UserHost            => 'UserHost',
       UDField1            => 'UDField1',
       UDField2            => 'UDField2',
-      UDField3            => 'UDField3',
       ActionCode          => 'ActionCode',
       IndustryInfo
         Type                => 'IndustryInfo',
@@ -512,21 +605,45 @@ from content(%content):
           Address             => 'ship_address',
           City                => 'ship_city',
           StateProv           => 'ship_state',
-          Country             => 'ship_country',
+          Country             => 'ship_country',  # forced to ISO-3166-alpha-3
           Phone               => 'ship_phone',
 
 =head1 NOTE
 
 =head1 COMPATIBILITY
 
+Version 0.07 changes the server name and path for IPPay's late 2012 update.
+
 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
 1.1.2.
 
 See http://www.ippay.com/ for more information.
 
-=head1 AUTHOR
+=head1 AUTHORS
+
+Original author: Jeff Finucane
+
+Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
+
+Reverse Authorization patch from dougforpres
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 1999 Jason Kohles
+Copyright (c) 2002-2003 Ivan Kohler
+Copyright (c) 2008-2021 Freeside Internet Services, Inc.
+
+All rights reserved. This program is free software; you can redistribute it
+and/or modify it under the same terms as Perl itself.
+
+=head1 ADVERTISEMENT
+
+Need a complete, open-source back-office and customer self-service solution?
+The Freeside software includes support for credit card and electronic check
+processing with IPPay and over 50 other gateways, invoicing, integrated
+trouble ticketing, and customer signup and self-service web interfaces.
 
-Jeff Finucane, ippay@weasellips.com
+http://freeside.biz/freeside/
 
 =head1 SEE ALSO