| Business-OnlinePayment-InternetSecure documentation | Contained in the Business-OnlinePayment-InternetSecure distribution. |
Business::OnlinePayment::InternetSecure - InternetSecure backend for Business::OnlinePayment
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";
}
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.
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.
Sets up the data prior to a transaction. CONTENT is an associative array (hash), containing some of the following fields:
What to do with the transaction. Only Normal Authorization is supported
at the moment.
Transaction type, being one of the following:
(This is actually ignored for the moment, and can be left blank or undefined.)
Credit card number. Spaces and dashes are automatically removed.
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.
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.
A short description of the transaction. See "Products list syntax" for an alternate syntax that allows a list of products to be specified.
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 of all amounts for this order. This can currently be either
CAD (default) or USD.
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" ]).
Customer information. country should be a two-letter code taken from ISO 3166-1.
Submit the transaction to InternetSecure.
Returns true if the transaction was submitted successfully.
Response code returned by InternetSecure.
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 (a string, actually) of this transaction, unique to all InternetSecure transactions.
Sales order number of this transaction. This is a number, unique to each merchant, which is incremented by 1 each time.
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 code for this transaction.
Results of the AVS and CVV2 checks. See the InternetSecure documentation for the list of possible values.
Date and time of the transaction. Format is YYYY/MM/DD hh:mm:ss.
Total amount billed for this order, including taxes.
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's name. This is currently a mere copy of the name field passed to submit().
Type of the credit card used for the submitted order, being one of the following:
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:
Unit price of this product.
Ordered quantity of this product.
Internal code for this product.
Description of this product
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.
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.
None by default.
Frédéric Brière, <fbriere@fbriere.net>
Copyright (C) 2006 by Frédéric Brière
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.4 or, at your option, any later version of Perl 5 you may have available.
| 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__