first crack at module
authorIvan Kohler <ivan@freeside.biz>
Thu, 6 Feb 2014 09:22:06 +0000 (01:22 -0800)
committerIvan Kohler <ivan@freeside.biz>
Thu, 6 Feb 2014 09:22:06 +0000 (01:22 -0800)
13 files changed:
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
ignore.txt [new file with mode: 0644]
lib/Business/OnlinePayment/IATSPayments.pm [new file with mode: 0644]
t/00-load.t [new file with mode: 0644]
t/boilerplate.t [new file with mode: 0644]
t/manifest.t [new file with mode: 0644]
t/pod-coverage.t [new file with mode: 0644]
t/pod.t [new file with mode: 0644]
t/transaction.t [new file with mode: 0644]
t/transaction_decline.t [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..75ba5da
--- /dev/null
+++ b/Changes
@@ -0,0 +1,6 @@
+
+Revision history for Perl extension Business::OnlinePayment::IATSPayments.
+
+0.01  unreleased
+        - original version.  the world might suspect something's afoot.
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..31c898a
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,13 @@
+Changes
+MANIFEST
+Makefile.PL
+README
+ignore.txt
+lib/Business/OnlinePayment/IATSPayments.pm
+t/00-load.t
+t/boilerplate.t
+t/manifest.t
+t/pod-coverage.t
+t/pod.t
+t/transaction.t
+t/transaction_decline.t
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..7ed1b7a
--- /dev/null
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+    NAME                => 'Business::OnlinePayment::IATSPayments',
+    AUTHOR              => q{Ivan Kohler <ivan-iatspayments@420.am>},
+    VERSION_FROM        => 'lib/Business/OnlinePayment/IATSPayments.pm',
+    ABSTRACT_FROM       => 'lib/Business/OnlinePayment/IATSPayments.pm',
+    ($ExtUtils::MakeMaker::VERSION >= 6.3002
+      ? ('LICENSE'=> 'perl')
+      : ()),
+    PL_FILES            => {},
+    PREREQ_PM => {
+        'Test::More' => 0,
+        'Business::OnlinePayment' => 3.01,
+        'SOAP::Lite' => 0,
+        'Data::Dumper' => 0,
+    },
+    dist                => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
+    clean               => { FILES => 'Business-OnlinePayment-IATSPayments-*' },
+);
+
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..68d731f
--- /dev/null
+++ b/README
@@ -0,0 +1,13 @@
+Copyright (c) 2014 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.
+
+This is Business::OnlinePayment::IATSPayments, a Business::OnlinePayment backend
+module for IATS Payments.  It is only useful if you have a merchant account with
+iATS Payments: http://home.iatspayments.com/
+
+Business::OnlinePayment is a generic interface for processing payments through
+online credit card processors, online check acceptance houses, etc.  (If you
+like buzzwords, call it an "multiplatform ecommerce-enabling middleware
+solution").
+
diff --git a/ignore.txt b/ignore.txt
new file mode 100644 (file)
index 0000000..6617fb5
--- /dev/null
@@ -0,0 +1,12 @@
+blib*
+Makefile
+Makefile.old
+Build
+Build.bat
+_build*
+pm_to_blib*
+*.tar.gz
+.lwpcookies
+cover_db
+pod2htm*.tmp
+Business-OnlinePayment-IATSPayments-*
diff --git a/lib/Business/OnlinePayment/IATSPayments.pm b/lib/Business/OnlinePayment/IATSPayments.pm
new file mode 100644 (file)
index 0000000..a3f7c96
--- /dev/null
@@ -0,0 +1,398 @@
+package Business::OnlinePayment::IATSPayments;
+use base qw( Business::OnlinePayment );
+
+use warnings;
+use strict;
+use Data::Dumper;
+use Business::CreditCard;
+use SOAP::Lite;
+#SOAP::Lite->import(+trace=>'debug');
+
+our $VERSION = '0.01';
+$VERSION = eval $VERSION; # modperlstyle: convert the string into a number
+
+sub _info {
+  {
+    'info_compat'       => '0.01',
+    'gateway_name'      => 'IATS Payments',
+    'gateway_url'       => 'http://home.iatspayments.com/',
+    'module_version'    => $VERSION,
+    'supported_types'   => [ 'CC', 'ECHECK' ],
+    #'token_support'     => 1,
+    'test_transaction'  => 1,
+
+    'supported_actions' => [ 'Normal Authorization',
+                             'Credit',
+                           ],
+  };
+}
+
+sub map_fields {
+    my($self) = @_;
+
+    my %content = $self->content();
+
+    # TYPE MAP
+    my %types = ( 'visa'               => 'CC',
+                  'mastercard'         => 'CC',
+                  'american express'   => 'CC',
+                  'discover'           => 'CC',
+                  'check'              => 'ECHECK',
+                );
+    $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
+    $self->transaction_type($content{'type'});
+    
+    # ACTION MAP 
+    my $action = lc($content{'action'});
+    my %actions =
+      ( 'normal authorization'  => 'ProcessCreditCardV1',
+        'credit'                => 'ProcessCreditCardRefundWithTransactionIdV1',
+      );
+    my %check_actions =
+      ( 'normal authorization'  => 'ProcessACHEFTV1',
+        'credit'                => 'ProcessACHEFTRefundWithTransactionIdV1',
+      );
+
+    if ($self->transaction_type eq 'CC') {
+      $content{'action'} = $actions{$action} || $action;
+    } elsif ($self->transaction_type eq 'ECHECK') {
+
+      $content{'action'} = $check_actions{$action} || $action;
+
+      # 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'};
+    }
+
+    # stuff it back into %content
+    $self->content(%content);
+
+}
+
+sub remap_fields {
+    my($self,%map) = @_;
+
+    my %content = $self->content();
+    foreach(keys %map) {
+        $content{$map{$_}} = $content{$_};
+    }
+    $self->content(%content);
+}
+
+# NA: VISA, MC, AMX, DSC
+# UK: VISA, MC, AMX, MAESTR
+our %mop = (
+  'VISA card'             => 'VISA',
+  'MasterCard'            => 'MC',
+  'Discover card'         => 'DSC',
+  'American Express card' => 'AMEX',
+  'Switch'                => 'MAESTR',
+  'Solo'                  => 'MAESTR',
+);
+
+#https://www.iatspayments.com/english/help/rejects.html
+our %reject = (
+  '1' => 'Agent code has not been set up on the authorization system. Please call iATS at 1-888-955-5455.',
+  '2' => 'Unable to process transaction. Verify and re-enter credit card information.',
+  '3' => 'Invalid Customer Code.',
+  '4' => 'Incorrect expiration date.',
+  '5' => 'Invalid transaction. Verify and re-enter credit card information.',
+  '6' => 'Please have cardholder call the number on the back of the card.',
+  '7' => 'Lost or stolen card.',
+  '8' => 'Invalid card status.',
+  '9' => 'Restricted card status. Usually on corporate cards restricted to specific sales.',
+  '10' => 'Error. Please verify and re-enter credit card information.',
+  '11' => 'General decline code. Please have client call the number on the back of credit card',
+  '12' => 'Incorrect CVV2 or Expiry date',
+  '14' => 'The card is over the limit.',
+  '15' => 'General decline code. Please have client call the number on the back of credit card',
+  '16' => 'Invalid charge card number. Verify and re-enter credit card information.',
+  '17' => 'Unable to authorize transaction. Authorizer needs more information for approval.',
+  '18' => 'Card not supported by institution.',
+  '19' => 'Incorrect CVV2 security code',
+  '22' => 'Bank timeout. Bank lines may be down or busy. Re-try transaction later.',
+  '23' => 'System error. Re-try transaction later.',
+  '24' => 'Charge card expired.',
+  '25' => 'Capture card. Reported lost or stolen.',
+  '26' => 'Invalid transaction, invalid expiry date. Please confirm and retry transaction.',
+  '27' => 'Please have cardholder call the number on the back of the card.',
+  '32' => 'Invalid charge card number.',
+  '39' => 'Contact IATS 1-888-955-5455.',
+  '40' => 'Invalid card number. Card not supported by IATS.',
+  '41' => 'Invalid Expiry date.',
+  '42' => 'CVV2 required.',
+  '43' => 'Incorrect AVS.',
+  '45' => 'Credit card name blocked. Call iATS at 1-888-955-5455.',
+  '46' => 'Card tumbling. Call iATS at 1-888-955-5455.',
+  '47' => 'Name tumbling. Call iATS at 1-888-955-5455.',
+  '48' => 'IP blocked. Call iATS at 1-888-955-5455.',
+  '49' => 'Velocity 1 – IP block. Call iATS at 1-888-955-5455.',
+  '50' => 'Velocity 2 – IP block. Call iATS at 1-888-955-5455.',
+  '51' => 'Velocity 3 – IP block. Call iATS at 1-888-955-5455.',
+  '52' => 'Credit card BIN country blocked. Call iATS at 1-888-955-5455.',
+  '100' => 'DO NOT REPROCESS. Call iATS at 1-888-955-5455.',
+  #Timeout     The system has not responded in the time allotted. Call iATS at 1-888-955-5455.
+);
+
+our %failure_status = (
+  '7'  => 'stolen',
+  '8'  => 'inactive',
+  '9'  => 'inactive',
+  '14' => 'nsf',
+  '24' => 'expired',
+  '25' => 'stolen',
+  '45' => 'blacklisted',
+  '48' => 'blacklisted',
+  '49' => 'blacklisted',
+  '50' => 'blacklisted',
+  '51' => 'blacklisted',
+  '52' => 'blacklisted',
+  #'100' => # it sounds serious.  but why?  it says nothing specific
+);
+
+sub submit {
+  my($self) = @_;
+
+  $self->map_fields;
+
+  $self->remap_fields(
+        login             => 'agentCode',
+        password          => 'password',
+
+        description       => 'comment',
+        amount            => 'total',
+        invoice_number    => 'invoiceNum',
+        customer_ip       => 'customerIPAddress',
+
+        last_name         => 'lastName',
+        first_name        => 'firstName',
+        address           => 'address',
+        city              => 'city',
+        state             => 'state',
+        zip               => 'zipCode',
+        #country           => 'x_Country',
+
+        card_number       => 'creditCardNum',
+        expiration        => 'creditCardExpiry',
+        cvv2              => 'cvv2',
+
+        authorization     => 'transactionId',
+
+        account_type      => 'accountType',
+
+  );
+
+  my %content = $self->content();
+
+  $content{'mop'} = $mop{ cardtype($content{creditCardNum}) }
+    if $content{'type'} eq 'CC';
+
+  if ( $self->test_transaction ) {
+    $content{agentCode} = 'TEST88';
+    $content{password}  = 'TEST88';
+  }
+
+  my $base_uri =
+    ( ! $content{currency} || $content{currency} =~ /^(USD|CAD)$/i )
+      ? 'https://www.iatspayments.com/NetGate/'
+      : 'https://www.uk.iatspayments.com/NetGate/';
+
+  my $action = $content{action};
+
+  my $uri = $base_uri. "ProcessLink.asmx?op=$action";
+
+  my %data = map { $_ => $content{$_} } (qw(
+    agentCode
+    password
+    comment
+    total
+    customerIPAddress
+  ));
+
+  if ( $action =~ /RefundWithTransacdtionIdV[\d\.]+$/ ) {
+
+    $data{ $_ } = $content{$_} for qw(
+      transactionId
+    );
+
+  } else {
+
+    $data{ $_ } = $content{$_} for qw(
+      invoiceNum
+      lastName
+      firstName
+      address
+      city
+      state
+      zipCode
+    );
+
+    if ( $content{'type'} eq 'CC' ) {
+
+      $data{$_} = $content{$_}
+        for qw( creditCardNum creditCardExpiry cvv2 mop );
+
+    } elsif ( $content{'type'} eq 'ECHECK' ) {
+
+      $data{'accountNum'}= $content{'routing_code'}. $content{'account_number'};
+
+      $data{$_} = $content{$_}
+        for qw( accountType );
+
+    }
+
+  }
+
+  my @opts = map { SOAP::Data->name($_)->value( $data{$_} ) }
+               keys %data;
+
+  my $result = SOAP::Lite
+                 ->proxy($uri)
+                 ->default_ns($base_uri)
+                 #->on_action( sub { join '/', @_ } )
+                 ->on_action( sub { join '', @_ } )
+                 ->autotype(0)
+
+                 ->$action( @opts )
+
+                 ->result();
+
+  my $iatsresponse = $result->{IATSRESPONSE};
+
+  if ( $iatsresponse->{STATUS} eq 'Failure' && $iatsresponse->{ERRORS} ) {
+    die 'iATS Payments error: '. $iatsresponse->{ERRORS}. "\n";
+  } elsif ( $iatsresponse->{STATUS} ne 'Success' ) {
+    die "Couldn't parse iATS Payments response: ". Dumper($result);
+  }
+
+  my $processresult = $iatsresponse->{PROCESSRESULT};
+
+  $self->authorization($processresult->{TRANSACTIONID} || '');
+
+  if ( $processresult->{AUTHORIZATIONRESULT} =~ /^\s*OK(:\s*\d+)?(:(\w))?\s*$/i ) {
+    $self->is_success(1);
+    $self->avs_code($3); #avs_code?  sure looks like one
+
+  } elsif ( $processresult->{AUTHORIZATIONRESULT} =~ /^\s*Timeout\s*$/i ) {
+    $self->is_success(0);
+    $self->error_message('The system has not responded in the time allotted. '.
+                         'Call iATS at 1-888-955-5455.');
+
+  } elsif ( $processresult->{AUTHORIZATIONRESULT}
+              =~ /^\s*REJ(ECT)?:\s*(\d+)\s*$/i
+          )
+  {
+    $self->is_success(0);
+    $self->error_message( $reject{$2} || $processresult->{AUTHORIZATIONRESULT});
+    $self->failure_status( $failure_status{$2} || 'decline' );
+
+  } else {
+    die "No/Unknown AUTHORIZATIONRESULT iATS Payments response: ".
+          Dumper($processresult);
+  }
+
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Business::OnlinePayment::IATSPayments - IATS Payments backend for Business::OnlinePayment
+
+=head1 SYNOPSIS
+
+  use Business::OnlinePayment;
+
+  my $tx =
+    new Business::OnlinePayment( 'IATSPayments' );
+
+  $tx->content(
+      login          => 'TEST88', # agentCode
+      password       => 'TEST88', #password 
+
+      type           => 'CC',
+      action         => 'Normal Authorization',
+      amount         => '1.00',
+
+      first_name     => 'Tofu',
+      last_name      => 'Beast',
+      address        => '123 Anystreet',
+      city           => 'Anywhere',
+      state          => 'UT',
+      zip            => '84058',
+
+      card_number    => '4111111111111111',
+      expiration     => '09/20',
+      cvv2           => '124',
+
+      #optional
+      description    => 'Business::OnlinePayment test',
+      customer_ip    => '1.2.3.4',
+      invoice_num    => 54,
+  );
+  $tx->submit();
+
+  if($tx->is_success()) {
+      print "Card processed successfully: ".$tx->authorization."\n";
+  } else {
+      print "Card was rejected: ".$tx->error_message."\n";
+  }
+
+=head1 SUPPORTED TRANSACTION TYPES
+
+=head2 CC, Visa, MasterCard, American Express, Discover
+
+Content required: type, login, action, amount, card_number, expiration.
+
+=head2 Check
+
+Content required: type, login, action, amount, name, account_number, routing_code.
+
+=head1 DESCRIPTION
+
+For detailed information see L<Business::OnlinePayment>.
+
+=head1 METHODS AND FUNCTIONS
+
+See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
+
+=head2 result_code
+
+Returns the response error code.
+
+=head2 error_message
+
+Returns the response error number.
+
+=head2 action
+
+The following actions are valid
+
+  Normal Authorization
+  Credit
+
+=head1 COMPATIBILITY
+
+Business::OnlinePayment::IATSPayments uses iATS WebServices ProcessLink 4.0
+and (for tokenization support) iATS WebServices CustomerLink 4.0.
+
+=head1 AUTHORS
+
+Ivan Kohler <ivan-iatspayments@freeside.biz>
+
+=head1 SEE ALSO
+
+perl(1). L<Business::OnlinePayment>.
+
+=cut
+
diff --git a/t/00-load.t b/t/00-load.t
new file mode 100644 (file)
index 0000000..dbce839
--- /dev/null
@@ -0,0 +1,10 @@
+#!perl -T
+
+use Test::More tests => 1;
+
+BEGIN {
+    use_ok( 'Business::OnlinePayment::IATSPayments' ) || print "Bail out!
+";
+}
+
+diag( "Testing Business::OnlinePayment::IATSPayments $Business::OnlinePayment::IATSPayments::VERSION, Perl $], $^X" );
diff --git a/t/boilerplate.t b/t/boilerplate.t
new file mode 100644 (file)
index 0000000..41d654b
--- /dev/null
@@ -0,0 +1,49 @@
+#!perl -T
+
+use strict;
+use warnings;
+use Test::More tests => 3;
+
+sub not_in_file_ok {
+    my ($filename, %regex) = @_;
+    open( my $fh, '<', $filename )
+        or die "couldn't open $filename for reading: $!";
+
+    my %violated;
+
+    while (my $line = <$fh>) {
+        while (my ($desc, $regex) = each %regex) {
+            if ($line =~ $regex) {
+                push @{$violated{$desc}||=[]}, $.;
+            }
+        }
+    }
+
+    if (%violated) {
+        fail("$filename contains boilerplate text");
+        diag "$_ appears on lines @{$violated{$_}}" for keys %violated;
+    } else {
+        pass("$filename contains no boilerplate text");
+    }
+}
+
+sub module_boilerplate_ok {
+    my ($module) = @_;
+    not_in_file_ok($module =>
+        'the great new $MODULENAME'   => qr/ - The great new /,
+        'boilerplate description'     => qr/Quick summary of what the module/,
+        'stub function definition'    => qr/function[12]/,
+    );
+}
+
+  not_in_file_ok(README =>
+    "The README is used..."       => qr/The README is used/,
+    "'version information here'"  => qr/to provide version information/,
+  );
+
+  not_in_file_ok(Changes =>
+    "placeholder date/time"       => qr(Date/time)
+  );
+
+  module_boilerplate_ok('lib/Business/OnlinePayment/IATSPayments.pm');
+
diff --git a/t/manifest.t b/t/manifest.t
new file mode 100644 (file)
index 0000000..45eb83f
--- /dev/null
@@ -0,0 +1,13 @@
+#!perl -T
+
+use strict;
+use warnings;
+use Test::More;
+
+unless ( $ENV{RELEASE_TESTING} ) {
+    plan( skip_all => "Author tests not required for installation" );
+}
+
+eval "use Test::CheckManifest 0.9";
+plan skip_all => "Test::CheckManifest 0.9 required" if $@;
+ok_manifest();
diff --git a/t/pod-coverage.t b/t/pod-coverage.t
new file mode 100644 (file)
index 0000000..c021dd4
--- /dev/null
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+use Test::More skip_all => "don't care about POD coverage right now";
+
+# Ensure a recent version of Test::Pod::Coverage
+my $min_tpc = 1.08;
+eval "use Test::Pod::Coverage $min_tpc";
+plan skip_all => "Test::Pod::Coverage $min_tpc required for testing POD coverage"
+    if $@;
+
+# Test::Pod::Coverage doesn't require a minimum Pod::Coverage version,
+# but older versions don't recognize some common documentation styles
+my $min_pc = 0.18;
+eval "use Pod::Coverage $min_pc";
+plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage"
+    if $@;
+
+all_pod_coverage_ok();
diff --git a/t/pod.t b/t/pod.t
new file mode 100644 (file)
index 0000000..ee8b18a
--- /dev/null
+++ b/t/pod.t
@@ -0,0 +1,12 @@
+#!perl -T
+
+use strict;
+use warnings;
+use Test::More;
+
+# Ensure a recent version of Test::Pod
+my $min_tp = 1.22;
+eval "use Test::Pod $min_tp";
+plan skip_all => "Test::Pod $min_tp required for testing POD" if $@;
+
+all_pod_files_ok();
diff --git a/t/transaction.t b/t/transaction.t
new file mode 100644 (file)
index 0000000..09de282
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use POSIX qw(strftime);
+use Test::More;
+
+use Business::OnlinePayment;
+
+my %content = (                                                                 
+    action         => "Normal Authorization",                                   
+    type           => "CC",                                                     
+    description    => "Business::OnlinePayment::IATSPayments test",     
+    card_number    => '4111111111111111',
+    cvv2           => '123',
+    expiration     => '12/20',
+    amount         => '2.00',
+    first_name     => 'Tofu',
+    last_name      => 'Beast',
+    address        => '1234 Soybean Ln.',
+    city           => 'Soyville',
+    state          => 'CA', #where else?
+    zip            => '54545',
+);                                                                              
+
+my $tx = new Business::OnlinePayment( 'IATSPayments' );
+
+$tx->content( %content );
+
+$tx->test_transaction(1);
+
+$tx->submit;
+
+is( $tx->is_success, 1, 'Test transaction successful')
+  or diag('iATS Payments error: '. $tx->error_message);
+            
+1;
diff --git a/t/transaction_decline.t b/t/transaction_decline.t
new file mode 100644 (file)
index 0000000..c62e8dd
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use POSIX qw(strftime);
+use Test::More;
+
+use Business::OnlinePayment;
+
+my %content = (                                                                 
+    action         => "Normal Authorization",                                   
+    type           => "CC",                                                     
+    description    => "Business::OnlinePayment::IATSPayments test",     
+    card_number    => '4111111111111111',
+    cvv2           => '123',
+    expiration     => '12/20',
+    amount         => '1.00',
+    first_name     => 'Tofu',
+    last_name      => 'Beast',
+    address        => '1234 Soybean Ln.',
+    city           => 'Soyville',
+    state          => 'CA', #where else?
+    zip            => '54545',
+
+    customer_ip => '1.2.3.4',
+    invoice_num => 64,
+);                                                                              
+
+my $tx = new Business::OnlinePayment( 'IATSPayments' );
+
+$tx->content( %content );
+
+$tx->test_transaction(1);
+
+$tx->submit;
+
+is( $tx->is_success, 0, 'Test decline transaction successful');
+            
+1;