Business-OCV
view release on metacpan or search on metacpan
# contained in an OCV message, so keep an eye out for "extra" fields :-).
# In addition to this transaction log, there is a server debug log ("DebugLog").
# The debug log is used to dump pretty well all the 'raw' data sent and
# receieved to/from the OCV server, plus other odds and sods. Debugging is
# turned off and on via the 'debug' constructor argument and debug() method,
# and is off by default. NOTE that the debug flag also controls general
# program debugging via carp (to recap, the debug log is for OCV interactions,
# STDERR (via carp) is used for general program debugging).
#
# The logreopen() method will reopen the log file/s, which you might
# want to do in response to a signal (also see reset()).
#
# WARNING: when debugging is turned on sensitive data could potentially
# be disclosed (e.g. card data within a purchase transaction message).
# To prevent such data being logged, the logdebug message filters it's
# output for strings which are contained in a 'debugfilter' list (only the
# card number at present). See the comments in logdebug() for more info.
# NOTE that this broad filtering is only done on the debug log (the card
# data written to the transaction log is separately "filtered").
#
# Totals and the Transaction Logs
#
# The beancounters want daily transaction summaries for the OPS to
# reconcile with the bank. [An aside: the OCV server provides a Totals
# request message, however the Ingenico documentation notes not to rely on
# it and "it is recommended that the client ... maintain its own totals". I
# found out why this might be when the OCV server crashed during testing
# and couldn't be restarted without deleting the "journal" files (and
# associated NT registry keys). It is these journal files which the OCV
# server uses to generate the totals, so when they are toasted you lose
# all the transaction records too.]
#
# The totals information can be gleaned from this module's transaction
# logs (in particular, the amounts, card types and settlement dates). Note
# though that the information is in pairs of log entries: the PURCHASE or
# REFUND message (type, amount); and the RESPONSE or STATUS message (status,
# settlement date). Further, note that if disaster strikes one (or both) of
# the pair may be missing, in which case the totals post-processor will
# have to raise an error notice for manual intervention (though I expect it
# will be difficult to detect if *both* messages are missing!).
#
#
# USAGE
# [note: usage has changed from below, though the gist is the same - I'll
# update this real-soon-now]
#
# There is only one exported constructor: OCV::new
# The required parameters are: server address, client ID, account number
# e.g. my $ocv = new OCV ('192.1.2.3:53005', 'MyClient', '2');
#
# There is one OCV method provided for each message type. Each message
# method constructs and sends an appropriate OCV message and, if 'polled
# mode' is off (i.e. blocking mode, the default), will wait to receive a
# response from the server. It will then either timeout and return an error,
# or return the server response in the form of an array or OCV::Message
# object. In polled mode it simply returns an empty message (empty list /
# undef).
#
# Note that due to the nature of the OCV protocol, if a timeout occurs
# (or any error, for that matter) the message exchange sequence will likely
# be out of synchronisation. The server connection should be terminated and
# reestablished, a reset() method is provided to do so.
#
# Error Conditions
#
# Generally, all methods return a true value/list on success, or
# undef/empty list on failure. An error message should be in $@.
# If a 'warning' is raised, a successful return value is given with
# $@ set to the warning message. i.e. if $@ is set, the result warrants
# closer inspection.
# e.g.
# if (my @m|$m = $ocv->purchase(...))
# {
# warn "Warning: $@" if $@;
# < ok, process @m|$m >
# }
# else
# {
# warn "Error: $@";
# }
#
# If you want to do a sequence of commands (e.g. using polling), try
# wrapping the whole lot in an eval to save a lot of result testing:
#
# eval # try
# {
# my $m = $ocv->purchase(..., PolledMode => POLL_NONBLOCK) or die "$@\n";
#
# my $n = 60; # don't keep trying forever
# do
# {
# sleep 2;
# # get the status of the last transaction
# # - status always "blocks" and returns a response
# $m = $ocv->status() or die "$@\n";
# } while ($m and $m->Result == TRANS_INPROGRESS and $n--);
#
# warn "Warning: $@" if $@;
#
# if ($m and defined($m->Result))
# {
# $m->Result == TRANS_APPROVED and print "Result: APPROVED\n";
# $m->Result == TRANS_INPROGRESS and print "Result: INPROGRESS\n";
# $m->Result == TRANS_DECLINED and print "Result: DECLINED: " .
# $m->ResponseText . ($m->Retry ? " RETRY":"") . "\n";
# }
#
# defined($m);
# }
# or do # catch
# {
# print "Error: $@\n";
# undef;
# };
#
# Any number of communications failures may occur between this client and
# the OCV server. Some of these error conditions could cause the command-
# response sequence to become missynchronised, thus it is advised that the
# connection be closed and re-opened upon error. A flush() method is
# provided if you wish to attempt to "manually" resynchronise. A
# reset() method is also provided: it closes the OCV connection,
# reopens the log file/s, and reopens the OCV connection. This should
# reset things to a virgin state. A reset() may also be in order in
# response to a HUP signal.
#
#
# NOTES/CLARIFICATIONS ON THE OCV SERVER DOCUMENTATION
#
# - Pre-authorisations and Completions
# These transactions are handled completely by the bank - that is, the
# OCV server doesn't do anything special with them. Moreover, they're
# apparently treated as disparate transactions - the OCV server (at least,
# possibly also the bank) does nothing to ensure pre-auths and completions
# match (card data, amount, etc). For example, it is apparently possible
# for a completion with a given preauth number to 'succeed' even when the
# card data does not match that of the pre-auth transaction. It appears
# that behaviour in these situations is undefined - it is up to the client
# to make sure the data match.
#
# Generally, a completion is equivalent to a purchase.
#
# - Accounts
# Each transaction to the bank must provide a merchant ID (to identify
# the merchant (e.g. bank account details)), and terminal ID (to identify
# the hardware). OCV "accounts" are used to abstract these details, and
# more importantly to allow concurrent transactions (requires multiple
# VPPs, which in turn requires both a multiple-VPP license from Ingenico and
# multiple merchant IDs and/or terminal IDs from the bank). The client (us)
# simply specifies which account to use and the server allocates the first
# available VPP allocated to that account. It returns the MerchantID and
# TerminalID as part of the RESPONSE message, if the client is interested.
#
# The account number 0 is the 'Default' account and cannot be removed.
# The Default account is for the OCV Server's internal use and must not be
# used by clients. Note that the Default account must have a VPP assigned to
# it (which is why you get 6 accounts when you purchase a 5 account license).
# Further, when processing concurrent transactions, if an account is busy
# you'll get a SERVER BUSY response so it pays to allocate as many VPPs to
# an account as possible (and make sure to retry BUSY responses).
#
# OCV DEVELOPMENT SERVER BUGS
#
# The OCV 'Development Server' supplied by Ingenico for testing and
# development purposes has a few bugs which mean it's not an entirely
# reliable means of testing your code. As of v.1.15, it:
# - often locks up and/or crashes with dud messages
# - does not respond well to polled requests. It 'locks' the account after
# serving some polled requests (i.e. subsequent transactions on the
# account return SERVER BUSY or RECORD NOT FOUND). In addition, on
# subsequent connections it erroneously sends a response to the polled
# request which mis-synchronises the rest of the communications.
# - does not return full details for status requests (for example, it omits
# the settlement date, card info, merchant + terminal IDs)
#
# OCV LIVE SERVER BUGS
#
# Unfortunately the Ingenico 'live' server (v2.08) has also shown problems,
# with one issue of a complete lockup after a totals requests (the NT registry
# had to be edited to restore service). Additionally, the server is found to
# issue unsolicted 'logon responses' around once per week. Ingenico have
# advised this is an "undocumented feature".
# To work around this, LOGON responses to non-LOGON requests are
# transparently discarded (the event is logged).
#
#
# MISCELLANEOUS NOTES ON THE CODE
#
# As is discussed below in "Message Format Specifications", each OCV
# message is described via a table of field name => data type pairs.
# Internally these are manipulated via hashes (see notes in the code
# for the details). The use of hashes has required a bit of mucking
# about due to a hash's unpredictable ordering, though at the time
# of writing there was mention of "pseduo-hashes", i.e. arrays which
# support string indices, with perl automatically managing the mapping
# from string to index. Perhaps if/when perl's pseudo-hashes become
# standard the code can be simplified and performance probably improved,
# for what it's worth :-).
#
#
######################################################################
#
# RCS Identifier:
# $Id:$
#
# Change Log:
# $Log:$
#
#
######################################################################
#
use strict; # try and pick up silly errors at compile time
use vars qw/@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION $OCV_VERSION
$AUTOLOAD $debug/;
$VERSION = 0.1; # this module
$OCV_VERSION = 1.08; # the OCV spec to which this module applies
use Exporter ();
@ISA = qw/Exporter/;
@EXPORT = qw//;
@EXPORT_OK = qw//;
%EXPORT_TAGS =
(
'server' => [qw/SERVER_PORT POLL_BLOCK POLL_NONBLOCK/],
'transaction' => [qw/TRANS_CLIENTNET TRANS_CLIENTTEL TRANS_CLIENTMAIL
TRANS_APPROVED TRANS_DECLINED TRANS_INPROGRESS
TRANS_BUSY/],
'statistics' => [qw/STATS_CURRENT STATS_PERMANENT/],
else
{
$self->logdebug("Connect failed $self->{'serveraddr'}:$self->{'port'}".
": $!");
$@ = "could not connect to [$self->{'serveraddr'}:$self->{'port'}]: $!";
return undef;
}
}
sub disconnect
# Close the connection to the server.
{
my ($self) = @_;
$self->logdebug('Closing connection');
$@ = "no IO object", return undef
unless $self->{'io'};
$self->{'sel'}->remove($self->{'io'}); # remove handle from IO::Select
$@ = "could not close connection: $!", return undef
unless $self->{'io'}->close();
$self->{'disconnected'} = 1;
return 1;
}
sub ping
# try and confirm the server connection is alive
{
my $self = shift;
$@ = "not connected", return undef unless $self->{'io'}->connected;
# there isn't an OCV 'noop' command, use a simple stats request
# - result should be a statistics array, or error
return ($self->statistics(SubCode => STATS_PERMANENT));
}
sub DESTROY
{
my $self = shift;
# sometimes the IO and other 'sub-objects' seem to have been cleaned up
# TODO - figure out why
#warn "$self = \n",
# map {my $s = $self->{$_} || '-'; $s =~ s/[\x00-\x1f\x7f-\xff]/?/g;
# "\t$_ => $s\n"} keys %{$self};
{
local $^W = 0; # ignore IO::Socket warnings
$self->disconnect(@_) if (!$self->{'disconnected'} and
$self->{'io'} and $self->{'io'}->connected);
}
}
sub open { shift-> connect(@_); }
sub close { shift->disconnect(@_); }
sub flush
# try and resynchronise the connection by dumping all pending input
# - probably better to close and (re-)open (see reset method)
{
my $self = shift;
my $buf;
while ($self->{'sel'}->can_read(0) and $self->{'io'}->sysread($buf, 8192))
{
$self->logdebug("flush: discarding [$buf]");
}
"\000"; # true, but "silent" (mainly for the ocv command line util)
}
sub _send
# assumes data is not fragmented
{
my $self = shift;
$@ = "send: not connected", return undef unless $self->{'io'}->connected;
$@ = "send: timeout", return undef
unless $self->{'sel'}->can_write($self->{'timeout'});
# see logdebug() re. logging of sensitive data
$self->logdebug(sprintf("send: %3d [%s]", length($_[0]), $_[0]));
my $r;
eval
{
local $SIG{__WARN__} = 'IGNORE';
local $SIG{ALRM} = sub { die "timeout\n" };
alarm ($self->{'timeout'});
$r = $self->{'io'}->syswrite($_[0], length($_[0]));
alarm (0);
};
chomp ($@), $@ = "send: syswrite: $@", return undef if $@;
$@ = "send: error: $!", return undef unless defined($r);
return $r;
}
sub _recv
# arguments (buf, len): reads len bytes into buf
# assumes data is not fragmented - i.e. if we ask for N bytes, we get N bytes,
# or an error
# - I don't do a dual-read (i.e. read header, extract message length, read
# the rest of the message). I couldn't see the point: once the message
# exchange sequence is messed up, I can no longer trust it.
{
my $self = shift;
$@ = "recv: not connected", return undef unless $self->{'io'}->connected;
$@ = "recv: timeout", return undef
unless $self->{'sel'}->can_read($self->{'timeout'});
my $r;
eval
{
# why do I always feel queasy when it comes to signals under perl :-)
( run in 0.476 second using v1.01-cache-2.11-cpan-5735350b133 )