From f6660ccc1cf3719650dc179aa7b1fd117b07ed0b Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 9 Jul 2014 12:13:23 -0700 Subject: [PATCH] initial try at a module --- Changes | 6 + MANIFEST | 13 + Makefile.PL | 23 ++ README | 14 + ignore.txt | 12 + .../OnlinePayment/FirstDataGlobalGateway.pm | 319 +++++++++++++++++++++ t/00-load.t | 10 + t/boilerplate.t | 49 ++++ t/manifest.t | 13 + t/pod-coverage.t | 18 ++ t/pod.t | 12 + t/transaction.t | 39 +++ t/transaction_decline.t | 38 +++ 13 files changed, 566 insertions(+) create mode 100644 Changes create mode 100644 MANIFEST create mode 100644 Makefile.PL create mode 100644 README create mode 100644 ignore.txt create mode 100644 lib/Business/OnlinePayment/FirstDataGlobalGateway.pm create mode 100644 t/00-load.t create mode 100644 t/boilerplate.t create mode 100644 t/manifest.t create mode 100644 t/pod-coverage.t create mode 100644 t/pod.t create mode 100644 t/transaction.t create mode 100644 t/transaction_decline.t diff --git a/Changes b/Changes new file mode 100644 index 0000000..e767bd2 --- /dev/null +++ b/Changes @@ -0,0 +1,6 @@ + +Revision history for Perl module Business::OnlinePayment::FirstDataGlobalGateway + +0.01 unreleased + - original version. + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..b7b925c --- /dev/null +++ b/MANIFEST @@ -0,0 +1,13 @@ +Changes +MANIFEST +Makefile.PL +README +ignore.txt +lib/Business/OnlinePayment/FirstDataGlobalGateway.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 index 0000000..838e74d --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,23 @@ +use strict; +use warnings; +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => 'Business::OnlinePayment::FirstDataGlobalGateway', + AUTHOR => q{Ivan Kohler }, + VERSION_FROM => 'lib/Business/OnlinePayment/FirstDataGlobalGateway.pm', + ABSTRACT_FROM => 'lib/Business/OnlinePayment/FirstDataGlobalGateway.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-FirstDataGlobalGateway-*' }, +); + diff --git a/README b/README new file mode 100644 index 0000000..27e01eb --- /dev/null +++ b/README @@ -0,0 +1,14 @@ +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::FirstDataGlobalGateway, a +Business::OnlinePayment backend module for First Data Global Gateway e4. It is +only useful if you have a merchant account with First Data: +https://www.firstdata.com/en_us/products/merchants/ecommerce/online-payment-processing.html + +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 index 0000000..c1fd48f --- /dev/null +++ b/ignore.txt @@ -0,0 +1,12 @@ +blib* +Makefile +Makefile.old +Build +Build.bat +_build* +pm_to_blib* +*.tar.gz +.lwpcookies +cover_db +pod2htm*.tmp +Business-OnlinePayment-FirstDataGlobalGateway-* diff --git a/lib/Business/OnlinePayment/FirstDataGlobalGateway.pm b/lib/Business/OnlinePayment/FirstDataGlobalGateway.pm new file mode 100644 index 0000000..081100d --- /dev/null +++ b/lib/Business/OnlinePayment/FirstDataGlobalGateway.pm @@ -0,0 +1,319 @@ +package Business::OnlinePayment::FirstDataGlobalGateway; +use base qw( Business::OnlinePayment ); + +use warnings; +use strict; +use Data::Dumper; +use Business::CreditCard; +use SOAP::Lite +trace => 'all'; +#SOAP::Lite->import(+trace=>'debug'); + +our $VERSION = '0.01'; +$VERSION = eval $VERSION; # modperlstyle: convert the string into a number + +our @alpha = ( 'a'..'z', 'A'..'Z', '0'..'9' ); + +sub _info { + { + 'info_compat' => '0.01', + 'gateway_name' => 'First Data Global Gateway e4', + 'gateway_url' => 'https://www.firstdata.com/en_us/products/merchants/ecommerce/online-payment-processing.html', + 'module_version' => $VERSION, + 'supported_types' => [ 'CC' ], #, 'ECHECK' ], + #'token_support' => 1, + #'test_transaction' => 1, + + 'supported_actions' => [ 'Normal Authorization', + #'Credit', + ], + }; +} + +sub set_defaults { + my $self = shift; + #my %opts = @_; + + #$self->build_subs(qw( order_number avs_code cvv2_response + # response_page response_code response_headers + # )); + + $self->build_subs(qw( avs_code )); + +} + +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' => '00', # Purchase + 'authorization_only' => '01', # + 'post authorization' => '02', # Pre-Authorization Completion + # '' => '03', # Forced Post + 'credit' => '04', # Refund + # '' => '05', # Pre-Authorization Only + 'void' => '13', # Void + #'reverse authorization' => '', + + # '' => '07', # PayPal Order + # '' => '32', # Tagged Pre-Authorization Completion + # '' => '33', # Tagged Void + # '' => '34', # Tagged Refund + # '' => '83', # CashOut (ValueLink, v9 or higher end point only) + # '' => '85', # Activation (ValueLink, v9 or higher end point only) + # '' => '86', # Balance Inquiry (ValueLink, v9 or higher end point only) + # '' => '88', # Reload (ValueLink, v9 or higher end point only) + # '' => '89', # Deactivation (ValueLink, v9 or higher end point only) + ); + + $content{'action'} = $actions{$action} || $action; + + # 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); +} + +sub submit { + my($self) = @_; + + $self->map_fields; + + $self->remap_fields( + 'login' => 'ExactID', + 'password' => 'Password', + + 'action' => 'TransactionType', + + 'amount' => 'DollarAmount', + 'currency' => 'Currency', + 'card_number' => 'Card_Number', + 'track1' => 'Track1', + 'track2' => 'Track2', + 'expiration' => 'Expiry_Date', + 'name' => 'CardHoldersName', + 'cvv2' => 'VerificationStr2', + + 'authorization' => 'Authorization_Num', + 'order_number' => 'Reference_No', + + 'zip' => 'ZipCode', + 'tax' => 'Tax1Amount', + 'customer_id' => 'Customer_Ref', + 'customer_ip' => 'Client_IP', + 'email' => 'Client_Email', + + #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'; + #} + + $content{Expiry_Date} =~ s/\///; + + $content{country} ||= 'US'; + + $content{VerificationStr1} = + join('|', map $content{$_}, qw( address zip city state country )); + $content{VerificationStr1} .= '|'. $content{'phone'} + if $content{'type'} eq 'ECHECK'; + + $content{CVD_Presence_Ind} = '1' if length($content{VerificationStr2}); + + $content{'Reference_No'} ||= join('', map $alpha[int(rand(62))], (1..20) ); + + #XXX this should be exposed as a standard B:OP field, not just recurring/no + if ( defined($content{'recurring_billing'}) + && $content{'recurring_billing'} =~ /^[y1]/ ) { + $content{'Ecommerce_Flag'} = '2'; + } else { + #$content{'Ecommerce_Flag'} = '1'; 7? if there's an IP? + } + + my $base_uri; + if ( $self->test_transaction ) { + $base_uri = + 'https://api.demo.globalgatewaye4.firstdata.com/transaction'; + } else { + $base_uri = + 'https://api.globalgatewaye4.firstdata.com/vplug-in/transaction'; + } + + my $proxy = "$base_uri/v11"; + my $uri = "$base_uri/rpc-enc"; + + my %transaction = map { $_ => $content{$_} } (qw( + ExactID Password Transaction_Type DollarAmount Card_Number Transaction_Tag + Track1 Track2 Authorization_Num Expiry_Date CardHoldersName + VerificationStr1 VerificationStr2 CVD_Presence_Ind Reference_No ZipCode + Tax1Amount Tax1Number Tax2Amount Tax2Number Customer_Ref Reference_3 + Language Client_IP Client_Email user_name Currency PartialRedemption + CAVV XID Ecommerce_Flag + )); + #TransarmorToken CardType EAN VirtualCard CardCost FraudSuspected + #CheckNumber CheckType BankAccountNumber BankRoutingNumber CustomerName + #CustomerIDType CustomerID + + #my @opts = map { SOAP::Data->name($_)->value( $data{$_} ) } + # keys %data; + + my $result = SOAP::Lite + ->proxy($proxy) + + ->default_ns($base_uri) + ->uri($uri) + + ->on_action( sub { join '/', @_ } ) + #->on_action( sub { join '', @_ } ) + #->on_action(sub { qq("$_[0]") }) #? https://firstdata.zendesk.com/entries/407569-First-Data-Global-Gateway-e4-Web-Service-API-Sample-Code-Perl + ->autotype(0) + + ->readable(1) + + ->ns($uri,'q1') + ->SendAndCommit( SOAP::Data->name('Transaction')->value( \%transaction ) ) + + ->result(); + + die Dumper($result); + + die Dumper($result->result) if $result->fault; + #die $result->fault->faultstring if $result->fault; + + die Dumper($result); + + #$self->is_success + #$self->authorization + #$self->avs_code + #$self->error_message + #$self->result_code + ##$self->failure_status + +} + +1; + +__END__ + +=head1 NAME + +Business::OnlinePayment::FirstDataGlobalGateway - First Data Global Gateway e4 backend for Business::OnlinePayment + +=head1 SYNOPSIS + + use Business::OnlinePayment; + + my $tx = + new Business::OnlinePayment( 'FirstDataGlobalGateway' ); + + $tx->content( + login => 'TEST88', # ExactID + 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 + customer_ip => '1.2.3.4', + ); + $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 (NOT YET) Check + +Content required: type, login, action, amount, name, account_number, routing_code. + +=head1 DESCRIPTION + +For detailed information see L. + +=head1 METHODS AND FUNCTIONS + +See L for the complete list. The following methods either override the methods in L 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 + Authorization Only + Post Authorization + Credit + Void + +=head1 COMPATIBILITY + +Business::OnlinePayment::FirstDataGlobalGateway uses the v11 version of the API +at this time. + +=head1 AUTHORS + +Ivan Kohler + +=head1 SEE ALSO + +perl(1). L. + +=cut + diff --git a/t/00-load.t b/t/00-load.t new file mode 100644 index 0000000..1a0cc65 --- /dev/null +++ b/t/00-load.t @@ -0,0 +1,10 @@ +#!perl -T + +use Test::More tests => 1; + +BEGIN { + use_ok( 'Business::OnlinePayment::FirstDataGlobalGateway' ) || print "Bail out! +"; +} + +diag( "Testing Business::OnlinePayment::FirstDataGlobalGateway $Business::OnlinePayment::FirstDataGlobalGateway::VERSION, Perl $], $^X" ); diff --git a/t/boilerplate.t b/t/boilerplate.t new file mode 100644 index 0000000..7a269c7 --- /dev/null +++ b/t/boilerplate.t @@ -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/FirstDataGlobalGateway.pm'); + diff --git a/t/manifest.t b/t/manifest.t new file mode 100644 index 0000000..45eb83f --- /dev/null +++ b/t/manifest.t @@ -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 index 0000000..c021dd4 --- /dev/null +++ b/t/pod-coverage.t @@ -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 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 index 0000000..28db227 --- /dev/null +++ b/t/transaction.t @@ -0,0 +1,39 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use POSIX qw(strftime); +use Test::More tests => 1; + +use Business::OnlinePayment; + +my %content = ( + login => '124freeside', + password => 'freeside124', + action => "Normal Authorization", + type => "CC", + description => "Business::OnlinePayment::FirstDataGlobalGateway 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', +); + +my $tx = new Business::OnlinePayment( 'FirstDataGlobalGateway' ); + +$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 index 0000000..47a4cd1 --- /dev/null +++ b/t/transaction_decline.t @@ -0,0 +1,38 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use POSIX qw(strftime); +use Test::More tests => 3; + +use Business::OnlinePayment; + +my %content = ( + action => "Normal Authorization", + type => "CC", + description => "Business::OnlinePayment::FirstDataGlobalGateway 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( 'FirstDataGlobalGateway' ); + +$tx->content( %content ); + +$tx->test_transaction(1); + +$tx->submit; + +unlike( $tx->error_message, qr/^Agent code has not been set up/, 'Test decline not a login error'); +is( $tx->is_success, 0, 'Test decline transaction successful'); +is( $tx->failure_status, 'decline', 'Test decline failure_status set'); + +1; -- 2.11.0