From: Mitch Jackson Date: Mon, 22 Apr 2019 01:58:05 +0000 (-0400) Subject: Implement payment profile creation X-Git-Url: http://git.freeside.biz/gitweb/?p=Business-OnlinePayment-Bambora.git;a=commitdiff_plain;h=24c86c6b9136ad878a118d57fc9b876eee3672f8 Implement payment profile creation --- diff --git a/lib/Business/OnlinePayment/Bambora.pm b/lib/Business/OnlinePayment/Bambora.pm index c92ff09..f0c7916 100755 --- a/lib/Business/OnlinePayment/Bambora.pm +++ b/lib/Business/OnlinePayment/Bambora.pm @@ -6,14 +6,18 @@ use feature 'unicode_strings'; use Carp qw( croak ); use Cpanel::JSON::XS; -use Data::Dumper; $Data::Dumper::Sortkeys = 1; +use Data::Dumper; + $Data::Dumper::Sortkeys = 1; + $Data::Dumper::Indent = 1; +use LWP::UserAgent; use MIME::Base64; +use Time::HiRes; use Unicode::Truncate qw( truncate_egc ); use URI::Escape; use vars qw/ $VERSION $DEBUG /; -$VERSION = '0.01'; -$DEBUG = 0; +$VERSION = '0.1'; +$DEBUG = 1; if ( $DEBUG ) { $Data::Dumper::Sortkeys = 1; @@ -35,6 +39,7 @@ sub set_defaults { # Create accessors for $self->build_subs(qw/ + card_token expiry_month expiry_year invoice_number @@ -118,36 +123,46 @@ sub submit_normal_authorization { if ( $content->{card_number} ) { $post{payment_method} = 'card'; - # Parse the expiration date into expiry_month and expiry_year - $self->set_expiration; + # Add card payment details to %post + $post{card} = $self->jhref_card; + return if $self->error_message; + + # Designate recurring payment label + $post{card}->{recurring_payment} = $content->{recurring_payment} ? 1 : 0; - $post{card} = { - number => $self->truncate( $content->{card_number}, 20 ), - name => $self->truncate( $content->{owner}, 64 ), - expiry_month => sprintf( '%02d', $content->{expiry_month} ), - expiry_year => sprintf( '%02d', $content->{expiry_year} ), - cvd => $content->{cvv2}, - recurring_payment => $content->{recurring_payment} ? 1 : 0, - complete => 1, - }; + # Direct API to issue a complete auth, instead of pre-auth + $post{card}->{complete} = 1; + + # $post{card} = { + # number => $self->truncate( $content->{card_number}, 20 ), + # name => $self->truncate( $content->{owner}, 64 ), + # expiry_month => sprintf( '%02d', $content->{expiry_month} ), + # expiry_year => sprintf( '%02d', $content->{expiry_year} ), + # cvd => $content->{cvv2}, + # recurring_payment => $content->{recurring_payment} ? 1 : 0, + # complete => 1, + # }; } else { - die 'unknown/unsupported payment method!'; + croak 'unknown/unsupported payment method!'; } my $action = lc $content->{action}; if ( $action eq 'normal authorization' ) { + # Perform complete authorization $self->path('/v1/payments'); + } elsif ( $action eq 'authorization only' ) { + # Perform pre-authorization $self->path('/v1/payments'); - if ( ref $post{card} ) { - $post{card}->{complete} = 0; - } + $post{card}->{complete} = 0; + } elsif ( $action eq 'post authorization' ) { + # Complete a pre-authorization croak 'post authorization cannot be completed - '. - 'bambora transaction_id must be set as order_number '. + 'bambora transaction_id must be set as content order_number '. 'before using submit()' unless $content->{order_number}; @@ -168,6 +183,7 @@ sub submit_normal_authorization { if ( $DEBUG ) { warn Dumper({ + path => $self->path, post_body => $post_body, post_href => \%post, }); @@ -175,7 +191,7 @@ sub submit_normal_authorization { my $response = $self->submit_api_request( $post_body ); - # Error messages already populated upon failure + # Any error messages will have been populated by submit_api_request return unless $self->is_success; # Populate transaction result values @@ -256,8 +272,9 @@ sub submit_void { } } + # The posted JSON string needs only contain the amount. + # The bambora order_number being voided is passed in the URL my %post = ( -# order_number => $self->truncate( $content->{invoice_number}, 30 ), amount => $content->{amount}, ); my $post_body = encode_json( \%post ); @@ -271,10 +288,92 @@ sub submit_void { $self->path( sprintf '/v1/payments/%s/returns', $content->{order_number} ); my $response = $self->submit_api_request( $post_body ); +} + +=head2 submit_tokenize + +Bambora tokenization is based on the Payment Profile feature of their API. + +The token created by this method represnets the Bambora customer_code for the +Payment Profile. The token resembles a credit card number. It is 16 digits +long, beginning with 99. No valid card number can begin with the digits 99. + +This method creates the payment profile, then replaces the customer_code +generated by Bambora with the card number resembling token. + +=cut + +sub submit_tokenize { + my $self = shift; + my $content = $self->{_content}; + + # Check if given card number is already a bambora customer_code + # under this module's token rules + croak "card_number is already tokenized" + if $content->{card_number} =~ /^99\d{14}$/; + + my %post = ( + customer_code => $self->generate_token, + card => $self->jhref_card, + billing => $self->jhref_billing_address, + validate => 0, + ); + + # jhref_card may have generated an exception + return if $self->error_message; + + $self->path('/v1/profiles'); + + my $post_body = encode_json( \%post ); + if ( $DEBUG ) { + warn Dumper({ + path => $self->path, + post_body => $post_body, + post_href => \%post, + }); + } + + my $response = $self->submit_api_request( $post_body ); + if ( $DEBUG ) { + warn Dumper({ + response => $response, + is_success => $self->is_success, + error_message => $self->error_message, + }); + } + return unless $self->is_success; + + my $customer_code = $response->{customer_code}; + if ( !$customer_code ) { + # Should not happen... + # API reported success codes, but + # customer_code value is missing + $self->error_message( + "Fatal error: API reported success, but did not return customer_code" + ); + return $self->is_success(0); + } + + if ( $customer_code ne $post{customer_code} ) { + # Should not happen... + # API reported success codes, but + # customer_code attached to created profiles does not match + # the token value we attempted to assign to the customer profile + $self->error_message( + "Fatal error: API failed to set payment profile customer_code value" + ); + return $self->is_success(0); + } + + $self->card_token( $customer_code ); + + return $response; } -=head2 submit_api_request json_string + + +=head2 submit_api_request json_string [ POST | PUT ] Make the appropriate API request with the given JSON string @@ -282,10 +381,14 @@ Make the appropriate API request with the given JSON string sub submit_api_request { my $self = shift; + my $post_body = shift or die 'submit_api_request() requires a json_string parameter'; - my ( $response_body, $response_code, %response_headers ) = $self->https_post( + # Default to using https_post, unless PUT has been specified + my $http_method = ( $_[0] && lc $_[0] eq 'put' ) ? 'https_put' : 'https_post'; + + my ($response_body, $response_code, %response_headers) = $self->$http_method( { headers => { $self->authorization_header }, 'Content-Type' => 'application/json', @@ -308,7 +411,7 @@ sub submit_api_request { }); } - # API should always return a JSON response, likely network problem + # API should always return a JSON response if ( $@ || !$response ) { $self->error_message( $response_body || 'connection error' ); $self->is_success( 0 ); @@ -333,7 +436,6 @@ sub submit_api_request { # Return the decoded json of the response back to handler $self->is_success( 1 ); return $response; - } =head2 submit_action_unsupported @@ -389,7 +491,7 @@ sub jhref_billing_address { my $content = $self->{_content}; - return { + return +{ name => $self->truncate( $content->{name}, 64 ), address_line1 => $self->truncate( $content->{address}, 64 ), city => $self->truncate( $content->{city}, 64 ), @@ -401,6 +503,76 @@ sub jhref_billing_address { }; } +=head2 jhref_card + +Return a hashref for inclusin into a json object +representing Card for the API + +If necessary values are missing from %content, will set +error_message and is_success + +=cut + +sub jhref_card { + my $self = shift; + my $content = $self->{_content}; + + $self->set_expiration; + + # Check required input + for my $f (qw/ + card_number + owner + expiry_month + expiry_year + cvv2 + /) { + next if $content->{$f}; + + $self->error_message( + "Cannot parse card payment - missing required content $f" + ); + + warn $self->error_message if $DEBUG; + $self->is_success( 0 ); + + return {}; + } + + return +{ + number => $self->truncate( $content->{card_number}, 20 ), + name => $self->truncate( $content->{owner}, 64 ), + expiry_month => sprintf( '%02d', $content->{expiry_month} ), + expiry_year => sprintf( '%02d', $content->{expiry_year} ), + cvd => $content->{cvv2}, + } +} + +=head2 generate_token + +Generate a 16-digit numeric token, beginning with the digits 99, +based on the current epoch time + +Implementation note: + +If this module is somehow used to tokenize multiple cardholders within +the same microsecond, these cardholders will be assigned the same +customer_code. In the unlikely event this does happen, the Bambora system +will decline to process cards for either of the profiles with a duplicate +customer_code. + +=cut + +sub generate_token { + my $self = shift; + my $time = Time::HiRes::time(); + + $time =~ s/\D//g; + $time = substr($time, 0, 14 ); # Eventually time() will contain 15 digits + + "99$time"; +} + =head2 set_country Country is expected to be set as an ISO-3166-1 2-letter country code @@ -410,7 +582,7 @@ Sets string to upper case. Dies unless country is a two-letter string. Could be extended to convert country names to their respective -country codes +country codes, or validate country codes See: L @@ -542,5 +714,40 @@ sub truncate { truncate_egc( "$string", $bytes, '' ); } +=head2 https_put { headers => \%headers }, post_body + +Implement a limited interface of https_get from Net::HTTPS::Any +for PUT instead of POST -- only implementing current use case of +submitting a JSON request body + +Todo: Properly implement https_put in Net::HTTPS::Any + +=cut + +sub https_put { + my ( $self, $args, $post_body ) = @_; + + my $ua = LWP::UserAgent->new; + + my %headers = %{ $args->{headers} } if ref $args->{headers}; + for my $k ( keys %headers ) { + $ua->default_header( $k => $headers{$k} ); + } + + my $url = $self->server().$self->path(); + my $res = $ua->put( $url, Content => $post_body ); + + $self->build_subs(qw/ response_page response_code response_headers/); + + my @response_headers = + map { $_ => $res->header( $_ ) } + $res->header_field_names; + + $self->response_headers( {@response_headers} ); + $self->response_code( $res->code ); + $self->response_page( $res->decoded_content ); + + ( $self->response_page, $self->response_code, @response_headers ); +} 1; diff --git a/t/021-generate_token.t b/t/021-generate_token.t new file mode 100755 index 0000000..14d38c1 --- /dev/null +++ b/t/021-generate_token.t @@ -0,0 +1,15 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +use lib 't'; +use Business::OnlinePayment; + +my $tr; +ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiatiate $tr' ); + +my $token; +ok( $token = $tr->generate_token, "\$tr->generate_token: $token" ); +ok( $token =~ /^99\d{14}$/, 'Token matches expected format' ); +done_testing; \ No newline at end of file diff --git a/t/021-payments-card-normal_authorization.t b/t/021-payments-card-normal_authorization.t deleted file mode 100755 index f4eb89b..0000000 --- a/t/021-payments-card-normal_authorization.t +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; -use Test::More; - -use lib 't'; -require 'TestFixtures.pm'; -use Business::OnlinePayment; - -my $merchant_id = $ENV{BAMBORA_MERCHANT_ID}; -my $api_key = $ENV{BAMBORA_API_KEY}; - -SKIP: { - skip 'Missing env vars BAMBORA_MERCHANT_ID and BAMBORA_API_KEY', 78 - unless $merchant_id && $api_key; - - my %content = ( - login => $merchant_id, - password => $api_key, - action => 'Normal Authorization', - amount => '9.99', - - owner => 'Freeside Internet Services', - name => 'Mitch Jackson', - address => '1407 Graymalkin Lane', - city => 'Vancouver', - state => 'BC', - zip => '111 111', - country => 'CA', - - card_number => '4242424242424242', - cvv2 => '111', - expiration => '1122', - phone => '251-300-1300', - email => 'mitch@freeside.biz', - ); - - # Test approved card numbers, - # ref: https://dev.na.bambora.com/docs/references/payment_APIs/test_cards/ - my %approved_cards = ( - visa => { card => '4030000010001234', cvv2 => '123' }, - mastercard => { card => '5100000010001004', cvv2 => '123' }, - mastercard2 => { card => '2223000048400011', cvv2 => '123' }, - amex => { card => '371100001000131', cvv2 => '1234' }, - visa => { card => '4030000010001234', cvv2 => '123' }, - discover => { card => '6011500080009080', cvv2 => '123' }, - ); - - for my $name ( keys %approved_cards ) { - $content{card_number} = $approved_cards{$name}->{card}; - $content{cvv2} = $approved_cards{$name}->{cvv2}; - - my $tr; - ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiatiate $tr' ); - ok( $tr->content( %content ), 'Set transaction content onto $tr' ); - { - local $@; - eval { $tr->submit }; - ok( !$@, "$name Process transaction (expect approve)" ); - } - - for my $attr (qw/ - is_success - message_id - authorization - order_number - txn_date - avs_code - /) { - ok( - defined $tr->$attr(), - sprintf '%s $tr->%s() = %s', - $name, - $attr, - $tr->$attr() - ); - } - } - - # Test declined card numbers, - # ref: https://dev.na.bambora.com/docs/references/payment_APIs/test_cards/ - my %decline_cards = ( - visa => { card => '4003050500040005', cvv2 => '123' }, - mastercard => { card => '5100000020002000', cvv2 => '123' }, - amex => { card => '342400001000180', cvv2 => '1234' }, - discover => { card => '6011000900901111', cvv2 => '123' }, - ); - for my $name ( keys %decline_cards ) { - $content{card_number} = $decline_cards{$name}->{card}; - $content{cvv2} = $decline_cards{$name}->{cvv2}; - - my $tr; - ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiate $tr' ); - ok( $tr->content( %content ), 'Set transaction content onto $tr' ); - { - local $@; - eval { $tr->submit }; - ok( !$@, "$name: Process transaction (expect decline)" ); - } - - ok( $tr->is_success == 0, '$tr->is_success == 0' ); - ok( $tr->result_code != 1, '$tr->result_code != 1' ); - ok( $tr->error_message, '$tr->error_message: '.$tr->error_message ); - } -} - -done_testing; \ No newline at end of file diff --git a/t/022-payments-card-pre-authorization-complete-void.t b/t/022-payments-card-pre-authorization-complete-void.t deleted file mode 100755 index 00c25f9..0000000 --- a/t/022-payments-card-pre-authorization-complete-void.t +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; -use Test::More; - -use lib 't'; -require 'TestFixtures.pm'; -use Business::OnlinePayment; - -my $merchant_id = $ENV{BAMBORA_MERCHANT_ID}; -my $api_key = $ENV{BAMBORA_API_KEY}; - -SKIP: { - skip 'Missing env vars BAMBORA_MERCHANT_ID and BAMBORA_API_KEY', 3 - unless $merchant_id && $api_key; - - my %content = ( - login => $merchant_id, - password => $api_key, - action => 'Authorization Only', - amount => '9.99', - - owner => 'Freeside Internet Services', - name => 'Mitch Jackson', - address => '1407 Graymalkin Lane', - city => 'Vancouver', - state => 'BC', - zip => '111 111', - country => 'CA', - - invoice_number => time(), - card_number => '4030000010001234', - cvv2 => '123', - expiration => '1122', - phone => '251-300-1300', - email => 'mitch@freeside.biz', - ); - - my $tr; - ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiatiate $tr' ); - ok( $tr->content( %content ), 'Set transaction content onto $tr' ); - { - local $@; - eval { $tr->submit }; - ok( !$@, "Submit pre-auth (expect approve)" ); - } - - my $response; - my %expect = ( - amount => '9.99', - approved => 1, - auth_code => 'TEST', - message => 'Approved', - message_id => 1, - payment_method => 'CC', - type => 'PA', - ); - my @expect = qw( - card - created - order_number - risk_score - id - ); - - ok( $response = $tr->response_decoded, 'response_decoded' ); - - for my $k ( keys %expect ) { - ok( - $response->{$k} eq $expect{$k}, - sprintf '$tr->%s == %s', $k, $expect{$k} - ); - } - - for my $k ( @expect ) { - ok( - defined $response->{$k}, - sprintf '$r->%s (%s)', - $k, $response->{$k} - ); - } - - %content = ( - %content, - action => 'Post Authorization', - order_number => $tr->order_number, - amount => '8.99', # $1 Less than pre-auth - ); - - my $tr_pa; - ok( $tr_pa = Business::OnlinePayment->new('Bambora'), 'Instantiate $tr_pa' ); - ok( $tr_pa->content( %content ), 'Set transaction content onto $tr_pa' ); - { - local $@; - eval { $tr_pa->submit }; - ok( !$@, "Submit post-auth" ); - warn "Error: $@" if $@; - } - - %expect = ( - amount => '8.99', - approved => '1', - message => 'Approved', - message_id => '1', - type => 'PAC', - ); - @expect = (qw/ - authorizing_merchant_id - card - created - order_number - id - /); - - my $response_pa; - ok( $response_pa = $tr_pa->response_decoded, 'response_decoded' ); - - for my $k ( keys %expect ) { - ok( - $response_pa->{$k} eq $expect{$k}, - sprintf '$tr->%s == %s', $k, $expect{$k} - ); - } - - for my $k ( @expect ) { - ok( - defined $response_pa->{$k}, - sprintf '$r->%s (%s)', - $k, $response_pa->{$k} - ); - } - - # - # Void Transaction - # - - my %content_void = ( - action => 'Void', - login => $content{login}, - password => $content{password}, - order_number => $tr_pa->order_number, - amount => '8.99', - ); - - my $tr_void; - ok( $tr_void = Business::OnlinePayment->new('Bambora'), 'Instantiate $tr_void' ); - ok( $tr_void->content( %content_void ), 'Set transaction content onto $tr_void' ); - { - local $@; - eval { $tr_void->submit }; - ok( !$@, "Submit void" ); - warn "Error: $@" if $@; - } - - %expect = ( - amount => '8.99', - approved => '1', - message => 'Approved', - message_id => '1', - type => 'R', - ); - @expect = (qw/ - authorizing_merchant_id - card - created - order_number - id - /); - -} - -done_testing; \ No newline at end of file diff --git a/t/031-payments-card-normal_authorization.t b/t/031-payments-card-normal_authorization.t new file mode 100755 index 0000000..f4eb89b --- /dev/null +++ b/t/031-payments-card-normal_authorization.t @@ -0,0 +1,107 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +use lib 't'; +require 'TestFixtures.pm'; +use Business::OnlinePayment; + +my $merchant_id = $ENV{BAMBORA_MERCHANT_ID}; +my $api_key = $ENV{BAMBORA_API_KEY}; + +SKIP: { + skip 'Missing env vars BAMBORA_MERCHANT_ID and BAMBORA_API_KEY', 78 + unless $merchant_id && $api_key; + + my %content = ( + login => $merchant_id, + password => $api_key, + action => 'Normal Authorization', + amount => '9.99', + + owner => 'Freeside Internet Services', + name => 'Mitch Jackson', + address => '1407 Graymalkin Lane', + city => 'Vancouver', + state => 'BC', + zip => '111 111', + country => 'CA', + + card_number => '4242424242424242', + cvv2 => '111', + expiration => '1122', + phone => '251-300-1300', + email => 'mitch@freeside.biz', + ); + + # Test approved card numbers, + # ref: https://dev.na.bambora.com/docs/references/payment_APIs/test_cards/ + my %approved_cards = ( + visa => { card => '4030000010001234', cvv2 => '123' }, + mastercard => { card => '5100000010001004', cvv2 => '123' }, + mastercard2 => { card => '2223000048400011', cvv2 => '123' }, + amex => { card => '371100001000131', cvv2 => '1234' }, + visa => { card => '4030000010001234', cvv2 => '123' }, + discover => { card => '6011500080009080', cvv2 => '123' }, + ); + + for my $name ( keys %approved_cards ) { + $content{card_number} = $approved_cards{$name}->{card}; + $content{cvv2} = $approved_cards{$name}->{cvv2}; + + my $tr; + ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiatiate $tr' ); + ok( $tr->content( %content ), 'Set transaction content onto $tr' ); + { + local $@; + eval { $tr->submit }; + ok( !$@, "$name Process transaction (expect approve)" ); + } + + for my $attr (qw/ + is_success + message_id + authorization + order_number + txn_date + avs_code + /) { + ok( + defined $tr->$attr(), + sprintf '%s $tr->%s() = %s', + $name, + $attr, + $tr->$attr() + ); + } + } + + # Test declined card numbers, + # ref: https://dev.na.bambora.com/docs/references/payment_APIs/test_cards/ + my %decline_cards = ( + visa => { card => '4003050500040005', cvv2 => '123' }, + mastercard => { card => '5100000020002000', cvv2 => '123' }, + amex => { card => '342400001000180', cvv2 => '1234' }, + discover => { card => '6011000900901111', cvv2 => '123' }, + ); + for my $name ( keys %decline_cards ) { + $content{card_number} = $decline_cards{$name}->{card}; + $content{cvv2} = $decline_cards{$name}->{cvv2}; + + my $tr; + ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiate $tr' ); + ok( $tr->content( %content ), 'Set transaction content onto $tr' ); + { + local $@; + eval { $tr->submit }; + ok( !$@, "$name: Process transaction (expect decline)" ); + } + + ok( $tr->is_success == 0, '$tr->is_success == 0' ); + ok( $tr->result_code != 1, '$tr->result_code != 1' ); + ok( $tr->error_message, '$tr->error_message: '.$tr->error_message ); + } +} + +done_testing; \ No newline at end of file diff --git a/t/032-payments-card-pre-authorization-complete-void.t b/t/032-payments-card-pre-authorization-complete-void.t new file mode 100755 index 0000000..7c575c9 --- /dev/null +++ b/t/032-payments-card-pre-authorization-complete-void.t @@ -0,0 +1,172 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +use lib 't'; +require 'TestFixtures.pm'; +use Business::OnlinePayment; + +my $merchant_id = $ENV{BAMBORA_MERCHANT_ID}; +my $api_key = $ENV{BAMBORA_API_KEY}; + +SKIP: { + skip 'Missing env vars BAMBORA_MERCHANT_ID and BAMBORA_API_KEY', 32 + unless $merchant_id && $api_key; + + my %content = ( + login => $merchant_id, + password => $api_key, + action => 'Authorization Only', + amount => '9.99', + + owner => 'Freeside Internet Services', + name => 'Mitch Jackson', + address => '1407 Graymalkin Lane', + city => 'Vancouver', + state => 'BC', + zip => '111 111', + country => 'CA', + + invoice_number => time(), + card_number => '4030000010001234', + cvv2 => '123', + expiration => '1122', + phone => '251-300-1300', + email => 'mitch@freeside.biz', + ); + + my $tr; + ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiatiate $tr' ); + ok( $tr->content( %content ), 'Set transaction content onto $tr' ); + { + local $@; + eval { $tr->submit }; + ok( !$@, "Submit pre-auth (expect approve)" ); + } + + my $response; + my %expect = ( + amount => '9.99', + approved => 1, + auth_code => 'TEST', + message => 'Approved', + message_id => 1, + payment_method => 'CC', + type => 'PA', + ); + my @expect = qw( + card + created + order_number + risk_score + id + ); + + ok( $response = $tr->response_decoded, 'response_decoded' ); + + for my $k ( keys %expect ) { + ok( + $response->{$k} eq $expect{$k}, + sprintf '$tr->%s == %s', $k, $expect{$k} + ); + } + + for my $k ( @expect ) { + ok( + defined $response->{$k}, + sprintf '$r->%s (%s)', + $k, $response->{$k} + ); + } + + %content = ( + %content, + action => 'Post Authorization', + order_number => $tr->order_number, + amount => '8.99', # $1 Less than pre-auth + ); + + my $tr_pa; + ok( $tr_pa = Business::OnlinePayment->new('Bambora'), 'Instantiate $tr_pa' ); + ok( $tr_pa->content( %content ), 'Set transaction content onto $tr_pa' ); + { + local $@; + eval { $tr_pa->submit }; + ok( !$@, "Submit post-auth" ); + warn "Error: $@" if $@; + } + + %expect = ( + amount => '8.99', + approved => '1', + message => 'Approved', + message_id => '1', + type => 'PAC', + ); + @expect = (qw/ + authorizing_merchant_id + card + created + order_number + id + /); + + my $response_pa; + ok( $response_pa = $tr_pa->response_decoded, 'response_decoded' ); + + for my $k ( keys %expect ) { + ok( + $response_pa->{$k} eq $expect{$k}, + sprintf '$tr->%s == %s', $k, $expect{$k} + ); + } + + for my $k ( @expect ) { + ok( + defined $response_pa->{$k}, + sprintf '$r->%s (%s)', + $k, $response_pa->{$k} + ); + } + + # + # Void Transaction + # + + my %content_void = ( + action => 'Void', + login => $content{login}, + password => $content{password}, + order_number => $tr_pa->order_number, + amount => '8.99', + ); + + my $tr_void; + ok( $tr_void = Business::OnlinePayment->new('Bambora'), 'Instantiate $tr_void' ); + ok( $tr_void->content( %content_void ), 'Set transaction content onto $tr_void' ); + { + local $@; + eval { $tr_void->submit }; + ok( !$@, "Submit void" ); + warn "Error: $@" if $@; + } + + %expect = ( + amount => '8.99', + approved => '1', + message => 'Approved', + message_id => '1', + type => 'R', + ); + @expect = (qw/ + authorizing_merchant_id + card + created + order_number + id + /); + +} + +done_testing; \ No newline at end of file diff --git a/t/041-tokenize-card.t b/t/041-tokenize-card.t new file mode 100755 index 0000000..f8a1292 --- /dev/null +++ b/t/041-tokenize-card.t @@ -0,0 +1,82 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Test::More; + +use lib 't'; +require 'TestFixtures.pm'; +use Business::OnlinePayment; + +my $merchant_id = $ENV{BAMBORA_MERCHANT_ID}; +my $api_key = $ENV{BAMBORA_API_KEY}; + +SKIP: { + skip 'Missing env vars BAMBORA_MERCHANT_ID and BAMBORA_API_KEY', 32 + unless $merchant_id && $api_key; + + my %content = ( + login => $merchant_id, + password => $api_key, + action => 'Tokenize', + amount => '9.99', + + owner => 'Freeside Internet', + name => 'Mitch Jackson', + address => '1407 Graymalkin Lane', + city => 'Vancouver', + state => 'BC', + zip => '111 111', + country => 'CA', + + invoice_number => time(), + card_number => '4030000010001234', + cvv2 => '123', + expiration => '1122', + phone => '251-300-1300', + email => 'mitch@freeside.biz', + ); + + my $tr; + ok( $tr = Business::OnlinePayment->new('Bambora'), 'Instantiatiate $tr' ); + ok( $tr->content( %content ), 'Set transaction content onto $tr' ); + { + local $@; + eval { $tr->submit }; + ok( !$@, "Submit request to create Payment Profile (tokenize)" ); + } + + my $response; + + my %expect = ( + code => 1, + message => 'Operation Successful', + ); + my @expect = qw( + customer_code + ); + + ok( $response = $tr->response_decoded, 'response_decoded' ); + + for my $k ( keys %expect ) { + ok( + $response->{$k} eq $expect{$k}, + sprintf '$tr->%s == %s', $k, $expect{$k} + ); + } + + for my $k ( @expect ) { + ok( + defined $response->{$k}, + sprintf '$r->%s (%s)', + $k, $response->{$k} + ); + } + + ok( + $response->{customer_code} eq $tr->card_token, + '$tr->card_token eq $response->{customer_code}' + ); + +} + +done_testing; \ No newline at end of file diff --git a/t/junk.t b/t/junk.t new file mode 100755 index 0000000..e69de29