Initial import start
authorjeff <jeff>
Fri, 11 May 2007 18:06:55 +0000 (18:06 +0000)
committerjeff <jeff>
Fri, 11 May 2007 18:06:55 +0000 (18:06 +0000)
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]
TransFirsteLink.pm [new file with mode: 0644]
t/00load.t [new file with mode: 0644]
t/bop.t [new file with mode: 0644]
t/credit_card.t [new file with mode: 0644]
t/echeck.t [new file with mode: 0644]
t/pod.t [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..9c28ff8
--- /dev/null
+++ b/Changes
@@ -0,0 +1,4 @@
+Revision history for Perl extension Business::OnlinePayment::TransFirsteLink.
+
+0.01  Wed Apr 29 07:55:25 EDT 2007
+       - original version; created by jeff
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..9c5fb47
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,11 @@
+TransFirsteLink.pm
+Changes
+MANIFEST
+Makefile.PL
+README
+t/00load.t
+t/bop.t
+t/credit_card.t
+t/echeck.t
+t/pod.t
+META.yml                                 Module meta-data (added by MakeMaker)
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..0469740
--- /dev/null
@@ -0,0 +1,21 @@
+use ExtUtils::MakeMaker;
+
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    NAME         => 'Business::OnlinePayment::TransFirsteLink',
+    VERSION_FROM => 'TransFirsteLink.pm',
+    PREREQ_PM    => {
+        Business::OnlinePayment        => '3',
+        Business::OnlinePayment::HTTPS => '0.05',
+    },
+    (
+        $] >= 5.005
+        ?    ## Add these new keywords supported since 5.005
+          (
+            ABSTRACT_FROM => 'TransFirsteLink.pm',
+            AUTHOR        => 'Jeff Finucane <transfirst@weasellips.com>',
+          )
+        : ()
+    ),
+);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..c5962ba
--- /dev/null
+++ b/README
@@ -0,0 +1,11 @@
+Copyright (c) 2007 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::TransFirsteLink, a Business::OnlinePayment
+back end module for Transfirst eLink.  It is only useful if you have
+a merchant account with TransFirst Payment Services: http://www.transfirst.com/
+
+Based on Business::OnlinePayment::AuthorizeNet written by Jason Kohles.
+Currently maintained by Jeff Finucane <transfirst@weasellips.com>.
+
diff --git a/TransFirsteLink.pm b/TransFirsteLink.pm
new file mode 100644 (file)
index 0000000..c5dcfe6
--- /dev/null
@@ -0,0 +1,778 @@
+package Business::OnlinePayment::TransFirsteLink;
+
+use strict;
+use vars qw($VERSION $DEBUG %error_messages);
+use Carp qw(carp croak);
+
+use base qw(Business::OnlinePayment::HTTPS);
+
+$VERSION = '0.01';
+$VERSION = eval $VERSION;
+$DEBUG   = 0;
+
+%error_messages = (
+  '000' => 'Approval',
+  '001' => 'Call Issuer',
+  '002' => 'Referral special',
+  '003' => 'Invalid merchant number',
+  '004' => 'Pick up',
+  '005' => 'Declined',
+  '006' => 'General error',
+  '007' => 'Pick up special',
+  '008' => 'Honor with ID',
+  '009' => 'General Decline',
+  '010' => 'Network Error',
+  '011' => 'Approval',
+  '012' => 'Invalid transaction type',
+  '013' => 'Invalid amount field',
+  '014' => 'Invalid card number',
+  '015' => 'Invalid issuer',
+  '016' => 'General Decline',
+  '017' => 'General Decline',
+  '018' => 'General Decline',
+  '019' => 'Re-enter',
+  '020' => 'General Decline',
+  '021' => 'No action taken',
+  '022' => 'General Decline',
+  '023' => 'General Decline',
+  '024' => 'General Decline',
+  '025' => 'Acct num miss',
+  '026' => 'General Decline',
+  '027' => 'General Decline',
+  '028' => 'File unavailable',
+  '029' => 'General Decline',
+  '030' => 'Format Error - Decline',
+  '031' => 'General Decline',
+  '032' => 'General Decline',
+  '033' => 'General Decline',
+  '034' => 'General Decline',
+  '036' => 'General Decline',
+  '037' => 'General Decline',
+  '038' => 'General Decline',
+  '039' => 'No card acct',
+  '040' => 'General Decline',
+  '041' => 'Lost card',
+  '042' => 'General Decline',
+  '043' => 'Stolen card',
+  '044' => 'General Decline',
+  '045' => 'General Decline',
+  '046' => 'General Decline',
+  '048' => 'General Decline',
+  '049' => 'General Decline',
+  '050' => 'General Decline',
+  '051' => 'Over limit',
+  '052' => 'No checking acct',
+  '053' => 'No saving acct',
+  '054' => 'Expired card',
+  '055' => 'Invalid pin',
+  '056' => 'General Decline',
+  '057' => 'TXN not allowed',
+  '058' => 'TXN not allowed term',
+  '059' => 'TXN not allowed - Merchant',
+  '060' => 'General Decline',
+  '061' => 'Over cash limit',
+  '062' => 'Restricted card',
+  '063' => 'Security violate',
+  '064' => 'General Decline',
+  '065' => 'Excessive authorizations',
+  '066' => 'General Decline',
+  '067' => 'General Decline',
+  '069' => 'General Decline',
+  '070' => 'General Decline',
+  '071' => 'General Decline',
+  '072' => 'General Decline',
+  '073' => 'General Decline',
+  '074' => 'General Decline',
+  '075' => 'Excessive pin entry tries',
+  '076' => 'Unable locate previous msg (ref# not found)',
+  '077' => 'Mismatched info',
+  '078' => 'No account',
+  '079' => 'Already reversed',
+  '080' => 'Invalid date',
+  '081' => 'Crypto error',
+  '082' => 'CVV failure',
+  '083' => 'Unable verify pin',
+  '084' => 'Duplicate trans',
+  '085' => 'No reason 2 decline',
+  '086' => 'Cannot verify pin',
+  '088' => 'General Decline',
+  '089' => 'General Decline',
+  '090' => 'General Decline',
+  '091' => 'Issuer unavailable',
+  '092' => 'Destination route not found',
+  '093' => 'Law violation',
+  '094' => 'Duplicate trans',
+  '096' => 'System malfunction',
+  '098' => 'General Decline',
+  '099' => 'General Decline',
+  '0B1' => 'Surcharge amount not permitted on Visa cards or EBT food stamps',
+  '0B2' => 'Surcharge amount not supported by debit network issuer',
+  '0EB' => 'Check digit error',
+  '0EC' => 'Cid format error',
+  '0N0' => 'FORCE STIP',
+  '0N3' => 'Service not available',
+  '0N4' => 'Exceeds limit issuer',
+  '0N5' => 'Ineligible for resubmission',
+  '0N7' => 'CVV2 failure',
+  '0N8' => 'Trans amount exceeds preauth amt',
+  '0P0' => 'Approved pvid miss',
+  '0P1' => 'Declined pvid miss',
+  '0P2' => 'Invalid bill info',
+  '0Q1' => 'Card auth failed',
+  '0R0' => 'Multipay stopped',
+  '0R1' => 'Multipay stopped merch',
+  '0R3' => 'Revocation of all authorizations order',
+  '0XA' => 'Forward to issue1',
+  '0XD' => 'Forward to issue2',
+  '0VD' => 'General Decline',
+  '0T0' => 'First Time Check',
+  '0T1' => 'Check is OK, but cannot be converted',
+  '0T2' => 'Invalid routing transit number or check belongs to a category that is not eligible for conversion',
+  '0T3' => 'Amount greater than established service limit',
+  '0T4' => 'Unpaid items, failed negative check',
+  '0T5' => 'Duplicate check number',
+  '0T6' => 'MICR Error',
+  '0T7' => 'Too many checks (over merchant or bank limit)',
+  '203' => 'Invalid merchant number',
+  '212' => 'Invalid transaction type',
+  '213' => 'Invalid amount field',
+  '214' => 'Invalid card number',
+  '254' => 'Expired card',
+  '257' => 'Txn not allowed',
+  '276' => 'Unable to locate prvious msg (ref # not found)',
+  '278' => 'No account',
+  '284' => 'General Decline',
+  '296' => 'System malfunction',
+  '2Q1' => 'Card authorization failed',
+  '300' => 'Invalid request format',
+  '301' => 'Missing file header',
+  '303' => 'Invalid sender ID',
+  '306' => 'Duplicate file number',
+  '307' => 'General Decline',
+  '309' => 'Comm link down',
+  '310' => 'Missing batch header',
+  '317' => 'Invalid MOTO ID',
+  '338' => 'General Decline',
+  '380' => 'Missing batch trailer',
+  '382' => 'Record count does not match number records in batch',
+  '383' => 'Net amount does not match file amount',
+  '384' => 'Duplicate transaction',
+  '385' => 'Invalid request format',
+  '394' => 'Record count does not match records in file',
+  '395' => 'Net amount does not match file amount',
+  '396' => 'Declined post - reauthorization attempt',
+  '318' => 'Invalid account data source',
+  '319' => 'Invalid POS entry mode',
+  '320' => 'Auth date invalid (transaction date)',
+  '321' => 'Invalid auth source code',
+  '322' => 'Invalid ACI code',
+  'REJ' => 'Rejected transaction that has been re-keyed',
+  '3AC' => 'Invalid authorization code (must be uppercase, no special chars)',
+  '3TI' => 'Invalid tax indicator',
+  '3VD' => 'Voided transaction',
+  '3AD' => 'AVS response code declined',
+  '3AR' => 'AVS required/address information not provided',
+  '3BD' => 'AVS and CVV2 response Code Declined',
+  '3BR' => 'AVS and CVV2 required/information not provided',
+  '3CD' => 'CVV2 response code declined',
+  '3CR' => 'CVV2 required/inrormation not provided',
+  '3L5' => 'No data sent',
+  '3L6' => 'Order number missing',
+  '3M1' => 'Auth date blank',
+  '3M2' => 'Auth amount blank',
+  '3MT' => 'Managed transaction',
+  '3RV' => 'Reversed transaction',
+  '3TO' => 'Timeout',
+  '600' => 'General Decline',
+  '990' => 'Voided',
+  '991' => 'Voided',
+  '992' => 'Voided',
+  '993' => 'Voided',
+  '994' => 'Voided',
+  '995' => 'Voided',
+  '996' => 'Voided',
+  '997' => 'Voided',
+  '998' => 'Voided',
+  '999' => 'Voided',
+  'XXX' => 'General Decline',
+);
+
+sub debug {
+    my $self = shift;
+
+    if (@_) {
+        my $level = shift || 0;
+        if ( ref($self) ) {
+            $self->{"__DEBUG"} = $level;
+        }
+        else {
+            $DEBUG = $level;
+        }
+        $Business::OnlinePayment::HTTPS::DEBUG = $level;
+    }
+    return ref($self) ? ( $self->{"__DEBUG"} || $DEBUG ) : $DEBUG;
+}
+
+sub set_defaults {
+    my $self = shift;
+    my %opts = @_;
+
+    # standard B::OP methods/data
+    $self->server("epaysecure1.transfirst.com");
+    $self->port("443");
+    $self->path("/");
+
+    $self->build_subs(qw( 
+                          merchantcustservnum
+                          order_number avs_code cvv2_response
+                          response_page response_code response_headers
+                          junk
+                     ));
+
+    # module specific data
+    if ( $opts{debug} ) {
+        $self->debug( $opts{debug} );
+        delete $opts{debug};
+    }
+
+    if ( $opts{merchantcustservnum} ) {
+        $self->merchantcustservnum( $opts{merchantcustservnum} );
+        delete $opts{merchantcustservnum};
+    }
+
+}
+
+sub _map_fields {
+    my ($self) = @_;
+
+    my %content = $self->content();
+
+    #ACTION MAP
+    my %actions = (
+        'normal authorization' => 32,    # Authorization/Settle transaction
+        'credit'               => 20,    # Credit (refund)
+        'authorization only'   => 30,    # Authorization only
+        'post authorization'   => 40,    # Settlement
+        'void'                 => 61,    # Void
+    );
+
+    $content{'TransactionCode'} = $actions{ lc( $content{'action'} ) }
+      || $content{'action'};
+
+    # TYPE MAP
+    my %types = (
+        'visa'             => 'CC',
+        'mastercard'       => 'CC',
+        'american express' => 'CC',
+        'discover'         => 'CC',
+        'cc'               => 'CC',
+
+        'check'            => 'ECHECK',
+    );
+
+    $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'};
+
+    $self->transaction_type( $content{'type'} );
+
+    # stuff it back into %content
+    $self->content(%content);
+}
+
+sub _revmap_fields {
+    my ( $self, %map ) = @_;
+    my %content = $self->content();
+    foreach ( keys %map ) {
+        $content{$_} =
+          ref( $map{$_} )
+          ? ${ $map{$_} }
+          : $content{ $map{$_} };
+    }
+    $self->content(%content);
+}
+
+sub expdate_mmyy {
+    my $self       = shift;
+    my $expiration = shift;
+    my $expdate_mmyy;
+    if ( defined($expiration) and $expiration =~ /^(\d+)\D+\d*(\d{2})$/ ) {
+        my ( $month, $year ) = ( $1, $2 );
+        $expdate_mmyy = sprintf( "%02d", $month ) . $year;
+    }
+    return defined($expdate_mmyy) ? $expdate_mmyy : $expiration;
+}
+
+sub required_fields {
+    my($self,@fields) = @_;
+
+    my @missing;
+    my %content = $self->content();
+    foreach(@fields) {
+      next
+        if (exists $content{$_} && defined $content{$_} && $content{$_}=~/\S+/);
+      push(@missing, $_);
+    }
+
+    Carp::croak("missing required field(s): " . join(", ", @missing) . "\n")
+      if(@missing);
+
+}
+
+sub submit {
+    my ($self) = @_;
+
+    $self->_map_fields();
+
+    my %content = $self->content;
+
+    my %required;
+    $required{CC_20} = [ qw( ePayAccountNum Password OrderNumber
+                             TransactionAmount AccountNumber ExpirationDate
+                             MerchantCustServNum ) ];
+    $required{CC_30} = [ qw( ePayAccountNum Password TransactionCode OrderNum
+                             TransactionAmount CardAccountNum ExpirationDate
+                             CardHolderZip MerchantCustServNum ) ];
+    $required{CC_32} = $required{CC_30};
+    $required{CC_61} = [ qw( ePayAccountNum Password TransactionCode
+                             ReferenceNum ) ];
+    $required{ECHECK_20} = [ qw( ePayAccountNum Password AccountNumber
+                                 RoutingNumber DollarAmount OrderNumber
+                                 CustomerNumber ) ];
+    $required{ECHECK_32} = [ qw( ePayAccountNum Password OrderNumber
+                                 AccountNumber RoutingNumber CheckNumber
+                                 DollarAmount CustomerName CustomerAddress
+                                 CustomerCity CustomerState CustomerZip
+                                 CustomerPhone ) ];
+
+    my %optional;
+    $optional{CC_20} = [ qw( CardHolderName CardHolderAddress CardHolderCity
+                             CardHolderState CardHolderZip CardHolderEmail
+                             CardHolderPhone CustomerNum Misc1 Misc2 CVV2
+                             Ecommerce DuplicateChecking AuthorizedAmount
+                             AutorizedDate AuthorizedTime FulfillmentDate
+                             CardHolderCountry POSEntryMode MerchantStoreNum
+                             CardHolderIDSource SICCATCode MerchantZipCode
+                             AccountDataSource AuthResponseCode AuthSourceCode
+                             AuthACICode AuthValidationCode AuthAVSResponse
+                             MerchantCustServNum CrossReferenceNum
+                             PaymentDescription ReferenceNum ) ];
+    $optional{CC_32} = $optional{CC_30};
+    $optional{CC_30} = [ qw( CardHolderName CardHolderAddress CardHolderCity
+                             CardHolderState CardHolderEmail CardHolderPhone
+                             CustomerNum Misc1 Misc2 CVV2 Ecommerce
+                             DuplicateChecking MessageSequenceNum
+                             CardHolderCountry POSEntryMode MerchantStoreNum
+                             CardHolderIDSource SICCATCode MerchantZipCode
+                             PaymenntDiscriptor CAVVCode ECIValue XID
+                             TaxIndicator TotalTaxAmount ) ];
+    $optional{CC_32} = $optional{CC_30};
+    $optional{CC_61} = [ qw( MessageSequenceNum CrossReferenceNum OrderNum
+                             CustomerNum ) ];
+    $optional{ECHECK_20} = ();
+    $optional{ECHECK_32} = [ qw( CustomerNumber Misc1 Misc2 CustomerEmail
+                                 DriversLicense DriversLicenseState
+                                 BirthDate SocSecNum ) ];
+
+    my $type_action = $self->transaction_type(). '_'. $content{TransactionCode};
+    unless ( exists($required{$type_action}) ) {
+        croak( "TransFirst eLink can't (yet?) handle transaction type: ".
+              "$content{action} on " . $self->transaction_type() );
+    }
+
+    my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} );
+    my $zip          = $content{'zip'};
+    $zip =~ s/[^[:alnum:]]//g;
+
+    my $merchantcustservnum = $self->merchantcustservnum;
+    my $account_number = $self->transaction_type() eq 'CC'
+                           ? $content{card_number}
+                           : $content{account_number} ;
+
+    my $invoice_number = $content{invoice_number} || "PAYMENT";  # make one up
+    my $check_number = $content{check_number} || "100";  # make one up
+
+    $self->_revmap_fields(
+
+        ePayAccountNum      => 'login',
+        Password            => 'password',
+        OrderNum            => \$invoice_number,
+        OrderNumber         => \$invoice_number,
+        MerchantCustServNum => \$merchantcustservnum,
+
+        TransactionAmount   => 'amount',
+        DollarAmount        => 'amount',
+        CardAccountNum      => 'card_number',
+        ExpirationDate      => \$expdate_mmyy,    # MMYY from 'expiration'
+        CVV2                => 'cvv2',
+
+        RoutingNumber       => 'routing_code',
+        AccountNumber       => \$account_number,
+        CheckNumber         => \$check_number,
+
+        CardHolderName      => 'name',
+        CustomerName        => 'account_name',
+        CardHolderAddress   => 'address',
+        CustomerAddress     => 'address',
+        CardHolderCity      => 'city',
+        CustomerCity        => 'city',
+        CardHolderState     => 'state',
+        CustomerState       => 'state',
+        CardHolderZip       => \$zip,          # 'zip' with non-alnums removed
+        CustomerZip         => \$zip,          # 'zip' with non-alnums removed
+        CardHolderEmail     => 'email',
+        CustomerEmail       => 'email',
+        CardHolderPhone     => 'phone',
+        CustomerPhone       => 'phone',
+        CustomerNum         => 'customer_id',
+        CustomerNumber      => 'customer_id',
+        CardHolderCountry   => 'country',
+
+        PaymentDescriptor   => 'description',
+
+        ReferenceNum        => 'order_number'
+    );
+
+    my %params = $self->get_fields( @{$required{$type_action}},
+                                    @{$optional{$type_action}},
+                                  );
+
+    $params{TestTransaction}='Y' if $self->test_transaction;
+
+    $params{InstallmentNum} = $params{InstallmentOf} = '01'
+      unless ($params{InstallmentNum} && $params{InstallmentOf}); 
+
+    if ( $type_action eq "CC_30" || $type_action eq "CC_32" ) {
+      $self->path($self->path."elink/authpd.asp");
+    } elsif ( $type_action eq "CC_61" ) {
+      $self->path($self->path."eLink/voidpd.asp");
+    } elsif ( $type_action eq "CC_20" ) {
+      $self->path($self->path."eLink/creditpd.asp");
+    } elsif ( $type_action eq "ECHECK_32" ) {
+      $self->path($self->path."eLink/checkPD.asp");
+    } elsif ( $type_action eq "ECHECK_20" ) {
+      $self->path($self->path."eLink/checkcreditPD.asp");
+    } else {
+      croak "don't know path for unexpected type and action $type_action";
+    }
+
+    warn join("\n", map{ "$_ => $params{$_}" } keys(%params)) if $DEBUG > 1;
+    my ( $page, $resp, %resp_headers ) =
+      $self->https_post( %params );
+
+    $self->response_code( $resp );
+    $self->response_page( $page );
+    $self->response_headers( \%resp_headers );
+
+    warn "$page\n" if $DEBUG > 1;
+    # $page should contain | separated values
+
+    $self->required_fields(@{$required{$type_action}});
+
+    my $status ='';
+
+    if ( $type_action eq "CC_30" || $type_action eq "CC_32" ) {
+      my ($format,$account,$tcode,$seq,$moi,$cardnum,$exp,$authamt,$authdate,
+          $authtime,$tstat,$custnum,$ordernum,$refnum,$rcode,$authsrc,$achar,
+          $transid,$vcode,$sic,$country,$avscode,$storenum,$cvv2resp,$cavvcode,
+          $crossrefnum,$etstat,$cavvresponse,$xid,$eci,@junk)
+         = split '\|', $page;
+
+      # AVS and CVS values may be set on success or failure
+      $self->avs_code($avscode);
+      $self->cvv2_response( $cvv2resp );
+      $self->result_code( $status = $etstat );
+      $self->order_number( $refnum );
+      $self->authorization( $rcode );
+      $self->junk( \@junk );
+      $self->error_message($error_messages{$status});
+
+    } elsif ( $type_action eq "CC_61" ) {
+      my ($format,$account,$tcode,$voidamt,$seq,$voiddate,$voidtime,$tstat,
+          $refnum,$filler1,$filler2,$filler3,$etstat,@junk)
+         = split '\|', $page;
+      $self->result_code( $status = $etstat );
+      $self->order_number( $refnum );
+      $self->junk( \@junk );
+      $self->error_message($error_messages{$status});
+
+    } elsif ( $type_action eq "CC_20" ) {
+      my ($format,$account,$tcode,$seq,$moi,$authamt,$authdate,$authtime,
+          $tstat,$refnum,$crossrefnum,$custnum,$ordernum,$etstat,@junk)
+         = split '\|', $page;
+      $self->result_code( $status = $etstat );
+      $self->order_number( $refnum );
+      $self->junk( \@junk );
+      $self->error_message($error_messages{$status});
+
+    } elsif ( $type_action eq "ECHECK_32" || $type_action eq "ECHECK_20" ) {
+      my ($responsecode,$response,$transactionid,$note,$errors,@junk)
+         = split '\|', $page;
+      # AVS and CVS values may be set on success or failure
+      $self->result_code( $status = $responsecode );
+      $self->order_number( $transactionid );
+      $self->error_message("$response $errors");
+      $self->junk( \@junk );
+
+    } else {
+      croak "can't interpret response for unexpected type and action $type_action";
+    }
+
+    if ( $resp eq "200" && ($status eq "000" || $status eq "011" || $status eq "085" || $status eq "0P0" || $status eq "P00") ) {
+        $self->is_success(1);
+    }
+    else {
+        $self->is_success(0);
+    }
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Business::OnlinePayment::TransFirsteLink - Transfirst eLink backend for Business::OnlinePayment
+
+=head1 SYNOPSIS
+
+  use Business::OnlinePayment;
+  
+  my $tx = new Business::OnlinePayment(
+      'TransFirsteLink',
+      'merchantcustservnum' => "8005551212",
+  );
+  
+  # See the module documentation for details of content()
+  $tx->content(
+      type           => 'CC',
+      action         => 'Normal Authorization',
+      description    => 'Business::OnlinePayment::TransFirsteLink test',
+      amount         => '49.95',
+      invoice_number => '100100',
+      customer_id    => 'jef',
+      name           => 'Jeff Finucane',
+      address        => '123 Anystreet',
+      city           => 'Anywhere',
+      state          => 'GA',
+      zip            => '30004',
+      email          => 'transfirst@weasellips.com',
+      card_number    => '4111111111111111',
+      expiration     => '12/09',
+      cvv2           => '123',
+      order_number   => 'string',
+  );
+  
+  $tx->submit();
+  
+  if ( $tx->is_success() ) {
+      print(
+          "Card processed successfully: ", $tx->authorization, "\n",
+          "order number: ",                $tx->order_number,  "\n",
+          "CVV2 response: ",               $tx->cvv2_response, "\n",
+          "AVS code: ",                    $tx->avs_code,      "\n",
+      );
+  }
+  else {
+      my $info = "";
+      $info = " (CVV2 mismatch)" if ( $tx->result_code == 114 );
+      
+      print(
+          "Card was rejected: ", $tx->error_message, $info, "\n",
+          "order number: ",      $tx->order_number,         "\n",
+      );
+  }
+
+=head1 DESCRIPTION
+
+This module is a back end driver that implements the interface
+specified by L<Business::OnlinePayment> to support payment handling
+via TransFirst's eLink Internet payment solution.
+
+See L<Business::OnlinePayment> for details on the interface this
+modules supports.
+
+=head1 Standard methods
+
+=over 4
+
+=item set_defaults()
+
+This method sets the 'server' attribute to 'epaysecure1.transfirst.com' and
+the port attribute to '443'.  This method also sets up the
+L</Module specific methods> described below.
+
+=item submit()
+
+=back
+
+=head1 Unofficial methods
+
+This module provides the following methods which are not officially part of the
+standard Business::OnlinePayment interface (as of 3.00_06) but are nevertheless
+supported by multiple gateways modules and expected to be standardized soon:
+
+=over 4
+
+=item L<order_number()|/order_number()>
+
+=item L<avs_code()|/avs_code()>
+
+=item L<cvv2_response()|/cvv2_response()>
+
+=back
+
+=head1 Module specific methods
+
+This module provides the following methods which are not currently
+part of the standard Business::OnlinePayment interface:
+
+=over 4
+
+=item L<expdate_mmyy()|/expdate_mmyy()>
+
+=item L<debug()|/debug()>
+
+=back
+
+=head1 Settings
+
+The following default settings exist:
+
+=over 4
+
+=item server
+
+epaysecure1.transfirst.com
+
+=item port
+
+443
+
+=back
+
+=head1 Handling of content(%content)
+
+The following rules apply to content(%content) data:
+
+=head2 type
+
+If 'type' matches one of the following keys it is replaced by the
+right hand side value:
+
+  'visa'               => 'CC',
+  'mastercard'         => 'CC',
+  'american express'   => 'CC',
+  'discover'           => 'CC',
+  'check'              => 'ECHECK',
+
+The value of 'type' is used to set transaction_type().  Currently this
+module only supports the above values.
+
+=head1 Setting TransFirst eLink parameters from content(%content)
+
+The following rules are applied to map data to TransFirst eLink parameters
+from content(%content):
+
+    # eLink param       => $content{<key>}
+      ePayAccountNum    => 'login',
+      Password          => 'password',
+      OrderNum          => 'invoice_number',
+      OrderNumber       => 'invoice_number',
+
+      TransactionAmount => 'amount',
+      DollarAmount      => 'amount',
+      CardAccountNum    => 'card_number',
+      ExpirationDate    => \( $month.$year ), # MM/YY from 'expiration'
+      CVV2              => 'cvv2',
+
+      RoutingNumber     => 'routing_code',
+      AccountNumber     => \( $type eq 'CC' ? $card_number : $account_number ),
+      CheckNumber       => 'check_number',
+
+      CardHolderName    => 'name',
+      CardHolderAddress => 'address',
+      CardHolderCity    => 'city',
+      CardHolderState   => 'state',
+      CardHolderZip     => \$zip,       # 'zip' with non-alphanumerics removed
+      CardHolderEmail   => 'email',
+      CardHolderPhone   => 'phone',
+      CardHolderCountry => 'country',
+
+      CustomerName      => 'name',
+      CustomerAddress   => 'address',
+      CustomerCity      => 'city',
+      CustomerState     => 'state',
+      CustomerZip       => \$zip,       # 'zip' with non-alphanumerics removed
+      CustomerEmail     => 'email',
+      CustomerPhone     => 'phone',
+
+      PaymentDescriptor => 'description',
+
+=head1 Mapping TransFirst eLink transaction responses to object methods
+
+The following methods provides access to the transaction response data
+resulting from a Payflow Pro request (after submit()) is called:
+
+=head2 order_number()
+
+This order_number() method returns the ReferenceNum field for card transactions
+and TransactionId for check transactions to uniquely identify the transaction.
+
+=head2 result_code()
+
+The result_code() method returns the Extended Transaction Status field for
+card transactions and the Result Code field for check transactions.  It is the
+numeric return code indicating the outcome of the attempted
+transaction.
+
+=head2 error_message()
+
+The error_message() method returns the Errors field for check
+transactions.  This provides more details about the transaction result.
+
+=head2 authorization()
+
+The authorization() method returns the Authorization Response Code field,
+which is the approval code obtained from the card processing network.
+
+=head2 avs_code()
+
+The avs_code() method returns the AVS Response Code field from the
+transaction result.
+
+=head2 cvv2_response()
+
+The cvv2_response() method returns the CVV2 Response Code field, which is a
+response message returned with the transaction result.
+
+=head2 expdate_mmyy()
+
+The expdate_mmyy() method takes a single scalar argument (typically
+the value in $content{expiration}) and attempts to parse and format
+and put the date in MMYY format as required by PayflowPro
+specification.  If unable to parse the expiration date simply leave it
+as is and let the PayflowPro system attempt to handle it as-is.
+
+=head2 debug()
+
+Enable or disble debugging.  The value specified here will also set
+$Business::OnlinePayment::HTTPS::DEBUG in submit() to aid in
+troubleshooting problems.
+
+=head1 COMPATIBILITY
+
+This module implements an interface to the TransFirst eLink API version
+3.4
+
+=head1 AUTHORS
+
+Jeff Finucane <transfirst@weasellips.com>
+
+Based on Business::OnlinePayment::PayflowPro written by Ivan Kohler
+and Phil Lobbes.
+
+=head1 SEE ALSO
+
+perl(1), L<Business::OnlinePayment>, L<Carp>, and the TransFirst
+e Payment Services Card Not Present eLink User Guide.
+
+=cut
diff --git a/t/00load.t b/t/00load.t
new file mode 100644 (file)
index 0000000..40e2dfb
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Test::More tests => 2;
+
+BEGIN {
+    use_ok("Business::OnlinePayment")
+      or BAIL_OUT("unable to load Business::OnlinePayment\n");
+
+    use_ok("Business::OnlinePayment::TransFirsteLink")
+      or BAIL_OUT("unable to load Business::OnlinePayment::TransFirsteLink\n");
+}
diff --git a/t/bop.t b/t/bop.t
new file mode 100644 (file)
index 0000000..dfa4148
--- /dev/null
+++ b/t/bop.t
@@ -0,0 +1,50 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Test::More tests => 10;
+
+use Business::OnlinePayment;
+
+my $package = "Business::OnlinePayment";
+my $driver  = "TransFirsteLink";
+
+{    # new
+    my $obj;
+
+    $obj = $package->new($driver);
+    isa_ok( $obj, $package );
+
+    # convenience methods
+    can_ok( $obj, qw(order_number avs_code cvv2_response) );
+    can_ok( $obj, qw(debug expdate_mmyy) );
+
+    # internal methods
+    can_ok( $obj, qw(_map_fields _revmap_fields) );
+
+    # defaults
+    my $server = "epaysecure1.transfirst.com";
+
+    is( $obj->server, $server, "server($server)" );
+    is( $obj->port, "443", "port(443)" );
+}
+
+{    # expdate
+    my $obj = $package->new($driver);
+    my @exp = (
+
+        #OFF [qw(1999.8   0899)],
+        #OFF [qw(1984-11  1184)],
+        #OFF [qw(06/7     0706)],
+        #OFF [qw(06-12    1206)],
+        [qw(12/06    1206)],
+        [qw(6/2000   0600)],
+        [qw(10/2000  1000)],
+        [qw(1/99     0199)],
+    );
+    foreach my $aref (@exp) {
+        my ( $exp, $moyr ) = @$aref;
+        my ($mmyy) = $obj->expdate_mmyy($exp);
+        is( $mmyy, $moyr, "$exp: MMYY '$mmyy' eq '$moyr' from $exp" );
+    }
+}
diff --git a/t/credit_card.t b/t/credit_card.t
new file mode 100644 (file)
index 0000000..fe04da2
--- /dev/null
@@ -0,0 +1,236 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use POSIX qw(strftime);
+use Test::More;
+
+use Business::OnlinePayment;
+
+my $runinfo =
+    "to test set environment variables:"
+  . " (required) ELINK_ACCOUNT and ELINK_PASSWORD";
+
+plan(
+      ( $ENV{"ELINK_ACCOUNT"} && $ENV{"ELINK_PASSWORD"} )
+    ? ( tests => 70 )
+    : ( skip_all => $runinfo )
+);
+
+my %opts = (
+    "debug"    => 0,
+    "merchantcustservnum" => "8005551212",
+);
+
+my %content = (
+    login          => $ENV{"ELINK_ACCOUNT"},
+    password       => $ENV{"ELINK_PASSWORD"},
+    action         => "Normal Authorization",
+    type           => "VISA",
+    description    => "Business::OnlinePayment::TransFirsteLink test",
+    card_number    => "4111111111111111",
+    cvv2           => "123",
+    expiration     => "12/" . strftime( "%y", localtime ),
+    amount         => "0.01",
+    invoice_number => "Test1",
+    first_name     => "Tofu",
+    last_name      => "Beast",
+    email          => 'transfirst@weasellips.com',
+    address        => "123 Anystreet",
+    city           => "Anywhere",
+    state          => "GA",
+    zip            => "30004",
+    country        => "US",
+);
+
+{    # valid card number test
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content(%content);
+    tx_check(
+        $tx,
+        desc          => "valid card_number",
+        is_success    => 1,
+        result_code   => "000",
+        authorization => "999999",
+        avs_code      => "9",      # useless
+        cvv2_response => "99",     # doubly useless - docs say 1 char
+    );
+}
+
+{    # invalid card number test
+
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, card_number => "4111111111111112" );
+    tx_check(
+        $tx,
+        desc          => "invalid card_number",
+        is_success    => 0,
+        result_code   => 214,
+        authorization => '',
+        avs_code      => '',
+        cvv2_response => '',
+    );
+}
+
+
+SKIP: {    # avs_code() / AVSZIP and AVSADDR tests
+
+    skip "AVS tests broken", 28;
+
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+
+    # IF first 3 chars of STREET <= 334 and >= 666 THEN AVSADDR == "N"
+    $tx->content( %content, "address" => "500 Any street" );
+    tx_check(
+        $tx,
+        desc          => "AVSADDR=N,AVSZIP=Y",
+        is_success    => 0,
+        result_code   => 126,
+        authorization => "010101",
+        avs_code      => "Z",
+        cvv2_response => "Y",
+    );
+
+    # IF first 3 chars of STREET >= 667 THEN AVSADDR == "X" (and AVSZIP="X")
+    $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, "address" => "700 Any street" );
+    tx_check(
+        $tx,
+        desc          => "AVSADDR=X,AVSZIP=X",
+        is_success    => 1,
+        result_code   => 0,
+        authorization => "010101",
+        avs_code      => "",
+        cvv2_response => "Y",
+    );
+
+#    # IF ZIP <= 50001 and >= 99999 THEN AVSZIP == "N"
+    $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, "zip" => "99999" );
+    tx_check(
+        $tx,
+        desc          => "AVSADDR=Y,AVSZIP=N",
+        is_success    => 0,
+        result_code   => 126,
+        authorization => "010101",
+        avs_code      => "A",
+        cvv2_response => "Y",
+    );
+
+    # Both AVSADDR and AVSZIP == "N"
+    $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, "address" => "500 Any street", "zip" => "99999" );
+    tx_check(
+        $tx,
+        desc          => "AVSADDR=N,AVSZIP=N",
+        is_success    => 0,
+        result_code   => 126,
+        authorization => "010101",
+        avs_code      => "N",
+        cvv2_response => "Y",
+    );
+}
+
+SKIP: {    # cvv2_response() / CVV2MATCH
+
+    skip "CVV2 tests broken", 14;
+
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+
+    # IF CVV2 >= 301 and <= 600 THEN CVV2MATCH == "N"
+    $tx->content( %content, "cvv2" => "301" );
+    tx_check(
+        $tx,
+        desc          => "cvv2(301)",
+        is_success    => 0,
+        result_code   => 126,
+        authorization => "010101",
+        avs_code      => "Y",
+        cvv2_response => "N",
+    );
+
+    # IF CVV2 >= 601 THEN CVV2MATCH == "X"
+    $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, "cvv2" => "601" );
+    tx_check(
+        $tx,
+        desc          => "cvv2(601)",
+        is_success    => 0,
+        result_code   => 126,
+        authorization => "010101",
+        avs_code      => "Y",
+        cvv2_response => "X",
+    );
+}
+
+SKIP: {    # refund test
+
+    skip "credit/refund tests broken", 7;
+
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, 'action' => "Credit",
+                            'card_number' => "4444333322221111",
+                );
+    tx_check(
+        $tx,
+        desc          => "refund/credit",
+        is_success    => 0,
+        result_code   => "000",
+        authorization => '',
+        avs_code      => '',
+        cvv2_response => '',
+    );
+}
+
+SKIP: {    # void test
+
+    skip "void tests broken", 7;
+
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, 'action' => "Void",
+                            'order_number' => "12345678901234",
+                );
+    tx_check(
+        $tx,
+        desc          => "void",
+        is_success    => 0,
+        result_code   => "000",
+        authorization => '',
+        avs_code      => '',
+        cvv2_response => '',
+    );
+}
+
+sub tx_check {
+    my $tx = shift;
+    my %o  = @_;
+
+    $tx->test_transaction(1);
+    $tx->submit;
+
+    is( $tx->is_success,    $o{is_success},    "$o{desc}: " . tx_info($tx) );
+    is( $tx->result_code,   $o{result_code},   "result_code(): RESULT" );
+    is( $tx->authorization, $o{authorization}, "authorization() / AUTHCODE" );
+    is( $tx->avs_code,  $o{avs_code},  "avs_code() / AVSADDR and AVSZIP" );
+    is( $tx->cvv2_response, $o{cvv2_response}, "cvv2_response() / CVV2MATCH" );
+    is( scalar(@{$tx->junk}), 0, "junk() / JUNK " );
+    like( $tx->order_number, qr/^(\d{14}|)$/, "order_number() / PNREF" );
+}
+
+sub tx_info {
+    my $tx = shift;
+
+    no warnings 'uninitialized';
+
+    return (
+        join( "",
+            "is_success(",     $tx->is_success,    ")",
+            " order_number(",  $tx->order_number,  ")",
+            " result_code(",   $tx->result_code,   ")",
+            " auth_info(",     $tx->authorization, ")",
+            " avs_code(",      $tx->avs_code,      ")",
+            " cvv2_response(", $tx->cvv2_response, ")",
+            $tx->junk ? " junk(". join('|', @{$tx->junk}). ")" : '',
+        )
+    );
+}
diff --git a/t/echeck.t b/t/echeck.t
new file mode 100644 (file)
index 0000000..2d3de74
--- /dev/null
@@ -0,0 +1,118 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use POSIX qw(strftime);
+use Test::More;
+
+use Business::OnlinePayment;
+
+my $runinfo =
+    "to test set environment variables:"
+  . " (required) ELINK_ACH_ACCOUNT and ELINK_ACH_PASSWORD";
+
+plan(
+      ( $ENV{"ELINK_ACH_ACCOUNT"} && $ENV{"ELINK_ACH_PASSWORD"} )
+    ? ( tests => 12 )
+    : ( skip_all => $runinfo )
+);
+
+my %opts = (
+    "debug"    => 0,
+    "merchantcustservnum" => "8005551212",
+);
+
+my %content = (
+    login          => $ENV{"ELINK_ACH_ACCOUNT"},
+    password       => $ENV{"ELINK_ACH_PASSWORD"},
+    action         => "Normal Authorization",
+    type           => "CHECK",
+    description    => "Business::OnlinePayment::TransFirsteLink test",
+    routing_code   => "052000113",
+    account_number => "000000000001",
+    check_number   => "100",
+    cvv2           => "123",
+    expiration     => "12/" . strftime( "%y", localtime ),
+    amount         => "0.01",
+    invoice_number => "1999",
+    account_name   => "Tofu Beast",
+    customer_id    => "TB01",
+    email          => 'transfirst@weasellips.com',
+    address        => "123 Anystreet",
+    city           => "Anywhere",
+    state          => "GA",
+    zip            => "30004",
+    country        => "US",
+    phone          => "4045551212",
+);
+
+{    # valid account test
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content(%content);
+    tx_check(
+        $tx,
+        desc          => "valid account",
+        is_success    => 1,
+        result_code   => "P00",
+    );
+}
+
+SKIP: {    # invalid account test
+
+    skip "invalid account tests broken", 4;
+
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, routing_code   => "052000113",
+                            account_number => "000000000001",
+                );
+
+    tx_check(
+        $tx,
+        desc          => "invalid account",
+        is_success    => 0,
+        result_code   => 214,
+    );
+}
+
+SKIP: {    # credit/refund test
+
+    skip "credit/refund tests broken", 4;
+
+    my $tx = new Business::OnlinePayment( "TransFirsteLink", %opts );
+    $tx->content( %content, action => "Credit");
+
+    tx_check(
+        $tx,
+        desc          => "credit/refund",
+        is_success    => 0,
+        result_code   => "P00",
+    );
+}
+
+sub tx_check {
+    my $tx = shift;
+    my %o  = @_;
+
+    $tx->test_transaction(1);
+    $tx->submit;
+
+    is( $tx->is_success,    $o{is_success},    "$o{desc}: " . tx_info($tx) );
+    is( $tx->result_code,   $o{result_code},   "result_code(): RESULT" );
+    is( scalar(@{$tx->junk}), 0, "junk() / JUNK " );
+    like( $tx->order_number, qr/^(\d{9}|)$/, "order_number() / PNREF" );
+}
+
+sub tx_info {
+    my $tx = shift;
+
+    no warnings 'uninitialized';
+
+    return (
+        join( "",
+            "is_success(",     $tx->is_success,    ")",
+            " order_number(",  $tx->order_number,  ")",
+            " result_code(",   $tx->result_code,   ")",
+            $tx->junk ? " junk(". join('|', @{$tx->junk}). ")" : '',
+        )
+    );
+}
diff --git a/t/pod.t b/t/pod.t
new file mode 100644 (file)
index 0000000..2c9935c
--- /dev/null
+++ b/t/pod.t
@@ -0,0 +1,9 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Test::More;
+
+eval "use Test::Pod 1.00";
+plan skip_all => "Test::Pod 1.00 required for testing POD" if $@;
+all_pod_files_ok();