From 4eac3eefba3e494cc771d43df1cd3fa3aaece916 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 12 Apr 2019 17:39:38 -0400 Subject: [PATCH] Adding post-auth, void --- lib/Business/OnlinePayment/Bambora.pm | 187 ++++++++++++++++++++++++++----- t/022-payments-card-authorization_only.t | 107 ------------------ t/022-payments-card-pre-authorization.t | 122 ++++++++++++++++++++ 3 files changed, 280 insertions(+), 136 deletions(-) delete mode 100755 t/022-payments-card-authorization_only.t create mode 100644 t/022-payments-card-pre-authorization.t diff --git a/lib/Business/OnlinePayment/Bambora.pm b/lib/Business/OnlinePayment/Bambora.pm index a61cda0..c721838 100755 --- a/lib/Business/OnlinePayment/Bambora.pm +++ b/lib/Business/OnlinePayment/Bambora.pm @@ -13,7 +13,7 @@ use URI::Escape; use vars qw/ $VERSION $DEBUG /; $VERSION = '0.01'; -$DEBUG = 0; +$DEBUG = 1; if ( $DEBUG ) { $Data::Dumper::Sortkeys = 1; @@ -50,7 +50,7 @@ sub set_defaults { =head2 submit -Dispatch to the appropriate hanlder based on the given action +Dispatch to the appropriate handler based on the given action =cut @@ -58,7 +58,7 @@ my %action_dispatch_table = ( 'normal authorization' => 'submit_normal_authorization', 'authorization only' => 'submit_authorization_only', 'post authorization' => 'submit_post_authorization', - 'reverse authorization' => 'rsubmit_everse_authorization', + 'reverse authorization' => 'submit_reverse_authorization', 'void' => 'submit_viod', 'credit' => 'submit_credit', 'tokenize' => 'submit_tokenize', @@ -91,37 +91,90 @@ See L sub submit_normal_authorization { my $self = shift; - - # Series of methods to populate or format field values - $self->make_invoice_number; - $self->set_payment_method; - $self->set_expiration; - my $content = $self->{_content}; - # Build a JSON string - my $post_body = encode_json({ - order_number => $self->truncate( $content->{invoice_number}, 30 ), - amount => $content->{amount}, - payment_method => $content->{payment_method}, + # Use epoch time as invoice_number, if none is specified + $content->{invoice_number} ||= time(); + + # Clarifying Bambora API and Business::OnlinePayment naming conflict + # + # Bambora API: + # - order_number: user supplied identifier for the order, displayed on reports + # - transaction_id: bambora supplied identifier for the order. + # this number must be referenced for future actions like voids, + # auth captures, etc + # + # Business::OnlinePayment + # - invoice_number: contains the bambora order number + # - order_number: contains the bambora transaction id + + my %post = ( + order_number => $self->truncate( $content->{invoice_number}, 30 ), + amount => $content->{amount}, + billing => $self->jhref_billing_address, + ); - billing => $self->jhref_billing_address, + # Credit Card + if ( $content->{card_number} ) { + $post{payment_method} = 'card'; - card => { + # Parse the expiration date into expiry_month and expiry_year + $self->set_expiration; + + $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!'; + } + + my $action = lc $content->{action}; + if ( $action eq 'normal authorization' ) { + $self->path('/v1/payments'); + } elsif ( $action eq 'authorization only' ) { + $self->path('/v1/payments'); + if ( ref $post{card} ) { + $post{card}->{complete} = 0; } - }); + } elsif ( $action eq 'post authorization' ) { + croak 'post authorization cannot be completed - '. + 'bambora transaction_id must be set as order_number '. + 'before using submit()' + unless $content->{order_number}; + + $self->path( + sprintf 'v1/payments/%s/completions', + $content->{order_number} + ); + + if ( ref $post{card} ) { + $post{card}->{complete} = 1 + } + } else { + die "unsupported action $action"; + } + + # Parse %post into a JSON string, to be attached to the request POST body + my $post_body = encode_json( \%post ); + if ( $DEBUG ) { - warn Dumper({ post_body => $post_body })."\n"; + warn Dumper({ + post_body => $post_body, + post_href => \%post, + }); } + $self->path('/v1/payments'); + my $response = $self->submit_api_request( $post_body ); # Error messages already populated upon failure @@ -134,6 +187,93 @@ sub submit_normal_authorization { $self->txn_date( $response->{created} ); $self->avs_code( $response->{card}{avs_result} ); $self->is_success( 1 ); + + $response; +} + +=head2 submit_authorization_only + +Capture a card authorization, but do not complete transaction + +=cut + +sub submit_authorization_only { + my $self = shift; + + $self->submit_normal_authorization; + + my $response = $self->response_decoded; + + if ( + $self->is_success + && ( + ref $response + && $response->{type} != 'PA' + ) + ) { + # Bambora API uses nearly identical API calls for normal + # card transactions and pre-authorization. Sanity check + # that response reported a pre-authorization code + die "Expected API Respose type=PA, but type=$response->{type}! ". + "Pre-Authorization attempt may have charged card!"; + } +} + +=head2 submit_post_authorization + +Complete a card pre-authorization + +=cut + +sub submit_post_authorization { + shift->submit_normal_authorization; +} + +=head2 submit_reverse_authorization + +Reverse a pre-authorization + +=cut + +sub submit_reverse_authorization { + shift->submit_void; +} + +=head2 submit_void + +Void a transaction + +=cut + +sub submit_void { + my $self = shift; + my $content = $self->{_content}; + + for my $f (qw/ order_number invoice_number amount/) { + unless ( $content->{$f} ) { + $self->error_message("Cannot process void - missing required content $f"); + warn $self->error_message if $DEBUG; + + return $self->is_success(0); + } + } + + my %post = ( + order_number => $self->truncate( $content->{invoice_number}, 30 ), + amount => $content->{amount}, + ); + my $post_body = encode_json( \%post ); + + if ( $DEBUG ) { + warn Dumper({ + post => \%post, + post_body => $post_body, + }); + } + $self->path( sprintf '/v1/payments/%s/void', $content->{order_number} ); + + my $response = $self->submit_api_request( $post_body ); + } =head2 submit_api_request json_string @@ -263,17 +403,6 @@ sub jhref_billing_address { }; } -=head2 make_invoice_number - -If an invoice number has not been specified, generate one using -the current epoch timestamp - -=cut - -sub make_invoice_number { - shift->{_content}{invoice_number} ||= time(); -} - =head2 set_country Country is expected to be set as an ISO-3166-1 2-letter country code diff --git a/t/022-payments-card-authorization_only.t b/t/022-payments-card-authorization_only.t deleted file mode 100755 index c95bdb8..0000000 --- a/t/022-payments-card-authorization_only.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/ - message_id - authorization - order_number - txn_date - avs_code - is_success - /) { - 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.t b/t/022-payments-card-pre-authorization.t new file mode 100644 index 0000000..acceee5 --- /dev/null +++ b/t/022-payments-card-pre-authorization.t @@ -0,0 +1,122 @@ +#!/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 + ); + + 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, + ); + + my $tr_pa; + ok( $tr_pa = Business::OnlinePayment->new('Bambora'), 'Instantiate $tr_pa' ); + ok( $tr->content( %content ), 'Set transaction content onto $tr_pa' ); + { + local $@; + eval { $tr_pa->submit }; + ok( !$@, "Submit post-auth" ); + warn "Error: $@" if $@; + } + + my $response_pa; + + + ok( $response_pa = $tr_pa->response_decoded, 'response_decoded' ); + + # for my $attr (qw/ + # message_id + # authorization + # order_number + # txn_date + # avs_code + # is_success + # /) { + # ok( + # defined $tr->$attr(), + # sprintf '%s $tr->%s() = %s', + # $name, + # $attr, + # $tr->$attr() + # ); + # } + +} + +done_testing; \ No newline at end of file -- 2.11.0