[interchange] Add gateway_log support to Braintree

Mark Johnson interchange-cvs at icdevgroup.org
Mon Nov 13 15:48:26 UTC 2017


commit 3f2fae4d377495d7e482dc587657a770da914502
Author: Mark Johnson <mark at endpoint.com>
Date:   Mon Nov 13 10:44:53 2017 -0500

    Add gateway_log support to Braintree

 lib/Vend/Payment/Braintree.pm  |  396 +++++++++++++++++++++++++++++++++++++++-
 lib/Vend/Payment/GatewayLog.pm |    5 +-
 2 files changed, 388 insertions(+), 13 deletions(-)
---
diff --git a/lib/Vend/Payment/Braintree.pm b/lib/Vend/Payment/Braintree.pm
index 96f6861..738cdde 100644
--- a/lib/Vend/Payment/Braintree.pm
+++ b/lib/Vend/Payment/Braintree.pm
@@ -516,7 +516,21 @@ sub transaction {
             unless $transtype =~ /[VF]/;
     }
 
-::logDebug("calling braintree's %s transaction method with args %s\n", $method, ::uneval(\@args));
+    my $gwl = Vend::Payment::Braintree::GWL
+        -> new({
+            Enabled => charge_param('gwl_enabled'),
+            LogTable => charge_param('gwl_table'),
+            Source => charge_param('gwl_source'),
+        })
+    ;
+    $gwl->request({
+        opt => {
+            %$opt,
+            transtype => $transtype,
+        },
+        args => $gwl->label_args(\@args),
+    });
+#::logDebug("calling braintree's %s transaction method with args %s\n", $method, ::uneval(\@args));
     my $result;
     {
         local $@;
@@ -530,18 +544,23 @@ sub transaction {
                 $config->$_($opt->{$_})
                     for qw/environment merchant_id public_key private_key/;
 
+                $gwl->start;
                 Net::Braintree::Transaction->$method(@args);
             }
             or do {
-                ::logError($@ || "Net::Braintree::Transaction returned no object but did not die for $method call");
+                my $err = $@ || "Net::Braintree::Transaction returned no object but did not die for $method call";
+                $gwl->stop;
+                $gwl->response({ return => {}, eval_error => $err, },);
                 return (
                     MStatus => 'failure-hard',
                     MErrMsg => ::errmsg('Unable to contact payment processor. Please try again.'),
                 );
             }
         ;
+        $gwl->stop;
+        $gwl->response({ return => { RESPMSG => 'N/A'}, raw => $result, });
     }
-::logDebug("braintree transaction $method result: " . ::uneval($result));
+#::logDebug("braintree transaction $method result: " . ::uneval($result));
 
     my ($success, $api_err, $t);
 
@@ -638,8 +657,9 @@ sub transaction {
         $response{$_} = $response{$response_map{$_}}
             if defined $response{$response_map{$_}};
     }
-::logDebug("braintree transaction $method response: " . ::uneval(\%response));
+#::logDebug("braintree transaction $method response: " . ::uneval(\%response));
 
+    $gwl->response({ return => \%response, raw => $result, });
     return %response;
 }
 
@@ -690,8 +710,21 @@ sub customer {
     }
 
     my @args = (\%params);
-
-::logDebug("calling braintree's %s customer method with args %s\n", $method, ::uneval(\@args));
+    my $gwl = Vend::Payment::Braintree::GWL
+        -> new({
+            Enabled => charge_param('gwl_enabled'),
+            LogTable => charge_param('gwl_table'),
+            Source => charge_param('gwl_source'),
+        })
+    ;
+    $gwl->request({
+        opt => {
+            %$opt,
+            transtype => $transtype,
+        },
+        args => $gwl->label_args(\@args),
+    });
+#::logDebug("calling braintree's %s customer method with args %s\n", $method, ::uneval(\@args));
     my $result;
     {
         local $@;
@@ -705,18 +738,23 @@ sub customer {
                 $config->$_($opt->{$_})
                     for qw/environment merchant_id public_key private_key/;
 
+                $gwl->start;
                 Net::Braintree::Customer->$method(@args);
             }
             or do {
-                ::logError($@ || "Net::Braintree::Customer returned no object but did not die for $method call");
+                my $err = $@ || "Net::Braintree::Customer returned no object but did not die for $method call";
+                $gwl->stop;
+                $gwl->response({ return => {}, eval_error => $err, },);
                 return (
                     MStatus => 'failure-hard',
                     MErrMsg => ::errmsg('Unable to contact payment processor. Please try again.'),
                 );
             }
         ;
+        $gwl->stop;
+        $gwl->response({ return => { RESPMSG => 'N/A'}, raw => $result, });
     }
-::logDebug("braintree customer $method result: " . ::uneval($result));
+#::logDebug("braintree customer $method result: " . ::uneval($result));
 
     my ($success, $api_err, $c, $pm, $ver);
 
@@ -829,8 +867,9 @@ sub customer {
         $response{$_} = $response{$response_map{$_}}
             if defined $response{$response_map{$_}};
     }
-::logDebug("braintree customer $method response: " . ::uneval(\%response));
+#::logDebug("braintree customer $method response: " . ::uneval(\%response));
 
+    $gwl->response({ return => \%response, raw => $result, });
     return %response;
 }
 
@@ -842,7 +881,7 @@ use warnings;
 sub braintree {
     my ($user, $amount) = @_;
 
-::logDebug("braintree called\n%s\n", ::uneval($user));
+#::logDebug("braintree called\n%s\n", ::uneval($user));
 
     my $opt;
     if(ref $user) {
@@ -912,4 +951,341 @@ sub braintree {
     return $sub->($transtype, $amount, $opt);
 }
 
+package Vend::Payment::Braintree::GWL;
+
+use base qw/Vend::Payment::GatewayLog/;
+use Scalar::Util qw/reftype/;
+
+# Return structure from Net::Braintree is exceptionally bloated. The response
+# is passed through a number of thinning processes to make it much more
+# readable and take considerably less storage space.
+
+# Constants to define arrays of typically bloating, useless keys per
+# transaction/reftype combination. They will be culled if they are present but
+# undefined; otherwise, they will persist into gateway_log table.
+
+use constant CUSTOMER_HASH_KEYS =>
+[qw/
+    customer
+/];
+
+use constant CUSTOMER_ARRAY_KEYS =>
+[qw/
+    refund_ids
+/];
+
+use constant CUSTOMER_SCALAR_KEYS =>
+[qw/
+    company
+    fax
+    refund_id
+    refunded_transaction_id
+    website
+/];
+
+use constant CUSTOMER_CC_KEYS =>
+[qw/
+    subscriptions
+/];
+
+use constant TRANSACTION_HASH_KEYS =>
+[qw/
+    descriptor
+    disbursement_details
+    processor_settlement_response_code
+    processor_settlement_response_text
+    subscription
+/];
+
+use constant TRANSACTION_ARRAY_KEYS =>
+[qw/
+    add_ons
+    discounts
+    disputes
+    partial_settlement_transaction_ids
+/];
+
+use constant TRANSACTION_SCALAR_KEYS =>
+[qw/
+    additional_processor_response
+    authorized_transaction_id
+    channel
+    escrow_status
+    master_merchant_account_id
+    plan_id
+    purchase_order_number
+    service_fee_amount
+    settlement_batch_id
+    sub_merchant_account_id
+    subscription_id
+    three_d_secure_info
+    voice_referral_number
+/];
+
+# Remove certain completely empty response objects corresponding to KEYS
+# definitions above.
+
+sub thin_hashes {
+    # hashes either empty or with all undef values
+    my ($obj, $prefix, $fields) = @_;
+    for my $k (@$fields) {
+        next unless exists $obj->{$k};
+        my $h = $obj->{$k};
+        if (grep { defined($h->{$_}) } keys %$h) {
+            ::logError("Unexpected defined value found in $prefix$k - preserving entire hash\n");
+        }
+        else {
+            delete $obj->{$k};
+        }
+    }
+    return;
+}
+
+sub thin_arrays {
+    # empty arrays
+    my ($obj, $prefix, $fields) = @_;
+    for my $k (@$fields) {
+        next unless exists $obj->{$k};
+        my $arr = $obj->{$k};
+        if (ref($arr) ne 'ARRAY' or @$arr) {
+            ::logError("Unexpected data type or array not empty in $prefix$k - preserving\n");
+        }
+        else {
+            delete $obj->{$k};
+        }
+    }
+    return;
+}
+
+sub thin_scalars {
+    # undef scalars
+    my ($obj, $prefix, $fields) = @_;
+    for my $k (@$fields) {
+        next unless exists $obj->{$k};
+        if (defined $obj->{$k}) {
+            ::logError("Unexpected defined value in $prefix$k - preserving\n");
+        }
+        else {
+            delete $obj->{$k};
+        }
+    }
+    return;
+}
+
+# Stringify timestamps for readability and bloat reduction
+
+sub thin_datetimes {
+    # stringify DateTime objects directly on source reference
+    my $ref = shift;
+
+    my $type = ref ($$ref)
+        or return;
+
+    if ( $type eq 'DateTime' ) {
+        $$ref = $$ref->formatter->format_datetime($$ref);
+        return;
+    }
+
+    my $rtype = reftype($$ref);
+    if ( $rtype eq 'HASH' ) {
+        thin_datetimes(\$_) for values %$$ref;
+    }
+    elsif ( $rtype eq 'ARRAY' ) {
+        thin_datetimes(\$_) for @$$ref;
+    }
+
+    return;
+}
+
+# Deflate all hashes
+
+sub hash_deflate {
+    # convert any hash objects into regular hashes
+    my $ref = shift;
+
+    my $rtype = reftype($$ref)
+        or return;
+
+    my $type = ref ($$ref);
+
+    if ($rtype eq 'HASH') {
+
+        hash_deflate(\$_) for values %$$ref;
+
+        if ($type ne $rtype) {
+            $$ref = { %$$ref };
+        }
+    }
+    elsif ($rtype eq 'ARRAY') {
+
+        hash_deflate(\$_) for @$$ref;
+
+    }
+
+    return;
+}
+
+# Main routine to act on the top-level response object. Delegates to the above
+# routines.
+
+sub thin_response_object {
+    my $orig = shift;
+    return $orig unless $orig and reftype($orig) eq 'HASH';
+
+    # Deep copy the object contents so we don't affect the original
+    my $obj = eval ::uneval($orig);
+
+    # Scrub all annoying DateTime objects
+    thin_datetimes(\$obj);
+
+    # Deflate all remaining hash objects
+    hash_deflate(\$obj);
+
+    delete $obj->{return}{CARD_DATA}{image_url}
+        if exists $obj->{return}{CARD_DATA};
+
+    return $obj unless exists $obj->{raw} and reftype($obj->{raw}) eq 'HASH';
+
+    if (exists $orig->{raw}{response}{customer}) {
+        my $customer = $obj->{raw}{response}{customer};
+        my $prefix = 'raw.response.customer.';
+        thin_hashes( $customer, $prefix, CUSTOMER_HASH_KEYS);
+        thin_arrays( $customer, $prefix, CUSTOMER_ARRAY_KEYS);
+        thin_scalars($customer, $prefix, CUSTOMER_SCALAR_KEYS);
+        my $cc_n = 0;
+        for my $cc (@{ $customer->{credit_cards} }) {
+            my $cc_prefix = $prefix . "credit_cards.$cc_n.";
+            ++$cc_n;
+            thin_arrays($cc, $cc_prefix, CUSTOMER_CC_KEYS);
+            delete $cc->{image_url};
+        }
+    }
+
+    if (exists $orig->{raw}{response}{transaction}) {
+        my $txn = $obj->{raw}{response}{transaction};
+        my $prefix = 'raw.response.trasaction.';
+        thin_hashes( $txn, $prefix, TRANSACTION_HASH_KEYS);
+        thin_arrays( $txn, $prefix, TRANSACTION_ARRAY_KEYS);
+        thin_scalars($txn, $prefix, TRANSACTION_SCALAR_KEYS);
+        delete $txn->{credit_card}{image_url};
+    }
+
+    return $obj;
+}
+
+# log_it() must be overridden.
+sub log_it {
+    my $self = shift;
+
+    my $request = $self->request;
+    unless ($request) {
+        ::logDebug('Cannot write to %s: no request present', $self->table);
+        return;
+    }
+
+    unless ($self->response) {
+        if ($Vend::Payment::Global_Timeout) {
+            my $msg = errmsg('No response. Global timeout triggered');
+            ::logDebug($msg);
+            $self->response({
+                return => {
+                    RESULT => -2,
+                    RESPMSG => $Vend::Payment::Global_Timeout,
+                },
+            });
+        }
+        else {
+            my $msg = errmsg('No response. Reason unknown');
+            ::logDebug($msg);
+            $self->response({
+                return => {
+                    RESULT => -3,
+                    RESPMSG => $msg,
+                },
+            });
+        }
+    }
+
+    my $response = $self->response;
+
+    my $return = $response->{return};
+    my $rc =
+        defined ($return->{RESULT})
+        && $return->{RESULT} =~ /^-?\d+$/
+            ? $return->{RESULT}
+            : undef
+    ;
+
+    my $opt = delete $request->{opt};
+    my $processor = $opt->{route} || $opt->{gateway};
+
+    my $thinned_response = eval {
+        thin_response_object($response)
+    };
+    if ($@ or !$thinned_response) {
+        ::logError("Error thinning Braintree response" . ($@ ? ": $@" : ''));
+        $thinned_response = $response;
+    }
+#::logDebug("Gateway log thinned response: " . ::uneval($thinned_response));
+
+    my %fields = (
+        trans_type => $opt->{transtype} || 'x',
+        processor => $processor || 'braintree',
+        catalog => $Vend::Cfg->{CatalogName},
+        result_code => $rc || '',
+        response_msg => $return->{RESPMSG} || '',
+        request_id => $return->{PNREF} || '',
+        order_number => $opt->{comment1} || '',
+        request_duration => $self->duration,
+        request_date => $self->timestamp,
+        request_source => $self->source,
+        email => $opt->{actual}{email} || '',
+        request => ::uneval($request) || '',
+        response => ::uneval($thinned_response) || '',
+        session_id => $::Session->{id},
+    );
+
+    $fields{order_md5} =
+        Digest::MD5::md5_hex(
+            $opt->{actual}{email},
+            $opt->{transtype} || 'x',
+            $request->{args}{ORIGID},
+            $request->{args}{AMT} || $request->{args}{amount},
+            $::Session->{id},
+            map { ($_->{code}, $_->{quantity}) } @$Vend::Items
+        )
+    ;
+
+    $self->write(\%fields);
+}
+
+sub label_args {
+    my $self = shift;
+    my $orig = shift;
+
+    return 'malformed request argument list' unless
+        reftype ($orig) eq 'ARRAY'
+        &&
+        scalar @$orig
+    ;
+
+    # Ensure manipulations of argument ref are insulated
+    my $arg = eval ::uneval($orig);
+
+    return $arg->[0] if ref($arg->[0]) and reftype($arg->[0]) eq 'HASH';
+
+    my @k = qw/ORIGID AMT/;
+    my %hsh;
+
+    while (@$arg && @k) {
+        $hsh{ shift (@k) } = shift @$arg;
+    }
+
+    if (@$arg) {
+        $hsh{unknown} = $arg;
+    }
+
+    return \%hsh;
+}
+
 1;
diff --git a/lib/Vend/Payment/GatewayLog.pm b/lib/Vend/Payment/GatewayLog.pm
index 979a63a..3c9e4bb 100644
--- a/lib/Vend/Payment/GatewayLog.pm
+++ b/lib/Vend/Payment/GatewayLog.pm
@@ -136,11 +136,10 @@ sub write {
     if ($@) {
         my $err = $@;
         ::logGlobal(
-            q{Couldn't write to %s: %s -- request: %s -- response: %s},
+            q{Couldn't write to table %s: %s -- data hash: %s},
             $self->table,
             $err,
-            ::uneval($self->request),
-            ::uneval($self->response)
+            ::uneval($data),
         );
     }
     else {



More information about the interchange-cvs mailing list