[interchange] Vend::Payment::Braintree payment module

Mark Johnson interchange-cvs at icdevgroup.org
Fri Nov 3 16:49:10 UTC 2017


commit f70ac0d03c25da276b39a9dfda027d7f30686115
Author: Mark Johnson <mark at endpoint.com>
Date:   Fri Nov 3 12:46:59 2017 -0400

    Vend::Payment::Braintree payment module
    
    Provides Interchange payment module to wrap Net::Braintree CPAN module
    suite for supporting modern Braintree Perl implementation.
    
    See POD for documentation.

 lib/Vend/Payment/Braintree.pm |  915 +++++++++++++++++++++++++++++++++++++++++
 1 files changed, 915 insertions(+), 0 deletions(-)
---
diff --git a/lib/Vend/Payment/Braintree.pm b/lib/Vend/Payment/Braintree.pm
new file mode 100644
index 0000000..96f6861
--- /dev/null
+++ b/lib/Vend/Payment/Braintree.pm
@@ -0,0 +1,915 @@
+package Vend::Payment::Braintree;
+
+=head1 NAME
+
+Vend::Payment::Braintree - Interchange Braintree support
+
+=head1 SYNOPSIS
+
+    &charge=braintree
+ 
+        or
+ 
+    [charge mode=braintree param1=value1 param2=value2]
+
+=head1 PREREQUISITES
+
+  Net::Braintree
+
+=head1 DESCRIPTION
+
+The Vend::Payment::Braintree module implements the braintree() routine
+for use with Interchange. It is compatible on a call level with the other
+Interchange payment modules.
+
+To enable this module, place this directive in C<interchange.cfg>:
+
+    Require module Vend::Payment::Braintree
+
+This I<must> be in interchange.cfg or a file included from it.
+
+The mode can be named anything, but the C<gateway> parameter must be set to
+C<braintree>. To make it the default payment gateway for all credit card
+transactions in a specific catalog, based on demo catalog settings, you can
+set in C<catalog.cfg>:
+
+    Variable   MV_PAYMENT_MODE  braintree
+
+It uses several of the standard settings from Interchange payment. Those with
+an asterisk are required. The active settings are:
+
+=over 4
+
+=item merchant_id*
+
+Supplied from Braintree
+
+=item public_key*
+
+Supplied from Braintree
+
+=item private_key*
+
+Supplied from Braintree
+
+=item environment*
+
+One of B<sandbox>, B<integration>, B<development>, B<qa>, or B<production>.
+Use C<production> for live transaction processing, and C<sandbox> for testing
+and development.
+
+=item transaction
+
+As Braintree is the PayPal successor to Payflow Pro, the transaction
+identifiers were patterned off of PFP's. The traditional Interchange
+identifiers for transactions are also supported:
+
+    Interchange         Braintree
+    ----------------    -----------------
+        auth            A
+        return          C
+        sale            S
+        settle          D
+        void            V
+
+Default transaction is C<A>.
+
+There are additional Braintree transactions specific to the particular gateway's API:
+
+=over 4
+
+=item mini_auth, M
+
+Use when a validation authorization is desired. The C<M> transaction will return
+any authorization error that may occur on the card along with AVS and CVC
+validation.
+
+The transaction ID in response to a C<M> is a C<payment_method_token>. This
+will have to be used for subsequent transaction activity as the Braintree
+nonce can only be used once. This is the scalar response from $Tag->charge or
+the value found in [data session payment_id].
+
+=item find, F
+
+Use to find the current details of a transaction in Braintree. Particularly
+useful for issuing refunds where, depending on status, Braintree requires
+either voiding the authorization or running a credit against the settlement.
+
+=item client_token, T
+
+Server-side interface for generating and returning the client token needed by
+front-end integration for interacting with Braintree. Token is returned as the
+transaction ID, i.e., scalar response from $Tag->charge or value found in
+[data session payment_id].
+
+=back
+
+=item payment_method_nonce
+
+Token supplied from Braintree frontend integration that references a specific
+payment instrument. The nonce is only capable of a single use. To acquire a
+permanent token to the payment instrument, issue a C<M> transaction and store
+the resulting transaction ID for use as a C<payment_method_token>
+
+=item payment_method_token
+
+Permanent token from Braintree vault for accessing the associated payment
+instrument. If both a C<payment_method_nonce> and C<payment_method_token> are
+present in the request, the token is preferred.
+
+=item comment1
+
+For compatibility with PFP for passing in the Interchange order number, or
+other local order identifier, to Braintree to pair with the transactions.
+
+=item order_id
+
+Transaction ID for follow-on transaction. E.g., transaction ID from an
+authorization to submit a capture request.
+
+=item check_sub
+
+Name of a global or catalog subroutine to post-process the raw result from the transaction type as received from Net::Braintree.
+
+Subroutine is provided 2 arguments:
+
+=over 4
+
+=item $result
+
+Hash reference to the full return structure from Net::Braintree. The structure
+will depend on which transaction type was run and the result of the
+transaction. So dissecting the first argument will require examining the arg
+structure itself, Net::Braintree code, and Braintree documentation.
+
+=item $transaction
+
+Canonical transaction type identifer. Will be one of C<A>, C<C>, C<D>, C<F>,
+C<M>, C<S>, C<T>, or C<V>.
+
+=back
+
+Subroutine should return perly true or false, to indiciate if the result of
+the transaction should be processed as success or failure.
+
+=item test
+
+Does not support a test identifier like most Interchange payment modules.
+Control whether running tests or live transactions by the C<environment>
+setting.
+
+=back
+
+=head2 Fraud detection parameters
+
+There are a number of settings associated with additional Braintree services
+for fraud detection. Currently implemented:
+
+=over 4
+
+=item *
+custom_fields
+
+=item *
+device_data
+
+=item *
+merchant_account_id
+
+=item *
+skip_advanced_fraud_checking
+
+=back
+
+Fraud results, when present, will be found in the RISK_DATA key of the results
+hash.
+
+See Braintree documentation for details on fraud-detection services.
+
+=head2 Credit card data
+
+Braintree shields sensitive card-holder data from the merchant to avoid higher
+levels of PCI burden.  However, that necessarily means the usual payment
+information is unavailable from any user-submitted forms as is typical for
+Interchange implementations.
+
+The card-holder data Braintree does make available after transaction
+processing can be found in the CARD_DATA key of the results hash. The hash can
+be accessed like a standard hash or, because it's a Hash::Inflator object, can
+have its keys accessed via method calls.
+
+A typical CARD_DATA hash might look like:
+
+ 'CARD_DATA' => {
+   'bin' => '411111',
+   'card_type' => 'Visa',
+   'cardholder_name' => 'Gimmy Giblets',
+   'commercial' => 'Unknown',
+   'country_of_issuance' => 'USA',
+   'customer_location' => 'US',
+   'debit' => 'Yes',
+   'durbin_regulated' => 'Yes',
+   'expiration_month' => '08',
+   'expiration_year' => '2021',
+   'healthcare' => 'No',
+   'issuing_bank' => 'Bank of America, National Association',
+   'last_4' => '1111',
+   'payroll' => 'No',
+   'prepaid' => 'No',
+   'product_id' => 'F',
+   'token' => 'foobar',
+   'unique_number_identifier' => 'xxxxxxxxxx80f47de7486c7809ea21e0'
+ },
+
+The exact composition will depend on a number of factors determined by
+Braintree and the specific payment instrument. But the most important
+keys--those identifying bin, last 4, cardtype, expiration--should always be
+present.
+
+=head2 Troubleshooting
+
+=over 4
+
+=item *
+
+Make sure you "Require"d the module in interchange.cfg:
+
+    Require module Vend::Payment::Braintree
+
+=item *
+
+Make sure Net::Braintree is installed and working. You can test to see whether
+your Perl thinks it is:
+
+    perl -MNet::Braintree -e 'print "$Net::Braintree::VERSION\n"'
+
+If that prints a version number, then the module is installed with the version noted.
+
+=item *
+
+Check the error logs, both catalog and global.
+
+=item *
+
+Make sure you set your required payment parameters properly.
+
+=item *
+
+Try an order, then put this code in a page:
+
+    <XMP>
+    [calc]
+        my $string = $Tag->uneval( { ref => $Session->{payment_result} });
+        $string =~ s/{/{\n/;
+        $string =~ s/,/,\n/g;
+        return $string;
+    [/calc]
+    </XMP>
+
+That should show what happened.
+
+=item *
+
+If all else fails, consultants are available to help with integration for a fee.
+See http://www.icdevgroup.org/ for mailing lists and other information.
+
+=back
+
+=head1 AUTHORS
+
+    Mark Johnson <mark at endpoint.com>
+    Jon Jensen <jon at endpoint.com>
+
+=cut
+
+use strict;
+use warnings;
+
+BEGIN {
+    require Net::Braintree;
+
+    # Redefining the refund() routine here because it is broken
+    # in Net::Braintree::Transaction. Without the override, you cannot
+    # submit additional parameters Braintree suppports.
+    #
+    # The CPAN modules are no longer actively maintained and a patch
+    # to fix this issue was rejected. To make handling Braintree as
+    # simple as possible in Interchange, this local override allows
+    # a direct CPAN install of Net::Braintree to fully function.
+
+    no warnings 'redefine';
+    *Net::Braintree::Transaction::refund = sub {
+        # Follow-on credit
+        my ($class, $id, $amount) = @_;
+        my $params;
+        if (ref $amount) {
+            $params = $amount;
+        }
+        else {
+            $params = {};
+            $params->{'amount'} = $amount if $amount;
+        }
+        $class->gateway->transaction->refund($id, $params);
+    };
+}
+
+::logGlobal('%s module loaded', __PACKAGE__);
+
+*charge_param = \&Vend::Payment::charge_param;
+
+sub get_token {
+    my $route = shift || charge_param('mode');
+    my $opt;
+    if (ref ($route) ) {
+        $opt = $route;
+    }
+    elsif ( not defined($route) && length($route)) {
+        ::logError(__PACKAGE__ . "::get_token requires a route hash or name");
+        return;
+    }
+    else {
+        $opt = $Vend::Cfg->{Route_repository}{$route};
+    }
+    my $config = Net::Braintree->configuration;
+    $config->$_($opt->{$_}) for qw( environment merchant_id public_key private_key );
+    return Net::Braintree::ClientToken->generate;
+}
+
+sub response_map {
+    return qw/
+        order-id       PNREF
+        pop.order-id   PNREF
+        pop.auth-code  AUTHCODE
+        pop.avs_code   AVSZIP
+        pop.avs_zip    AVSZIP
+        pop.avs_addr   AVSADDR
+    /;
+}
+
+sub method_map {
+    local $_ = shift;
+
+    return 'create' if /M/;
+    return 'sale' if /S|A/;
+    return 'submit_for_settlement' if /D/;
+    return 'void' if /V/;
+    return 'refund' if /C/;
+    return 'find' if /F/;
+
+    die "'$_' is an invalid transaction type";
+}
+
+sub scrub_addresses {
+    my $opt = shift;
+
+    for (qw( country b_country )) {
+        $opt->{$_} = 'GB' if $opt->{$_} eq 'UK';
+    }
+
+    # Braintree says postal codes must be no more than 9 alphanumeric characters, with spaces and hyphens ignored
+    # https://developers.braintreepayments.com/reference/request/address/create/ruby#postal_code
+    for (qw( zip b_zip )) {
+        $opt->{$_} =~ s/[^A-Za-z0-9]+//g;
+        $opt->{$_} = substr($opt->{$_}, 0, 9);
+    }
+
+    return;
+}
+
+sub handle_avs {
+    my $E = shift;
+
+    return ('','') if $E;
+
+    my @rv;
+
+    for (@_) {
+        my $v = /M/ ? 'Y' : /N/ ? 'N' : /[UI]/ ? 'X' : '';
+        push @rv, $v;
+    }
+
+    return @rv;
+}
+
+sub gateway_rejection_reason_code {
+    my $reason = shift;
+    # These negative number codes are our arbitrary invention to include gateway rejections in the same result code.
+    # See https://developers.braintreepayments.com/reference/response/credit-card-verification/ruby#gateway_rejection_reason
+    my %map = (
+        avs                    => -101,
+        avs_and_cvv            => -102,
+        cvv                    => -103,
+        duplicate              => -104,
+        fraud                  => -105,
+        three_d_secure         => -106,
+        application_incomplete => -107,
+    );
+    return $map{$reason} || -100;
+}
+
+sub client_token {
+    my ($transtype, $amount, $opt) = @_;
+    my $result;
+    {
+        local $@;
+        $result =
+            eval {
+                no warnings 'redefine';
+                local *Carp::longmess = \&Carp::shortmess
+                    if $opt->{environment} eq 'production';
+                get_token($opt);
+            }
+            or do {
+                my $err = ::errmsg('Unable to retrieve client token: %s', $@ || 'no information from eval block.');
+                ::logError($err);
+                return (
+                    MStatus => 'failure-hard',
+                    MErrMsg => $err,
+                );
+            }
+        ;
+    }
+
+    return (
+        MStatus => 'success',
+        'order-id' => $result,
+    );
+}
+
+sub transaction {
+    my ($transtype, $amount, $opt) = @_;
+
+    my $method = method_map($transtype);
+
+    my @args;
+
+    if ($transtype =~ /[SA]/) {
+        my ($k, $v);
+        for (grep { $opt->{$_} } qw/payment_method_token payment_method_nonce/) {
+            $k = $_;
+            $v = $opt->{$_};
+            last;
+        }
+
+        $v or return (
+            MStatus => 'failure-hard',
+            MErrMsg => ::errmsg('No nonce or token present to process %s type transaction', $transtype),
+        );
+
+        my %act = %{ $opt->{actual} };
+        scrub_addresses(\%act);
+
+        my %params = (
+            amount => $amount,
+            $k => $v,
+            options => {},
+            shipping => {
+                first_name => $act{fname},
+                last_name => $act{lname},
+                street_address => $act{address1},
+                extended_address => $act{address2},
+                locality => $act{city},
+                region => $act{state},
+                postal_code => $act{zip},
+                country_code_alpha2 => $act{country},
+            },
+        );
+
+        for (grep { $opt->{$_} } qw/custom_fields device_data merchant_account_id/) {
+            $params{$_} = $opt->{$_};
+        }
+
+        for (grep { defined $opt->{$_} } qw/skip_advanced_fraud_checking/) {
+            $params{options}{$_} = $opt->{$_};
+        }
+
+        # if $k is a nonce, then we're not using a previously defined
+        # customer. So supply user's billing information and email
+        if ($k eq 'payment_method_nonce') {
+            $params{billing} = {
+                first_name => $act{b_fname},
+                last_name => $act{b_lname},
+                street_address => $act{b_address1},
+                extended_address => $act{b_address2},
+                locality => $act{b_city},
+                region => $act{b_state},
+                postal_code => $act{b_zip},
+                country_code_alpha2 => $act{b_country},
+            };
+            $params{customer} = {
+                first_name => $act{b_fname},
+                last_name => $act{b_lname},
+                email => $act{email},
+                phone => $act{b_phone} || $act{phone} || $act{phone_day} || $act{phone_night},
+            };
+        }
+
+        $params{options}{submit_for_settlement} = $transtype =~ /S/ ? 1 : 0;
+        $params{order_id} = $opt->{comment1}
+            if $opt->{comment1};
+
+        push (@args, \%params);
+    }
+    else {
+        push (@args, $opt->{order_id});
+        push (@args, $amount)
+            unless $transtype =~ /[VF]/;
+    }
+
+::logDebug("calling braintree's %s transaction method with args %s\n", $method, ::uneval(\@args));
+    my $result;
+    {
+        local $@;
+        $result =
+            eval {
+                no warnings 'redefine';
+                local *Carp::longmess = \&Carp::shortmess
+                    if $opt->{environment} eq 'production';
+
+                my $config = Net::Braintree->configuration;
+                $config->$_($opt->{$_})
+                    for qw/environment merchant_id public_key private_key/;
+
+                Net::Braintree::Transaction->$method(@args);
+            }
+            or do {
+                ::logError($@ || "Net::Braintree::Transaction returned no object but did not die for $method call");
+                return (
+                    MStatus => 'failure-hard',
+                    MErrMsg => ::errmsg('Unable to contact payment processor. Please try again.'),
+                );
+            }
+        ;
+    }
+::logDebug("braintree transaction $method result: " . ::uneval($result));
+
+    my ($success, $api_err, $t);
+
+    unless ($api_err = $result->api_error_response) {
+        $t = $result->transaction;
+        $success = $result->is_success
+            && defined($t)
+            && $t->processor_response_code =~ /^1\d{3}$/;
+    }
+
+    if (
+        $success
+            and
+        my $check_sub_name = $opt->{check_sub} || charge_param('check_sub')
+    ) {
+        my $check_sub = $Vend::Cfg->{Sub}{$check_sub_name}
+            || $Global::GlobalSub->{$check_sub_name};
+        if (ref $check_sub eq 'CODE') {
+            $success =
+                $check_sub->(
+                    $result,
+                    $transtype,
+                )
+            ;
+        }
+    }
+
+    my %response;
+
+    if ($api_err) {
+        my ($msg, $code);
+        if (exists $api_err->{transaction}) {
+            my $et = $api_err->{transaction};
+            $msg  = $et->{processor_response_text};
+            $code = $et->{processor_response_code};
+        }
+        if (exists $api_err->{errors}{transaction}{errors}) {
+            my $arr = $api_err->{errors}{transaction}{errors};
+            if (ref($arr) eq 'ARRAY' and @$arr) {
+                my $e = $arr->[0];
+                $msg  = $e->{message};
+                $code = $e->{code};
+            }
+        }
+        %response = (
+            RESPMSG => $msg || $api_err->{message} || 'Unknown',
+            RESULT => $code || -1,
+        );
+    }
+    elsif ($t) {
+        %response = (
+            PNREF => $t->id,
+            RESPMSG => $t->processor_response_text || $result->message || 'Unknown',
+            RESULT => $t->processor_response_code || -1,
+            STATUS => $t->status,
+        );
+
+        $response{AUTHCODE} = $t->processor_authorization_code
+            if $t->processor_authorization_code;
+
+        @response{qw/AVSADDR AVSZIP/} =
+            handle_avs(
+                $t->avs_error_response_code,
+                $t->avs_street_address_response_code,
+                $t->avs_postal_code_response_code,
+            )
+        ;
+
+        {
+            local $_ = $t->cvv_response_code;
+            $response{CVV2MATCH} = /M/ ? 'Y' : /N/ ? 'N' : /[UI]/ ? 'X' : '';
+        }
+
+        $response{CARD_DATA} = $t->credit_card_details
+            if $t->credit_card_details;
+
+        eval {
+            $response{RISK_DATA} = $response{risk_data} = $t->risk_data
+                if $t->risk_data;
+        };
+    }
+    else {
+        %response = (
+            RESPMSG => $result->{message} || 'Unknown',
+            RESULT => -1,
+        );
+    }
+
+    $response{MStatus} = $success ? 'success' : 'failed';
+    $response{MErrMsg} = $response{RESPMSG} if !$success;
+
+    my %response_map = response_map();
+    for (keys %response_map) {
+        $response{$_} = $response{$response_map{$_}}
+            if defined $response{$response_map{$_}};
+    }
+::logDebug("braintree transaction $method response: " . ::uneval(\%response));
+
+    return %response;
+}
+
+sub customer {
+    my ($transtype, $amount, $opt) = @_;
+
+    my $method = method_map($transtype);
+
+    $opt->{payment_method_nonce}
+        or return (
+            MStatus => 'failure-hard',
+            MErrMsg => ::errmsg('Nonce missing; unable to process %s type transaction', $transtype),
+        )
+    ;
+
+    my %act = %{ $opt->{actual} };
+    scrub_addresses(\%act);
+
+    my %params = (
+        first_name => $act{b_fname},
+        last_name => $act{b_lname},
+        email => $act{email},
+        phone => $act{b_phone} || $act{phone} || $act{phone_day} || $act{phone_night},
+        credit_card => {
+            payment_method_nonce => $opt->{payment_method_nonce},
+            cardholder_name => "$act{b_fname} $act{b_lname}",
+            billing_address => {
+                first_name => $act{b_fname},
+                last_name => $act{b_lname},
+                street_address => $act{b_address1},
+                extended_address => $act{b_address2},
+                locality => $act{b_city},
+                region => $act{b_state},
+                postal_code => $act{b_zip},
+                country_code_alpha2 => $act{b_country},
+            },
+            options => {
+                verify_card => 1,
+            },
+        },
+    );
+
+    $params{credit_card}{options}{verification_merchant_account_id} = $opt->{merchant_account_id}
+        if $opt->{merchant_account_id};
+
+    for (grep { $opt->{$_} } qw/custom_fields device_data/) {
+        $params{$_} = $opt->{$_};
+    }
+
+    my @args = (\%params);
+
+::logDebug("calling braintree's %s customer method with args %s\n", $method, ::uneval(\@args));
+    my $result;
+    {
+        local $@;
+        $result =
+            eval {
+                no warnings 'redefine';
+                local *Carp::longmess = \&Carp::shortmess
+                    if $opt->{environment} eq 'production';
+
+                my $config = Net::Braintree->configuration;
+                $config->$_($opt->{$_})
+                    for qw/environment merchant_id public_key private_key/;
+
+                Net::Braintree::Customer->$method(@args);
+            }
+            or do {
+                ::logError($@ || "Net::Braintree::Customer returned no object but did not die for $method call");
+                return (
+                    MStatus => 'failure-hard',
+                    MErrMsg => ::errmsg('Unable to contact payment processor. Please try again.'),
+                );
+            }
+        ;
+    }
+::logDebug("braintree customer $method result: " . ::uneval($result));
+
+    my ($success, $api_err, $c, $pm, $ver);
+
+    unless ($api_err = $result->api_error_response) {
+        $c = $result->customer;
+        $pm = $c && $c->payment_methods->[0];
+        $ver = $pm && $pm->verifications->[0];
+
+        $success = $result->is_success
+            && defined($ver)
+            && $ver->processor_response_code =~ /^1\d{3}$/;
+    }
+
+    if (
+        $success
+            and
+        my $check_sub_name = $opt->{check_sub} || charge_param('check_sub')
+    ) {
+        my $check_sub = $Vend::Cfg->{Sub}{$check_sub_name}
+            || $Global::GlobalSub->{$check_sub_name};
+        if (ref $check_sub eq 'CODE') {
+            $success =
+                $check_sub->(
+                    $result,
+                    $transtype,
+                )
+            ;
+        }
+    }
+
+    my %response;
+
+    if ($api_err) {
+        my ($msg, $code);
+        if (exists $api_err->{verification}) {
+            my $ev = $api_err->{verification};
+            if ($ev->{gateway_rejection_reason}) {
+                # "If a transaction was authorized before being rejected, the gateway will automatically void it."
+                # https://articles.braintreepayments.com/control-panel/transactions/gateway-rejections
+                $msg = $api_err->{message};
+                $code = gateway_rejection_reason_code($ev->{gateway_rejection_reason});
+            }
+            else {
+                $msg  = $ev->{processor_response_text};
+                $code = $ev->{processor_response_code};
+            }
+        }
+        if (exists $api_err->{errors}{customer}{credit_card}{errors}) {
+            my $arr = $api_err->{errors}{customer}{credit_card}{errors};
+            if (ref($arr) eq 'ARRAY' and @$arr) {
+                my $e = $arr->[0];
+                $msg  = $e->{message};
+                $code = $e->{code};
+            }
+        }
+        if (exists $api_err->{errors}{customer}{credit_card}{billing_address}{errors}) {
+            my $arr = $api_err->{errors}{customer}{credit_card}{billing_address}{errors};
+            if (ref($arr) eq 'ARRAY' and @$arr) {
+                my $e = $arr->[0];
+                $msg  = $e->{message};
+                $code = $e->{code};
+            }
+        }
+        %response = (
+            RESPMSG => $msg || $api_err->{message} || 'Unknown',
+            RESULT => $code || -1,
+        );
+    }
+    elsif ($ver) {
+        %response = (
+            PNREF => $pm->token,
+            RESPMSG => $ver->processor_response_text || $result->message || 'Unknown',
+            RESULT => $ver->processor_response_code || -1,
+            CUSTOMER_ID => $c->id,
+        );
+
+        @response{qw/AVSADDR AVSZIP/} =
+            handle_avs(
+                $ver->avs_error_response_code,
+                $ver->avs_street_address_response_code,
+                $ver->avs_postal_code_response_code,
+            )
+        ;
+
+        {
+            local $_ = $ver->cvv_response_code;
+            $response{CVV2MATCH} = /M/ ? 'Y' : /N/ ? 'N' : /[UI]/ ? 'X' : '';
+        }
+
+        $response{CARD_DATA} = $ver->credit_card
+            if $ver->credit_card;
+
+        eval {
+            $response{RISK_DATA} = $response{risk_data} = $ver->risk_data
+                if $ver->risk_data;
+        };
+    }
+    else {
+        %response = (
+            RESPMSG => $result->{message} || 'Unknown',
+            RESULT => -1,
+        );
+    }
+
+    $response{MStatus} = $success ? 'success' : 'failed';
+    $response{MErrMsg} = $response{RESPMSG} if !$success;
+
+    my %response_map = response_map();
+    for (keys %response_map) {
+        $response{$_} = $response{$response_map{$_}}
+            if defined $response{$response_map{$_}};
+    }
+::logDebug("braintree customer $method response: " . ::uneval(\%response));
+
+    return %response;
+}
+
+package Vend::Payment;
+
+use strict;
+use warnings;
+
+sub braintree {
+    my ($user, $amount) = @_;
+
+::logDebug("braintree called\n%s\n", ::uneval($user));
+
+    my $opt;
+    if(ref $user) {
+        $opt = $user;
+    }
+    else {
+        $opt = {};
+    }
+
+    my $actual;
+    if($opt->{actual}) {
+        $actual = $opt->{actual};
+    }
+    else {
+        my (%actual) = map_actual();
+        $actual = \%actual;
+    }
+    # This quirk is for ensuring access to actual in subprocessing routines
+    $opt->{actual} ||= $actual;
+
+    my %type_map = qw/
+        sale          S
+        auth          A
+        authorize     A
+        void          V
+        settle        D
+        settle_prior  D
+        credit        C
+        mini_auth     M
+        verify        M
+        status        F
+        find          F
+        client_token  T
+        token         T
+        T             T
+        F             F
+        S             S
+        C             C
+        D             D
+        V             V
+        A             A
+        M             M
+    /;
+
+    my $transtype = $opt->{transaction} || charge_param('transaction') || 'A';
+
+    $transtype = $type_map{$transtype}
+        or return (
+                MStatus => 'failure-hard',
+                MErrMsg => ::errmsg('Unrecognized transaction: %s', $transtype),
+            );
+
+    $amount = $opt->{total_cost} if ! $amount;
+
+    if (! $amount) {
+        my $precision = $opt->{precision} || charge_param('precision') || 2;
+        my $cost = Vend::Interpolate::total_cost();
+        $amount = Vend::Util::round_to_frac_digits($cost, $precision);
+    }
+
+    my $sub =
+        $transtype eq 'M' ? \&Vend::Payment::Braintree::customer     :
+        $transtype eq 'T' ? \&Vend::Payment::Braintree::client_token :
+                            \&Vend::Payment::Braintree::transaction
+    ;
+
+    return $sub->($transtype, $amount, $opt);
+}
+
+1;



More information about the interchange-cvs mailing list