Business::OnlinePayment::InternetSecure - InternetSecure backend for Business::OnlinePayment


Business-OnlinePayment-InternetSecure documentation Contained in the Business-OnlinePayment-InternetSecure distribution.

Index


Code Index:

NAME

Top

Business::OnlinePayment::InternetSecure - InternetSecure backend for Business::OnlinePayment

SYNOPSIS

Top

  use Business::OnlinePayment;

  $txn = new Business::OnlinePayment 'InternetSecure',
  					merchant_id => '0000';

  $txn->content(
	action		=> 'Normal Authorization',

  	type		=> 'Visa',			# Optional
	card_number	=> '4111 1111 1111 1111',
	expiration	=> '2004-07',
	cvv2		=> '000',			# Optional

	name		=> "Fr\x{e9}d\x{e9}ric Bri\x{e8}re",
	company		=> '',
	address		=> '123 Street',
	city		=> 'Metropolis',
	state		=> 'ZZ',
	zip		=> 'A1A 1A1',
	country		=> 'CA',
	phone		=> '(555) 555-1212',
	email		=> 'fbriere@fbriere.net',

	amount		=> 49.95,
	currency	=> 'CAD',
	taxes		=> 'GST PST',
	description	=> 'Test transaction',
	);

  $txn->submit;

  if ($txn->is_success) {
  	print "Card processed successfully: " . $tx->authorization . "\n";
  } else {
  	print "Card was rejected: " . $tx->error_message . "\n";
  }

DESCRIPTION

Top

Business::OnlinePayment::InternetSecure is an implementation of Business::OnlinePayment that allows for processing online credit card payments through InternetSecure.

See Business::OnlinePayment for more information about the generic Business::OnlinePayment interface.

CREATOR

Top

Object creation is done via Business::OnlinePayment; see its manpage for details. The merchant_id processor option is required, and corresponds to the merchant ID assigned to you by InternetSecure.

METHODS

Top

Transaction setup and transmission

content( CONTENT )

Sets up the data prior to a transaction. CONTENT is an associative array (hash), containing some of the following fields:

action (required)

What to do with the transaction. Only Normal Authorization is supported at the moment.

type

Transaction type, being one of the following:

- Visa
- MasterCard
- American Express
- Discover
- JCB
- CC

(This is actually ignored for the moment, and can be left blank or undefined.)

card_number (required)

Credit card number. Spaces and dashes are automatically removed.

expiration (required)

Credit card expiration date. Since Business::OnlinePayment does not specify any syntax, this module is rather lax regarding what it will accept. The recommended syntax is YYYY-MM, but forms such as MM/YYYY or MMYY are allowed as well.

cvv2

Three- or four-digit verification code printed on the card. This can be left blank or undefined, in which case no check will be performed. Whether or not a transaction will be declined in case of a mismatch depends on the merchant account configuration.

This number may be called Card Verification Value (CVV2), Card Validation Code (CVC2) or Card Identification number (CID), depending on the card issuer.

description

A short description of the transaction. See "Products list syntax" for an alternate syntax that allows a list of products to be specified.

amount (usually required)

Total amount to be billed, excluding taxes if they are to be added separately by InternetSecure.

This field is required if description is a string, but should be left undefined if description contains a list of products instead, as outlined in "Products list syntax".

currency

Currency of all amounts for this order. This can currently be either CAD (default) or USD.

taxes

Taxes to be added automatically to amount by InternetSecure. Available taxes are GST, PST and HST.

This argument can either be a single string of taxes concatenated with spaces (such as GST PST), or a reference to an array of taxes (such as [ "GST", "PST" ]).

name / company / address / city / state / zip / country / phone / email

Customer information. country should be a two-letter code taken from ISO 3166-1.

submit()

Submit the transaction to InternetSecure.

Post-submission methods

is_success()

Returns true if the transaction was submitted successfully.

result_code()

Response code returned by InternetSecure.

error_message()

Error message if the transaction was unsuccessful; undef otherwise. (You should not rely on this to test whether a transaction was successful; use is_success() instead.)

receipt_number()

Receipt number (a string, actually) of this transaction, unique to all InternetSecure transactions.

order_number()

Sales order number of this transaction. This is a number, unique to each merchant, which is incremented by 1 each time.

uuid()

Universally Unique Identifier associated to this transaction. This is a 128-bit value returned as a 36-character string such as f81d4fae-7dec-11d0-a765-00a0c91e6bf6. See RFC 4122 for more details on UUIDs.

guid() is provided as an alias to this method.

authorization()

Authorization code for this transaction.

avs_code() / cvv2_response()

Results of the AVS and CVV2 checks. See the InternetSecure documentation for the list of possible values.

date()

Date and time of the transaction. Format is YYYY/MM/DD hh:mm:ss.

total_amount()

Total amount billed for this order, including taxes.

tax_amounts()

Returns a reference to a hash that maps taxes, which were listed under the taxes argument to submit(), to the amount that was calculated by InternetSecure.

cardholder()

Cardholder's name. This is currently a mere copy of the name field passed to submit().

card_type()

Type of the credit card used for the submitted order, being one of the following:

- Visa
- MasterCard
- American Express
- Discover
- JCB

NOTES

Top

Products list syntax

Optionally, the description field of content() can contain a reference to an array of products, instead of a simple string. Each element of this array represents a different product, and must be a reference to a hash with the following fields:

amount (required)

Unit price of this product.

quantity

Ordered quantity of this product.

sku

Internal code for this product.

description

Description of this product

taxes

Taxes that should be automatically added to this product. If specified, this overrides the taxes field passed to content().

When using a products list, the amount field passed to content() should be left undefined.

Character encoding

When using non-ASCII characters, all data provided to contents() should have been decoded beforehand via the Encode module, unless your data is in ISO-8859-1 and you haven't meddled with the encoding pragma. (Please don't.)

InternetSecure currently does not handle characters outside of ISO-8859-1, so these will be replaced with ? before being transmitted.

EXPORT

Top

None by default.

SEE ALSO

Top

Business::OnlinePayment

AUTHOR

Top

Frédéric Brière, <fbriere@fbriere.net>

COPYRIGHT AND LICENSE

Top


Business-OnlinePayment-InternetSecure documentation Contained in the Business-OnlinePayment-InternetSecure distribution.

package Business::OnlinePayment::InternetSecure;

use 5.008;
use strict;
use warnings;

use Carp;
use Encode;
use Net::SSLeay qw(make_form post_https);
use XML::Simple qw(xml_in xml_out);

use base qw(Business::OnlinePayment Exporter);


our $VERSION = '0.04';


use constant SUCCESS_CODES => qw(2000 90000 900P1);

use constant CARD_TYPES => {
				AM => 'American Express',
				JB => 'JCB',
				MC => 'MasterCard',
				NN => 'Discover',
				VI => 'Visa',
			};


# Convenience functions to avoid undefs and escape products strings
sub _def($) { defined $_[0] ? $_[0] : '' }
sub _esc($) { local $_ = shift; tr/|:/ /s; tr/"`/'/s; return $_ }


sub set_defaults {
	my ($self) = @_;

	$self->server('secure.internetsecure.com');
	$self->port(443);
	$self->path('/process.cgi');

	$self->build_subs(qw(
				receipt_number	order_number	uuid	guid
				date
				card_type	cardholder
				total_amount	tax_amounts
				avs_code	cvv2_response
			));
	
	# Just in case someone tries to call tax_amounts() *before* submit()
	$self->tax_amounts( {} );
}

# Backwards-compatible support for renamed fields
sub avs_response { shift()->avs_code(@_) }
sub sales_number { shift()->order_number(@_) }


# Combine get_fields and remap_fields for convenience.  Unlike OnlinePayment's
# remap_fields, this doesn't modify content(), and can therefore be called
# more than once.  Also, unlike OnlinePayment's get_fields in 3.x, this doesn't
# exclude undefs.
#
sub get_remap_fields {
	my ($self, %map) = @_;

	my %content = $self->content();
	my %data;

	while (my ($to, $from) = each %map) {
		$data{$to} = $content{$from};
	}

	return %data;
}

# Since there's no standard format for expiration dates, we try to do our best
#
sub parse_expdate {
	my ($self, $str) = @_;

	local $_ = $str;

	my ($y, $m);

	if (/^(\d{4})\W(\d{1,2})$/ ||		# YYYY.MM  or  YYYY-M
			/^(\d\d)\W(\d)$/ ||	# YY/M  or  YY-M
			/^(\d\d)[.-](\d\d)$/) {	# YY-MM
		($y, $m) = ($1, $2);
	} elsif (/^(\d{1,2})\W(\d{4})$/ ||	# MM-YYYY  or  M/YYYY
			/^(\d)\W(\d\d)$/ ||	# M/YY  or  M-YY
			/^(\d\d)\/?(\d\d)$/) {	# MM/YY  or  MMYY
		($y, $m) = ($2, $1);
	} else {
		croak "Unable to parse expiration date: $str";
	}

	$y += 2000 if $y < 2000;  # Aren't we glad Y2K is behind us?

	return ($y, $m);
}

# Convert a single product into a product string
#
sub prod_string {
	my ($self, $currency, %data) = @_;

	croak "Missing amount in product" unless defined $data{amount};

	my @flags = ($currency);

	my @taxes;
	if (ref $data{taxes}) {
		@taxes = @{ $data{taxes} };
	} elsif ($data{taxes}) {
		@taxes = split ' ' => $data{taxes};
	}

	foreach (@taxes) {
		croak "Unknown tax code $_" unless /^(GST|PST|HST)$/i;
		push @flags, uc $_;
	}

	if ($self->test_transaction) {
		push @flags, $self->test_transaction < 0 ? 'TESTD' : 'TEST';
	}

	return join '::' =>
				sprintf('%.2f' => $data{amount}),
				$data{quantity} || 1,
				_esc _def $data{sku},
				_esc _def $data{description},
				join('' => map "{$_}" => @flags),
				;
}

# Generate the XML document for this transaction
#
sub to_xml {
	my ($self) = @_;

	my %content = $self->content;

	# Backwards-compatible support for exp_date
	if (exists $content{exp_date} && ! exists $content{expiration}) {
		$content{expiration} = delete $content{exp_date};
		$self->content(%content);
	}

	$self->required_fields(qw(action card_number expiration));

	croak "Unsupported transaction type: $content{type}"
		if $content{type} &&
			! grep lc($content{type}) eq lc($_),
				values %{+CARD_TYPES}, 'CC';
	
	croak 'Unsupported action'
		unless $content{action} =~ /^Normal Authori[zs]ation$/i;
	
	$content{currency} = uc($content{currency} || 'CAD');
	croak "Unknown currency code ", $content{currency}
		unless $content{currency} =~ /^(CAD|USD)$/;
	
	my %data = $self->get_remap_fields(qw(
			xxxCard_Number		card_number

			xxxName			name
			xxxCompany		company
			xxxAddress		address
			xxxCity			city
			xxxProvince		state
			xxxPostal		zip
			xxxCountry		country
			xxxPhone		phone
			xxxEmail		email

			xxxShippingName		ship_name
			xxxShippingCompany	ship_company
			xxxShippingAddress	ship_address
			xxxShippingCity		ship_city
			xxxShippingProvince	ship_state
			xxxShippingPostal	ship_zip
			xxxShippingCountry	ship_country
			xxxShippingPhone	ship_phone
			xxxShippingEmail	ship_email
		));
	
	$data{MerchantNumber} = $self->merchant_id;

	$data{xxxCard_Number} =~ tr/- //d;
	$data{xxxCard_Number} =~ s/^[^3-6]/4/ if $self->test_transaction;

	my ($y, $m) = $self->parse_expdate($content{expiration});
	$data{xxxCCYear} = sprintf '%.4u' => $y;
	$data{xxxCCMonth} = sprintf '%.2u' => $m;

	if (defined $content{cvv2} && $content{cvv2} ne '') {
		$data{CVV2} = 1;
		$data{CVV2Indicator} = $content{cvv2};
	} else {
		$data{CVV2} = 0;
		$data{CVV2Indicator} = '';
	}
	
	if (ref $content{description}) {
		$data{Products} = join '|' => map $self->prod_string(
						$content{currency},
						taxes => $content{taxes},
						%$_),
					@{ $content{description} };
	} else {
		$self->required_fields(qw(amount));
		$data{Products} = $self->prod_string(
					$content{currency},
					taxes       => $content{taxes},
					amount      => $content{amount},
					description => $content{description},
				);
	}

	# The encode() makes sure to a) strip off non-Latin-1 characters, and
	# b) turn off the utf8 flag, which confuses XML::Simple
	encode('ISO-8859-1', xml_out(\%data,
		NoAttr		=> 1,
		RootName	=> 'TranxRequest',
		SuppressEmpty	=> undef,
		XMLDecl		=> '<?xml version="1.0" encoding="iso-8859-1" standalone="yes"?>',
	));
}

# Map the various fields from the response, and put their values into our
# object for retrieval.
#
sub infuse {
	my ($self, $data, %map) = @_;

	while (my ($k, $v) = each %map) {
		no strict 'refs';
		$self->$k($data->{$v});
	}
}

sub extract_tax_amounts {
	my ($self, $response) = @_;

	my %tax_amounts;

	my $products = $response->{Products};
	return unless $products;

	foreach my $node (@$products) {
		my $flags = $node->{flags};
		if ($flags &&
			grep($_ eq '{TAX}', @$flags) &&
			grep($_ eq '{CALCULATED}', @$flags))
		{
			$tax_amounts{ $node->{code} } = $node->{subtotal};
		}
	}

	return %tax_amounts;
}

# Parse the server's response and set various fields
#
sub parse_response {
	my ($self, $response) = @_;

	$self->server_response($response);

	local $/ = "\n";  # Make sure to avoid bug #17687
	
	$response = xml_in($response,
 			ForceArray => [qw(product flag)],
 			GroupTags => { qw(Products product flags flag) },
 			KeyAttr => [],
 			SuppressEmpty => undef,
		);
	
	$self->infuse($response,
			result_code	=> 'Page',
			error_message	=> 'Verbiage',
			authorization	=> 'ApprovalCode',
			avs_code	=> 'AVSResponseCode',
			cvv2_response	=> 'CVV2ResponseCode',

			receipt_number	=> 'ReceiptNumber',
			order_number	=> 'SalesOrderNumber',
			uuid		=> 'GUID',
			guid		=> 'GUID',

			date		=> 'Date',
			cardholder	=> 'xxxName',
			card_type	=> 'CardType',
			total_amount	=> 'TotalAmount',
			);
	
	$self->is_success(scalar grep $self->result_code eq $_, SUCCESS_CODES);

	# Completely undocumented field that sometimes override <Verbiage>
	$self->error_message($response->{Error}) if $response->{Error};

	# Delete error_message if transaction was successful
	$self->error_message(undef) if $self->is_success;
	
	$self->card_type(CARD_TYPES->{$self->card_type});
	
	$self->tax_amounts( { $self->extract_tax_amounts($response) } );

	return $self;
}

sub submit {
	my ($self) = @_;

	croak "Missing required argument 'merchant_id'"
		unless defined $self->{merchant_id};

	my ($page, $response, %headers) = 
		post_https(
				$self->server,
				$self->port,
				$self->path,
				undef,
				make_form(
					xxxRequestMode => 'X',
					xxxRequestData => $self->to_xml,
				)
			);

	croak 'Error connecting to server' unless $page;
	croak 'Server responded, but not in XML' unless $page =~ /^<\?xml/;

	# The response is marked UTF-8, but it's really Latin-1.  Sigh.
	$page =~ s/^(<\?xml.*?) encoding="utf-8"/$1 encoding="iso-8859-1"/si;

	$self->parse_response($page);
}


1;

__END__