From 882e59c8f72abc8e3b0047a0bd9887bf5d1112b6 Mon Sep 17 00:00:00 2001 From: jeff Date: Fri, 11 May 2007 18:06:55 +0000 Subject: [PATCH 1/1] Initial import --- Changes | 4 + MANIFEST | 11 + Makefile.PL | 21 ++ README | 11 + TransFirsteLink.pm | 778 +++++++++++++++++++++++++++++++++++++++++++++++++++++ t/00load.t | 13 + t/bop.t | 50 ++++ t/credit_card.t | 236 ++++++++++++++++ t/echeck.t | 118 ++++++++ t/pod.t | 9 + 10 files changed, 1251 insertions(+) create mode 100644 Changes create mode 100644 MANIFEST create mode 100644 Makefile.PL create mode 100644 README create mode 100644 TransFirsteLink.pm create mode 100644 t/00load.t create mode 100644 t/bop.t create mode 100644 t/credit_card.t create mode 100644 t/echeck.t create mode 100644 t/pod.t diff --git a/Changes b/Changes new file mode 100644 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 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 index 0000000..0469740 --- /dev/null +++ b/Makefile.PL @@ -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 ', + ) + : () + ), +); diff --git a/README b/README new file mode 100644 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 . + diff --git a/TransFirsteLink.pm b/TransFirsteLink.pm new file mode 100644 index 0000000..c5dcfe6 --- /dev/null +++ b/TransFirsteLink.pm @@ -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 to support payment handling +via TransFirst's eLink Internet payment solution. + +See L 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 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 + +=item L + +=item L + +=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 + +=item L + +=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{} + 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 + +Based on Business::OnlinePayment::PayflowPro written by Ivan Kohler +and Phil Lobbes. + +=head1 SEE ALSO + +perl(1), L, L, 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 index 0000000..40e2dfb --- /dev/null +++ b/t/00load.t @@ -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 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 index 0000000..fe04da2 --- /dev/null +++ b/t/credit_card.t @@ -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 index 0000000..2d3de74 --- /dev/null +++ b/t/echeck.t @@ -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 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(); -- 2.11.0