Business-OCV

 view release on metacpan or  search on metacpan

OCV.pm  view on Meta::CPAN

# 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/],

OCV.pm  view on Meta::CPAN

	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 )