package Vend::Payment::GatewayLog; use strict; use warnings; use Time::HiRes; sub new { my ($class, $opt) = @_; #::logDebug("Called in class $class, with opt hash %s", ::uneval($opt)); my $self = bless ({}, $class); $self->init(%{$opt || {}}); $Vend::Payment::Global_Timeout = undef; return $self; } sub init { my $self = shift; my %opt = @_; $self->{_log_table} = delete ($opt{LogTable}) || 'gateway_log'; $self->{_enabled} = delete ($opt{Enabled}) || ''; $self->{_source} = delete ($opt{Source}) || ''; unless (length ($self->{_source})) { my $host = `hostname -s`; chomp ($self->{_source} = $host); } if (my @k = keys %opt) { @$self{@k} = @opt{@k}; } return 1; } sub start { my $self = shift; return unless $self->_enabled; my $override = shift; $self->{__start} = Time::HiRes::clock_gettime() if $override || !$self->{__start}; #::logDebug("Start time: $self->{__start}"); return $self->{__start}; } sub stop { my $self = shift; return unless $self->_enabled; my $override = shift; $self->{__stop} = Time::HiRes::clock_gettime() if $override || !$self->{__stop}; #::logDebug("Stop time: $self->{__stop}"); return $self->{__stop}; } sub duration { my $self = shift; return unless $self->_enabled; my $fmt = shift || '%0.3f'; my $rv = sprintf ($fmt, $self->stop - $self->start); #::logDebug("Duration calculated at $rv"); return $rv; } sub timestamp { my $self = shift; return unless $self->_enabled; my $fmt = shift || '%Y-%m-%d %T'; my $rv = POSIX::strftime($fmt, localtime($self->start)); #::logDebug("Start formatted timestamp returned $rv"); return $rv; } sub request { my $self = shift; return unless $self->_enabled; my $request = shift; return $self->{__request} unless $request; unless (UNIVERSAL::isa($request, 'HASH')) { ::logDebug( 'Skipping non-HASH request set: received %s (unevals to %s)', $request, ::uneval($request) ); return; } #::logDebug('Setting request: %s', ::uneval($request)); $self->{__request} = { %$request }; } sub response { my $self = shift; return unless $self->_enabled; my $response = shift; return $self->{__response} unless $response; unless (UNIVERSAL::isa($response, 'HASH')) { ::logDebug( 'Skipping non-HASH response set: received %s (unevals to %s)', $response, ::uneval($response) ); return; } #::logDebug('Setting response: %s', ::uneval($response)); $self->{__response} = { %$response }; } sub clean { my $self = shift; return unless $self->_enabled; delete $self->{$_} for grep { /^__/ } keys %$self; #::logDebug('Cleaned all object data'); return 1; } sub log_it { die 'Must override log_it() in subclass'; } sub write { my $self = shift; my $data = shift; #::logDebug('Ready to write: %s', ::uneval($data)); eval { my $table = $self->table; my $db = ::database_exists_ref($table) or die "'$table' not a valid Interchange table"; $db = $db->ref; $db->set_slice( [ { dml => 'insert' } ], $data ) or die "set_slice for $table failed"; }; # End eval if ($@) { my $err = $@; ::logGlobal( q{Couldn't write to table %s: %s -- data hash: %s}, $self->table, $err, ::uneval($data), ); } else { #::logDebug('Cleaning out object data after successful write to database'); $self->clean; } return 1; } sub table { return shift->{_log_table}; } sub _enabled { return shift->{_enabled}; } sub source { return shift->{_source}; } sub DESTROY { my $self = shift; return 1 unless $self->_enabled; # Unexpected order of operations causes any evals # within DESTROY to execute after any eval that may # contain the DESTROYed object. Effect is that $@ upon # exit of the containing eval is *not* controlled by # that eval. Localization contains the effect. local $@; eval { $self->log_it }; ::logGlobal("log_it eval died: $@") if $@; 1; } 1; __END__ =head1 NAME Vend::Payment::GatewayLog - Basic package and methods for enabling full transaction logging in any of the gateways within the Vend::Payment::* namespace. =head1 VERSION 1.00 =head1 USAGE From within the normally unused namespace of the payment gateway: package Vend::Payment::WhizBang; use Vend::Payment::GatewayLog use base qw/Vend::Payment::GatewayLog/; sub log_it { # Override log_it() with gateway-dependent mapping to a logging table, # by default gateway_log ... } Then from inside the gateway sub itself: ... my $gwl = Vend::Payment::WhizBang -> new({ Enabled => charge_param('gwl_enabled'), }); $gwl->request(\%scrubbed_request_hash); $gwl->start; ... Code that calls out to gateway's API ... $gwl->stop; ... $gwl->response(\%response_hash); =head1 DESCRIPTION Module sets up an object with some utility methods to facilitate full database logging of all transaction attempts executed through the gateway. Often these data are either missing or insufficient for proper evaluation of problem events or general reporting. It's important to scrub all sensitive data appropriate for permanent storage. Minimally, this is recommended to include credit card numbers and CVV2 values, any senstitive gateway credentials (passwords or secret keys), or any personal data that can be used to exploit a customer's identity (SSN, date of birth, etc.). You do not want to call log_it() yourself. It is set up to execute automatically on destruction to help ensure that unexpected exits or aborts are still logged. =head2 Options Pass hashref to the constructor to include the following options: =over 4 =item Enabled Boolean to indicate that actual logging should be performed. Default is false; thus logging must be explicitly requested. Can be set in the constructor with Route param "gwl_enabled" or globally with MV_PAYMENT_GWL_ENABLED in catalog.cfg. =item LogTable Name of table to which logging should be directed. Default is gateway_log. Can be set in the constructor with Route param "gwl_table" or globally with MV_PAYMENT_GWL_TABLE in catalog.cfg. =item Source Maps to the request_source field in the log table. Value is most meaningful in a distributed environment, where multiple servers running the Interchange application may be handling requests behind a load balancer. Default value obtained from `hostname -s`. Can be set in the constructor with Route param "gwl_source" or globally with MV_PAYMENT_GWL_SOURCE in catalog.cfg. =back =head1 METHODS =over 4 =item new() Constructor with optional hash ref indicating any of the above Options. Note that when Enabled, it will trigger a log write as soon as it goes out of scope, so consider when and where you want to call the constructor. =item start() Return, and optionally set, the Time::HiRes::clock_gettime() of the beginning of the call to the gateway's API. When called for the first time, it will set the value in the object and return said value; otherwise, it simply returns the previously set value. The set can be overridden by calling the method with a perly true arg. =item stop() Return, and optionally set, the Time::HiRes::clock_gettime() of the end of the call to the gateway's API. Same conditions apply as do for start() =item duration() Returns the delta of stop() - start(). Be aware that calling this method prior to calling either start() or stop() will cause the current time to be used for each and frozen. Default format is '%0.3f', but can be overriden passed as an arg. =item timestamp() Returns the timestamp of the value of start(). Again, calling this method prior to calling start() will cause the current time to be used and frozen. Default format is '%Y-%m-%d %T', but can be overriden passed as an arg. =item request() Freeze the hash request sent to the payment gateway if passed as an arg. Code will freeze a copy of the hash (though not a deep copy, if that impacts your particular gateway's request structure) so that any post-request processing on the original hash will not affect the stored request. Method only accepts a hash reference and will skip any other data structure that is attempted to be saved and logs the issue to the debug log, along with an ::uneval() of whatever was passed in. Calling with no args will only return the currently stored request hash reference. =item response() Freeze the hash response from the payment gateway if passed as an arg. Code will freeze a copy of the hash (though not a deep copy, if that impacts your particular gateway's response structure) so that any post-response processing on the original hash will not affect the stored response. Method only accepts a hash reference and will skip any other data structure that is attempted to be saved and logs the issue to the debug log, along with an ::uneval() of whatever was passed in. Calling with no args will only return the currently stored response hash reference. =item clean() Will purge all stored data from the object. =item log_it() Stub that must be overridden in the subclass. Invoking the object when log_it() has failed to be overridden will cause the code to die. =item table() Returns the name of the table against which the database update is to be performed. Default is 'gateway_log', but can be overridden in the constructor using the LogTable option. =item source() Returns the value set in the constructor for the Source option. =back =head1 AUTHOR Mark Johnson (mark@endpointdev.com), End Point Corp. =head1 LICENSE AND COPYRIGHT Copyright (C) 2017 Interchange Development Group and others This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see: http://www.gnu.org/licenses/ =cut