First pass at adding key files

This commit is contained in:
dsainty
2024-06-17 21:49:12 +10:00
commit aa25e9347f
1274 changed files with 392549 additions and 0 deletions

View File

@ -0,0 +1,787 @@
# ====================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
# GT::Payment::AuthorizeDotNet
# Author: Jason Rhinelander
# CVS Info : 087,071,086,086,085
# $Id: AuthorizeDotNet.pm,v 1.8 2008/09/23 23:55:26 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
# ====================================================================
#
# Description:
# Enter description here.
#
package GT::Payment::Direct::AuthorizeDotNet;
use strict;
use vars qw/%REQUIRED %ERRORS %PARAM $AUTOLOAD %VALID %CURRENCY/;
use Carp;
use Net::SSLeay; # Just to make sure it's available, since GT::WWW doesn't load
use GT::WWW; # Net::SSLeay until attempting to establish the connection.
use Net::hostent;
%ERRORS = (
INVALID => "Invalid value entered for %s: '%s'",
INVALID_PIPE => "Invalid value entered for %s: '%s' ('|' is not permitted)",
INVALID_CURRENCY => "Invalid currency specified for %s: '%s'",
MISSING_FIELDS => 'The following must be set before calling %s: %s',
CHECK_INVALID => 'Invalid type to check: %s',
AUTHORIZE_FIRST => 'You must authorize before capturing',
CAPTURE_REF_NONE => 'No capture reference ID entered',
CAPTURE_REF_INVALID => "Invalid capture reference ID '%s': %s",
HTTP_CONNECTING => 'An error occurred while connecting to the Authorize.net gateway: %s',
HTTP_COMMUNICATING => 'An error occurred while communicating with the Authorize.net gateway: %s',
TEST_CONN_RESOLVE => 'Unable to resolve gateway host: %s',
TEST_CONNECTION => 'Unable to establish a SSL test connection: %s',
DECLINED => 'Credit card declined: %s',
);
# Also required in addition to this list that is set automatically:
# x_Version (3.1), x_Delim_Data (TRUE), x_Type (AUTH_CAPTURE, AUTH_ONLY, etc.),
# x_Method (CC)
%REQUIRED = (
AUTHORIZE => [
'account_username', # x_Login
'account_key', # x_Trans_Key
'credit_card_number', # x_Card_Num
'credit_card_expiry_month', # x_Exp_Date (part 1, month)
'credit_card_expiry_year', # x_Exp_Date (part 2, year)
'charge_total', # x_Amount
'billing_fname',
'billing_lname',
'billing_address_1',
'billing_city',
'billing_state',
'billing_postal_code',
'billing_country',
'billing_phone',
'order_id'
],
CAPTURE => [qw(
account_username
charge_total
capture_reference_id
)],
# Can be used to refund an already settled payment partially or completely
CREDIT => [qw(
account_username
charge_total
capture_reference_id
)],
# Can be used to cancel a previously made payment. This can apply to an authorization,
# capture, or sale - provided, with the latter two, that the payment has not already
# been settled.
VOID => [qw(
account_username
charge_total
capture_reference_id
)]
);
# Scalar ref = use this value,
# Scalar = call this method, use the return value
# undef = the method (auth, capture, etc.) will set it
%PARAM = (
x_Delim_Char => \'|',
x_Delim_Data => \'TRUE',
x_Encap_Char => \'',
# x_ADC_URL => \'FALSE',
x_Test_Request => 'test_mode', # this means nothing real actually happens. Values are 'TRUE' or 'FALSE'.
x_Login => 'account_username', # required
x_Tran_Key => 'account_key', # supposedly required
x_Password => 'account_password', # Optional under AIM (a merchant option)
x_Version => \'3.1', # Authorize.net protocol and response version.
x_Method => \'CC', # Authorize.Net also supports 'ECHECK', but it has different requirements and so should probably be a subclass
# x_Auth_Code => ???, # ???
x_Trans_ID => 'capture_reference_id', # Required for CREDIT, VOID, and PRIOR_AUTH_CAPTURE
x_Card_Num => 'credit_card_number', # required
x_Card_Code => 'credit_card_code', # optional
x_Exp_Date => 'credit_card_expiry', # required - mmyy, mm/yy, or mm/yyyy
x_Amount => 'charge_total', # required
x_Currency_Code => 'currency', # optional - default is 'USD'
x_Invoice_Num => 'order_id', # not strictly required by Authorize.Net, but we require it anyway
x_Description => 'charge_description', # optional
x_Freight => 'charge_freight', # optional
x_Tax => 'charge_tax', # optional
x_Tax_Exempt => 'charge_tax_exempt', # optional - 'TRUE' or 'FALSE' (default)
x_Description => 'charge_description', # optional
x_Duty => 'charge_duty', # optional - valid is "any valid amount"
x_First_Name => 'billing_fname', # required
x_Last_Name => 'billing_lname', # required
x_Company => 'billing_company', # optional
x_Address => 'billing_address', # required - equivelant to a combination of Moneris' billing_address_1 and ..._2
x_City => 'billing_city', # required
x_State => 'billing_state', # required
x_Country => 'billing_country', # required
x_Zip => 'billing_postal_code', # required
x_Phone => 'billing_phone', # required
x_Fax => 'billing_fax', # optional
x_Customer_IP => 'billing_ip', # required; Moneris doesn't have this. It is the IP of whoever placed the order
x_Email => 'confirmation_email', # optional
x_Email_Customer => 'confirmation_confirm', # optional - Whether a confirmation e-mail should be sent to the customer. 'TRUE' or 'FALSE'. Default is configurable through Merchant interface
x_Merchant_Email => 'confirmation_merchant', # optional - if set, an e-mail will be sent here in addition to the normal merchant e-mail address
# x_Recurring_Billing => ???, # optional - TRUE or FALSE (FALSE is default)
# All optional:
x_Ship_To_First_Name => 'shipping_fname',
x_Ship_To_Last_Name => 'shipping_lname',
x_Ship_To_Company => 'shipping_company',
x_Ship_To_Address => 'shipping_address',
x_Ship_To_City => 'shipping_city',
x_Ship_To_State => 'shipping_state',
x_Ship_To_Country => 'shipping_country',
x_Ship_To_Zip => 'shipping_postal_code',
x_Type => undef, # This has to be set by auth(), or capture() to one of:
#
# AUTH_CAPTURE: Auth-Capture is the normal transaction method; a transaction is
# sent to the system for approval, the transaction is approved, the merchant is
# notified of the approval, and the transaction automatically settles at the
# end of the business day without any further action by the merchant.
#
# AUTH_ONLY: Auth-Only stands for Authorization-Only and means obtaining an
# authorization for a certain amount on a customer's credit card without
# actually charging the card. If the money is not captured within 30 days, the
# transaction will expire.
#
# PRIOR_AUTH_CAPTURE: A Prior-Auth-Capture transaction is used to capture funds
# authorized previously using an Auth-Only transaction. Prior-Auth-Capture is
# really just an operation on an already existing transaction.
# Prior-Auth-Capture should only be used on Auth-Only transactions processed
# using the system.
#
# CAPTURE_ONLY: Capture-Only transactions are used when an authorization-only is
# obtained through any means other than the system.
#
# CREDIT: Credits are not processed in real time, but are submitted at
# settlement time with other transactions.
#
# VOID: Voiding a transaction prevents a charge to a credit card/bank account
# from occurring. Voids are performed on existing transactions that have yet to
# be settled.
#
#x_Use_Fraudscreen => ???, # "Not yet supported"
);
my $monetary = '^(?:\d+\.?\d*|\.\d+)$';
# A series of regex for field assignment. References are special values, as follows:
# BOOL => accept a boolean (1 or undef)
# CURRENCY => accept a key of the %CURRENCY hash
#
# undef means any string can be assigned. Note that anything NOT in here CANNOT
# be called as a method.
%VALID = (
account_username => undef,
account_key => undef,
account_password => undef,
capture_reference_id => undef,
credit_card_number => '^\d{13,19}$',
credit_card_expiry_month => '^(?:0?[1-9]|1[012])$',
credit_card_expiry_year => '^\d\d(?:\d\d)?$',
#credit_card_expiry => '^(?:0?[1-9]|1[12])(?:[-/]?\d\d(?:\d\d)?)$', # mmyy, mm/yy, mm-yy, mmyyyy, mm/yyyy, or mm-yyyy
credit_card_code => '^\d{3,4}$', # The 3 or 4 digit code on the back of the credit card (or front of Amer. Exp.)
currency => \'CURRENCY',
charge_total => $monetary,
charge_freight => $monetary,
charge_tax => $monetary,
charge_tax_exempt => \'BOOL',
charge_duty => $monetary,
charge_description => undef,
charge_duty => $monetary,
billing_fname => undef,
billing_lname => undef,
billing_company => undef,
billing_address_1 => undef,
billing_address_2 => undef,
billing_city => undef,
billing_state => undef,
billing_country => undef,
billing_postal_code => undef,
billing_phone => undef,
billing_fax => undef,
billing_ip => '^(?:(?:1?\d?\d|2(?:[0-4]\d|5[0-5]))\.){3}(?:1?\d?\d|2(?:[0-4]\d|5[0-5]))$',
confirmation_email => '^\S+@([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]+$',
confirmation_confirm => \'BOOL',
confirmation_merchant => '^\S+@([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]+$',
shipping_fname => undef,
shipping_lname => undef,
shipping_company => undef,
shipping_address => undef,
shipping_city => undef,
shipping_state => undef,
shipping_country => undef,
shipping_postal_code => undef,
order_id => '^.{1,20}$',
test_mode => \'BOOL'
);
# The official list of supported currencies:
%CURRENCY = (
AFA => 'Afghani (Afghanistan)',
DZD => 'Algerian Dinar (Algeria)',
ADP => 'Andorran Peseta (Andorra)',
ARS => 'Argentine Peso (Argentina)',
AMD => 'Armenian Dram (Armenia)',
AWG => 'Aruban Guilder (Aruba)',
AUD => 'Australian Dollar (Australia)',
AZM => 'Azerbaijanian Manat (Azerbaijan)',
BSD => 'Bahamian Dollar (Bahamas)',
BHD => 'Bahraini Dinar (Bahrain)',
THB => 'Baht (Thailand)',
PAB => 'Balboa (Panama)',
BBD => 'Barbados Dollar (Barbados)',
BYB => 'Belarussian Ruble (Belarus)',
BEF => 'Belgian Franc (Belgium)',
BZD => 'Belize Dollar (Belize)',
BMD => 'Bermudian Dollar (Bermuda)',
VEB => 'Bolivar (Venezuela)',
BOB => 'Boliviano (Bolivia)',
BRL => 'Brazilian Real (Brazil)',
BND => 'Brunei Dollar (Brunei Darussalam)',
BGN => 'Bulgarian Lev (Bulgaria)',
BIF => 'Burundi Franc (Burundi)',
CAD => 'Canadian Dollar (Canada)',
CVE => 'Cape Verde Escudo (Cape Verde)',
KYD => 'Cayman Islands Dollar (Cayman Islands)',
GHC => 'Cedi (Ghana)',
XOF => 'CFA Franc BCEAO (Guinea-Bissau)',
XAF => 'CFA Franc BEAC (Central African Republic)',
XPF => 'CFP Franc (New Caledonia)',
CLP => 'Chilean Peso (Chile)',
COP => 'Colombian Peso (Colombia)',
KMF => 'Comoro Franc (Comoros)',
BAM => 'Convertible Marks (Bosnia And Herzegovina)',
NIO => 'Cordoba Oro (Nicaragua)',
CRC => 'Costa Rican Colon (Costa Rica)',
CUP => 'Cuban Peso (Cuba)',
CYP => 'Cyprus Pound (Cyprus)',
CZK => 'Czech Koruna (Czech Republic)',
GMD => 'Dalasi (Gambia)',
DKK => 'Danish Krone (Denmark)',
MKD => 'Denar (The Former Yugoslav Republic Of Macedonia)',
DEM => 'Deutsche Mark (Germany)',
AED => 'Dirham (United Arab Emirates)',
DJF => 'Djibouti Franc (Djibouti)',
STD => 'Dobra (Sao Tome And Principe)',
DOP => 'Dominican Peso (Dominican Republic)',
VND => 'Dong (Vietnam)',
GRD => 'Drachma (Greece)',
XCD => 'East Caribbean Dollar (Grenada)',
EGP => 'Egyptian Pound (Egypt)',
SVC => 'El Salvador Colon (El Salvador)',
ETB => 'Ethiopian Birr (Ethiopia)',
EUR => 'Euro (Europe)',
FKP => 'Falkland Islands Pound (Falkland Islands)',
FJD => 'Fiji Dollar (Fiji)',
HUF => 'Forint (Hungary)',
CDF => 'Franc Congolais (The Democratic Republic Of Congo)',
FRF => 'French Franc (France)',
GIP => 'Gibraltar Pound (Gibraltar)',
XAU => 'Gold',
HTG => 'Gourde (Haiti)',
PYG => 'Guarani (Paraguay)',
GNF => 'Guinea Franc (Guinea)',
GWP => 'Guinea-Bissau Peso (Guinea-Bissau)',
GYD => 'Guyana Dollar (Guyana)',
HKD => 'Hong Kong Dollar (Hong Kong)',
UAH => 'Hryvnia (Ukraine)',
ISK => 'Iceland Krona (Iceland)',
INR => 'Indian Rupee (India)',
IRR => 'Iranian Rial (Islamic Republic Of Iran)',
IQD => 'Iraqi Dinar (Iraq)',
IEP => 'Irish Pound (Ireland)',
ITL => 'Italian Lira (Italy)',
JMD => 'Jamaican Dollar (Jamaica)',
JOD => 'Jordanian Dinar (Jordan)',
KES => 'Kenyan Shilling (Kenya)',
PGK => 'Kina (Papua New Guinea)',
LAK => 'Kip (Lao People\'s Democratic Republic)',
EEK => 'Kroon (Estonia)',
HRK => 'Kuna (Croatia)',
KWD => 'Kuwaiti Dinar (Kuwait)',
MWK => 'Kwacha (Malawi)',
ZMK => 'Kwacha (Zambia)',
AOR => 'Kwanza Reajustado (Angola)',
MMK => 'Kyat (Myanmar)',
GEL => 'Lari (Georgia)',
LVL => 'Latvian Lats (Latvia)',
LBP => 'Lebanese Pound (Lebanon)',
ALL => 'Lek (Albania)',
HNL => 'Lempira (Honduras)',
SLL => 'Leone (Sierra Leone)',
ROL => 'Leu (Romania)',
BGL => 'Lev (Bulgaria)',
LRD => 'Liberian Dollar (Liberia)',
LYD => 'Libyan Dinar (Libyan Arab Jamahiriya)',
SZL => 'Lilangeni (Swaziland)',
LTL => 'Lithuanian Litas (Lithuania)',
LSL => 'Loti (Lesotho)',
LUF => 'Luxembourg Franc (Luxembourg)',
MGF => 'Malagasy Franc (Madagascar)',
MYR => 'Malaysian Ringgit (Malaysia)',
MTL => 'Maltese Lira (Malta)',
TMM => 'Manat (Turkmenistan)',
FIM => 'Markka (Finland)',
MUR => 'Mauritius Rupee (Mauritius)',
MZM => 'Metical (Mozambique)',
MXN => 'Mexican Peso (Mexico)',
MXV => 'Mexican Unidad de Inversion (Mexico)',
MDL => 'Moldovan Leu (Republic Of Moldova)',
MAD => 'Moroccan Dirham (Morocco)',
BOV => 'Mvdol (Bolivia)',
NGN => 'Naira (Nigeria)',
ERN => 'Nakfa (Eritrea)',
NAD => 'Namibia Dollar (Namibia)',
NPR => 'Nepalese Rupee (Nepal)',
ANG => 'Netherlands (Netherlands)',
NLG => 'Netherlands Guilder (Netherlands)',
YUM => 'New Dinar (Yugoslavia)',
ILS => 'New Israeli Sheqel (Israel)',
AON => 'New Kwanza (Angola)',
TWD => 'New Taiwan Dollar (Province Of China Taiwan)',
ZRN => 'New Zaire (Zaire)',
NZD => 'New Zealand Dollar (New Zealand)',
BTN => 'Ngultrum (Bhutan)',
KPW => 'North Korean Won (Democratic People\'s Republic Of Korea)',
NOK => 'Norwegian Krone (Norway)',
PEN => 'Nuevo Sol (Peru)',
MRO => 'Ouguiya (Mauritania)',
TOP => 'Pa\'anga (Tonga)',
PKR => 'Pakistan Rupee (Pakistan)',
XPD => 'Palladium',
MOP => 'Pataca (Macau)',
UYU => 'Peso Uruguayo (Uruguay)',
PHP => 'Philippine Peso (Philippines)',
XPT => 'Platinum',
PTE => 'Portuguese Escudo (Portugal)',
GBP => 'Pound Sterling (United Kingdom)',
BWP => 'Pula (Botswana)',
QAR => 'Qatari Rial (Qatar)',
GTQ => 'Quetzal (Guatemala)',
ZAL => 'Rand (Financial) (Lesotho)',
ZAR => 'Rand (South Africa)',
OMR => 'Rial Omani (Oman)',
KHR => 'Riel (Cambodia)',
MVR => 'Rufiyaa (Maldives)',
IDR => 'Rupiah (Indonesia)',
RUB => 'Russian Ruble (Russian Federation)',
RUR => 'Russian Ruble (Russian Federation)',
RWF => 'Rwanda Franc (Rwanda)',
SAR => 'Saudi Riyal (Saudi Arabia)',
ATS => 'Schilling (Austria)',
SCR => 'Seychelles Rupee (Seychelles)',
XAG => 'Silver',
SGD => 'Singapore Dollar (Singapore)',
SKK => 'Slovak Koruna (Slovakia)',
SBD => 'Solomon Islands Dollar (Solomon Islands)',
KGS => 'Som (Kyrgyzstan)',
SOS => 'Somali Shilling (Somalia)',
ESP => 'Spanish Peseta (Spain)',
LKR => 'Sri Lanka Rupee (Sri Lanka)',
SHP => 'St Helena Pound (St Helena)',
ECS => 'Sucre (Ecuador)',
SDD => 'Sudanese Dinar (Sudan)',
SRG => 'Surinam Guilder (Suriname)',
SEK => 'Swedish Krona (Sweden)',
CHF => 'Swiss Franc (Switzerland)',
SYP => 'Syrian Pound (Syrian Arab Republic)',
TJR => 'Tajik Ruble (Tajikistan)',
BDT => 'Taka (Bangladesh)',
WST => 'Tala (Samoa)',
TZS => 'Tanzanian Shilling (United Republic Of Tanzania)',
KZT => 'Tenge (Kazakhstan)',
TPE => 'Timor Escudo (East Timor)',
SIT => 'Tolar (Slovenia)',
TTD => 'Trinidad and Tobago Dollar (Trinidad And Tobago)',
MNT => 'Tugrik (Mongolia)',
TND => 'Tunisian Dinar (Tunisia)',
TRL => 'Turkish Lira (Turkey)',
UGX => 'Uganda Shilling (Uganda)',
ECV => 'Unidad de Valor Constante (Ecuador)',
CLF => 'Unidades de fomento (Chile)',
USN => 'US Dollar (Next day) (United States)',
USS => 'US Dollar (Same day) (United States)',
USD => 'US Dollar (United States)',
UZS => 'Uzbekistan Sum (Uzbekistan)',
VUV => 'Vatu (Vanuatu)',
KRW => 'Won (Republic Of Korea)',
YER => 'Yemeni Rial (Yemen)',
JPY => 'Yen (Japan)',
CNY => 'Yuan Renminbi (China)',
ZWD => 'Zimbabwe Dollar (Zimbabwe)',
PLN => 'Zloty (Poland)'
);
use constants
POST_HOST => 'secure.authorize.net',
POST_PATH => '/gateway/transact.dll';
sub new {
# -----------------------------------------------------------------------------
my $class = shift;
$class = ref $class if ref $class;
my $self = { debug => 0 };
bless $self, $class;
$self->debug("New $class object created") if $self->{debug} and $self->{debug} >= 2;
while (@_) {
my ($method, $value) = splice @_, 0, 2;
$self->debug("Found '$method' => '$value' in new() arguments - calling \$self->$method($value)") if $self->{debug} and $self->{debug} >= 2;
$self->$method($value);
}
return $self;
}
DESTROY { }
sub errcode {
# -----------------------------------------------------------------------------
my $self = shift;
$self->{errcode};
}
sub error {
# -----------------------------------------------------------------------------
my $self = shift;
if (@_) {
my $code = shift;
$self->{errcode} = $code;
my $error = sprintf($ERRORS{$code} || $code, @_);
$self->debug($error) if $self->{debug};
$self->{error} = $error;
return undef;
}
$self->{error};
}
sub clear_error {
my $self = shift;
$self->{error} = $self->{errcode} = undef;
$self->debug("Clearing error code") if $self->{debug} >= 2;
}
sub fatal {
# -----------------------------------------------------------------------------
my ($self, $code) = splice @_, 0, 2;
my $error = sprintf($ERRORS{$code} || $code, @_);
my $me = ref $self || $self;
croak "$me: @_";
}
sub debug {
# -----------------------------------------------------------------------------
my $self = @_ > 1 ? shift : __PACKAGE__;
$self = ref $self if ref $self;
carp "$self: @_";
}
sub debug_level {
# -----------------------------------------------------------------------------
my $self = shift;
if (@_) {
$self->{debug} = int shift;
}
$self->{debug};
}
AUTOLOAD {
my ($method) = $AUTOLOAD =~ /([^:]+)$/;
if (exists $VALID{$method}) {
no strict 'refs';
my $validation = $VALID{$method};
*$method = sub {
my $self = shift;
if (@_) {
$self->{error} = undef;
if (ref $validation) {
if ($$validation eq 'BOOL') {
if (shift) {
$self->debug("Setting '$method' option to true") if $self->{debug};
$self->{$method} = 'TRUE';
}
else {
$self->debug("Setting '$method' option to false") if $self->{debug};
$self->{$method} = 'FALSE';
}
}
elsif ($$validation eq 'CURRENCY') {
my $value = uc shift;
unless (exists $CURRENCY{$value}) {
$self->debug("Not setting '$method' to '$value' (Invalid currency code)") if $self->{debug};
return $self->error(INVALID_CURRENCY => $method, $value);
}
$self->debug("Setting '$method' to '$value' (Currency code accepted)") if $self->{debug};
$self->{$method} = $value;
}
}
elsif (defined $validation) {
my $value = shift;
$value =~ s/\s+//g if $method eq 'credit_card_number';
if ($value =~ /$validation/) {
if (index($value, '|') >= 0) {
$self->debug("Not setting '$method' to '$value' (Value contains illegal character '|')") if $self->{debug};
return $self->error(INVALID_PIPE => $method, $value);
}
$self->debug("Setting '$method' to '$value' (Validation regex: $validation passed)") if $self->{debug};
$self->{$method} = $value;
}
else {
$self->debug("Not setting '$method' to '$value' (Validation regex: $validation failed)") if $self->{debug};
return $self->error(INVALID => $method, $value);
}
}
else {
my $value = shift;
if (index($value, '|') >= 0) {
$self->debug("Not setting '$method' to '$value' (Value contains illegal character '|')") if $self->{debug};
return $self->error(INVALID_PIPE => $method, $value);
}
$self->debug("Setting '$method' to '$value' (No validation regex)") if $self->{debug};
$self->{$method} = $value;
}
return 1;
}
my $value = $self->{$method};
$self->debug("Retrieving '$method': '$value'") if $self->{debug} and $self->{debug} >= 2;
return $value;
};
}
else {
croak qq|Can't locate object method "$method" via package "| . (ref $_[0] or $_[0] or __PACKAGE__) . qq|"|;
}
goto &$method;
}
sub billing_address {
my $self = shift;
my ($one, $two) = ($self->billing_address_1, $self->billing_address_2);
return unless defined $one;
return $two ? $one . "\n" . $two : $one;
}
sub credit_card_expiry {
my $self = shift;
my ($month, $year) = ($self->credit_card_expiry_month, $self->credit_card_expiry_year);
return unless defined $month and defined $year;
return $month . '/' . $year;
}
sub check {
# -----------------------------------------------------------------------------
# Checks that all necessary data is provided for an authorize, capture, or sale.
# Takes one argument - 'authorize', 'capture', or 'sale', though 'sale' is
# really no different from 'authorize'.
my ($self, $type) = @_;
$self->clear_error();
$self->fatal(CHECK_INVALID => $type) unless $type =~ /^(?:authorize|capture|sale)$/i;
my @bad;
for my $field (@{$REQUIRED{uc(lc $type eq 'sale' ? 'authorize' : $type)}}) {
my $value = $self->$field();
if ($field eq 'charge_total') {
push @bad, $field if $value <= 0;
}
else {
push @bad, $field if not defined $value or not length $value;
}
}
if (@bad) {
$self->error(MISSING_FIELDS => $type => "@bad");
return undef;
}
return 1;
}
sub response {
# -----------------------------------------------------------------------------
my $self = shift;
$self->{response};
}
sub _init_www {
# -----------------------------------------------------------------------------
my ($self, $type) = @_;
my $www = $self->{www} ||= GT::WWW->new(debug => $self->{debug});
$www->url('https://' . POST_HOST . POST_PATH);
my @param;
while (my ($key, $value) = each %PARAM) {
if (ref $value) {
push @param, $key, $$value;
}
elsif ($key eq 'x_Type') {
push @param, 'x_Type', $type;
}
else {
my $val = $self->$value();
push @param, $key, $val if defined $val;
}
}
$www->header(Connection => 'close');
$www->parameters(@param);
return $www;
}
sub post_payment_request {
# -----------------------------------------------------------------------------
my ($self, $type) = @_;
my $www = $self->_init_www($type);
my $response = $www->post;
unless ($response) {
return $self->error(HTTP_CONNECTING => $www->error);
}
unless ($response->status) {
return $self->error(HTTP_COMMUNICATING => int($response->status()) . ' ' . $response->status());
}
my @fields = split /\|/, "$response";
$self->{response} = { fields => \@fields };
$self->{response}->{code} = $fields[0]; # 1 = Approved, 2 = Denied, 3 = Error
$self->{response}->{reason_code} = $fields[2];
$self->{response}->{reason_text} = $fields[3];
$self->{response}->{approval_code} = $fields[4]; # The six-digit alphanumeric authorization or approval code
$self->{response}->{avs_code} = $fields[5]; # See the AIM Implementation Guide
# "This number identifies the transaction in the system and can be used to
# submit a modification of this transaction at a later time, such as voiding,
# crediting or capturing the transaction."
$self->{response}->{trans_id} = $fields[6];
# The 8th through 37th fields are just the form input echoed back.
# 38 is a "system-generated MD5 hash that may be validated by the merchant to
# authenticate a transaction response received from the gateway"
$self->{response}->{md5_hash} = $fields[37];
# 39 "indicates the results of Card Code verification" - see the AIM Implementation Guide
$self->{response}->{card_code_response} = $fields[38];
$self->{transaction_error_code} = $self->{response}->{reason_code};
# What we return is:
# 1 - Payment request successful
# 0 - Payment request declined
# -1 - An error occurred
if ($self->{response}->{code} == 1) {
my @receipt;
push @receipt, 'Approval Code', $self->{response}->{approval_code};
push @receipt, 'AVS Code', $self->{response}->{avs_code} if $self->{response}->{avs_code};
push @receipt, 'Transaction ID', $self->{response}->{trans_id};
push @receipt, 'Card Code Response', $self->{response}->{card_code_response} if $self->{response}->{card_code_response};
$self->{response}->{receipt} = \@receipt;
}
return $self->{response}->{code} == 1 ? 1 : $self->{response}->{code} == 2 ? 0 : -1;
}
sub authorize {
# -----------------------------------------------------------------------------
my $self = shift;
$self->debug("Performing authorization") if $self->{debug};
$self->{type} = 'AUTH_ONLY';
$self->check('authorize') or return undef;
my $ret = $self->post_payment_request('AUTH_ONLY');
# Set the transaction ID as our 'capture_reference_id', so that this object can
# capture() immediately after authorize()ing.
$self->{capture_reference_id} = $self->{response}->{trans_id};
return $ret;
}
sub capture {
# -----------------------------------------------------------------------------
my $self = shift;
$self->debug("Performing prior-auth capture") if $self->{debug};
$self->{type} = 'PRIOR_AUTH_CAPTURE';
$self->check('capture') or return undef;
return $self->post_payment_request('PRIOR_AUTH_CAPTURE');
}
sub sale {
# -----------------------------------------------------------------------------
my $self = shift;
$self->debug("Performing auth-capture (sale)") if $self->{debug};
$self->{type} = 'AUTH_CAPTURE';
$self->check('sale') or return undef;
return $self->post_payment_request('AUTH_CAPTURE');
}
sub test_connection {
# -----------------------------------------------------------------------------
# Call this on your object when setting up a payment system to verify that the
# payment gateway is reachable. This does a simple HEAD request of
# http://secure.authorize.net - if 200 status is returned, it is assumed to be
# reachable.
my $self = shift;
my $www = $self->{www} ||= GT::WWW->new();
# We're just going to do a HEAD request to make sure we can properly establish
# an HTTPS connection.
unless (gethost(POST_HOST)) {
return $self->error(TEST_CONN_RESOLVE => POST_HOST);
}
$www->url('https://' . POST_HOST);
my $response = $www->head();
unless ($response and my $status = $response->status) {
return $self->error(TEST_CONNECTION => ($response ? "Server response: " . int($status) . " " . $status : $www->error));
}
$self->{connection_tested} = 1;
return 1;
}
#sub test_account {
# -----------------------------------------------------------------------------
1;

View File

@ -0,0 +1,773 @@
# ====================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
# GT::Payment::Direct::Moneris
# Author: Jason Rhinelander
# CVS Info : 087,071,086,086,085
# $Id: Moneris.pm,v 1.12 2008/09/23 23:55:26 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
# ====================================================================
#
# Description:
# Handle payment processing via Moneris eSelect Plus.
#
package GT::Payment::Direct::Moneris;
use strict;
use vars qw/@ISA $ERRORS $VERSION %REQUIRED %RESPONSE $AUTOLOAD %BRANDS %NAME_MAP/;
use GT::Base;
use GT::WWW;
use GT::WWW::https;
$VERSION = sprintf "%d.%03d", q$Revision: 1.12 $ =~ /(\d+)\.(\d+)/;
use constants
LIVE_SERVER => 'https://www3.moneris.com:43924/gateway2/servlet/MpgRequest',
TEST_SERVER => 'https://esqa.moneris.com:43924/gateway2/servlet/MpgRequest',
TIMEOUT => 60;
@ISA = 'GT::Base';
%REQUIRED = (
AUTHORIZE => [qw(
account_token
account_token2
credit_card_number
credit_card_expiry_month
credit_card_expiry_year
charge_total
billing_fname
billing_lname
billing_address
billing_city
billing_state
billing_postal_code
billing_country
order_id
)],
CAPTURE => [qw(
account_token
charge_total
capture_reference_id
order_id
)]
);
# The following credit card brands are supported by Moneris
%BRANDS = (
VISA => 1,
MASTERCARD => 1, # Can also be passed as 'MC'
AMERICAN_EXPRESS => 1, # Can also be passed as 'AMEX'
DISCOVER => 1, # Can also be passed as 'DISC'
NOVA => 1,
DINERS => 1,
EUROCARD => 1
);
%RESPONSE = (
0 => 'Approved, account balances included',
1 => 'Approved, account balances not included',
2 => 'Approved, country club',
3 => 'Approved, maybe more ID',
4 => 'Approved, pending ID (sign paper draft)',
5 => 'Approved, blind',
6 => 'Approved, VIP',
7 => 'Approved, administrative transaction',
8 => 'Approved, national NEG file hit OK',
9 => 'Approved, commercial',
23 => 'Amex - credit approval',
24 => 'Amex 77 - credit approval',
25 => 'Amex - credit approval ',
26 => 'Amex - credit approval ',
27 => 'Credit card approval',
28 => 'VIP Credit Approved',
29 => 'Credit Response Acknowledgement',
50 => 'Decline',
51 => 'Expired Card',
52 => 'PIN retries exceeded',
53 => 'No sharing',
54 => 'No security module',
55 => 'Invalid transaction',
56 => 'No Support',
57 => 'Lost or stolen card',
58 => 'Invalid status',
59 => 'Restricted Card',
60 => 'No Chequing account',
60 => 'No Savings account',
61 => 'No PBF',
62 => 'PBF update error',
63 => 'Invalid authorization type',
64 => 'Bad Track 2',
65 => 'Adjustment not allowed',
66 => 'Invalid credit card advance increment',
67 => 'Invalid transaction date',
68 => 'PTLF error',
69 => 'Bad message error',
70 => 'No IDF',
71 => 'Invalid route authorization',
72 => 'Card on National NEG file ',
73 => 'Invalid route service (destination)',
74 => 'Unable to authorize',
75 => 'Invalid PAN length',
76 => 'Low funds',
77 => 'Pre-auth full',
78 => 'Duplicate transaction',
79 => 'Maximum online refund reached',
80 => 'Maximum offline refund reached',
81 => 'Maximum credit per refund reached',
82 => 'Number of times used exceeded',
83 => 'Maximum refund credit reached',
84 => 'Duplicate transaction - authorization number has already been corrected by host.',
85 => 'Inquiry not allowed',
86 => 'Over floor limit ',
87 => 'Maximum number of refund credit by retailer',
88 => 'Place call ',
89 => 'CAF status inactive or closed',
90 => 'Referral file full',
91 => 'NEG file problem',
92 => 'Advance less than minimum',
93 => 'Delinquent',
94 => 'Over table limit',
95 => 'Amount over maximum',
96 => 'PIN required',
97 => 'Mod 10 check failure',
98 => 'Force Post',
99 => 'Bad PBF',
100 => 'Unable to process transaction',
101 => 'Place call',
102 => '',
103 => 'NEG file problem',
104 => 'CAF problem',
105 => 'Card not supported',
106 => 'Amount over maximum',
107 => 'Over daily limit',
108 => 'CAF Problem',
109 => 'Advance less than minimum',
110 => 'Number of times used exceeded',
111 => 'Delinquent',
112 => 'Over table limit',
113 => 'Timeout',
115 => 'PTLF error',
121 => 'Administration file problem',
122 => 'Unable to validate PIN: security module down',
150 => 'Merchant not on file',
200 => 'Invalid account',
201 => 'Incorrect PIN',
202 => 'Advance less than minimum',
203 => 'Administrative card needed',
204 => 'Amount over maximum ',
205 => 'Invalid Advance amount',
206 => 'CAF not found',
207 => 'Invalid transaction date',
208 => 'Invalid expiration date',
209 => 'Invalid transaction code',
210 => 'PIN key sync error',
212 => 'Destination not available',
251 => 'Error on cash amount',
252 => 'Debit not supported',
426 => 'AMEX - Denial 12',
427 => 'AMEX - Invalid merchant',
429 => 'AMEX - Account error',
430 => 'AMEX - Expired card',
431 => 'AMEX - Call Amex',
434 => 'AMEX - Call 03',
435 => 'AMEX - System down',
436 => 'AMEX - Call 05',
437 => 'AMEX - Declined',
438 => 'AMEX - Declined',
439 => 'AMEX - Service error',
440 => 'AMEX - Call Amex',
441 => 'AMEX - Amount error',
475 => 'CREDIT CARD - Invalid expiration date',
476 => 'CREDIT CARD - Invalid transaction, rejected',
477 => 'CREDIT CARD - Refer Call',
478 => 'CREDIT CARD - Decline, Pick up card, Call',
479 => 'CREDIT CARD - Decline, Pick up card',
480 => 'CREDIT CARD - Decline, Pick up card',
481 => 'CREDIT CARD - Decline',
482 => 'CREDIT CARD - Expired Card',
483 => 'CREDIT CARD - Refer',
484 => 'CREDIT CARD - Expired card - refer',
485 => 'CREDIT CARD - Not authorized',
486 => 'CREDIT CARD - CVV Cryptographic error',
487 => 'CREDIT CARD - Invalid CVV',
489 => 'CREDIT CARD - Invalid CVV',
490 => 'CREDIT CARD - Invalid CVV',
800 => 'Bad format',
801 => 'Bad data',
802 => 'Invalid Clerk ID',
809 => 'Bad close ',
810 => 'System timeout',
811 => 'System error',
821 => 'Bad response length',
877 => 'Invalid PIN block',
878 => 'PIN length error',
880 => 'Final packet of a multi-packet transaction',
881 => 'Intermediate packet of a multi-packet transaction',
889 => 'MAC key sync error',
898 => 'Bad MAC value',
899 => 'Bad sequence number - resend transaction',
900 => 'Capture - PIN Tries Exceeded',
901 => 'Capture - Expired Card',
902 => 'Capture - NEG Capture',
903 => 'Capture - CAF Status 3',
904 => 'Capture - Advance < Minimum',
905 => 'Capture - Num Times Used',
906 => 'Capture - Delinquent',
907 => 'Capture - Over Limit Table',
908 => 'Capture - Amount Over Maximum',
909 => 'Capture - Capture',
960 => 'Initialization failure - merchant number mismatch',
961 => 'Initialization failure -pinpad mismatch',
963 => 'No match on Poll code',
964 => 'No match on Concentrator ID',
965 => 'Invalid software version number',
966 => 'Duplicate terminal name'
);
# This contains a list of generic methods that take any value, and are handled
# via AUTOLOAD.
%NAME_MAP = (
billing_fname => 1,
billing_lname => 1,
billing_company => 1,
billing_address_1 => 1,
billing_address_2 => 1,
billing_city => 1,
billing_state => 1,
billing_postal_code => 1,
billing_country => 1,
billing_email => 1,
billing_phone => 1,
billing_fax => 1,
billing_note => 1,
order_id => 1,
account_token => 1,
account_token2 => 1
);
$ERRORS = {
CARD_NUMBER_NONE => "No credit card number entered",
CARD_NUMBER_NUMERIC => "Credit card number is not numeric",
CARD_NUMBER_LENGTH => "Invalid credit card number: Invalid length",
CARD_NUMBER_INVALID => "The credit card number entered is not valid: %s",
BRAND_NONE => "No credit card brand entered",
BRAND_INVALID => "Credit card brand '%s' is invalid or not supported%s",
EXPIRY_INVALID => "Invalid expiry date entered: %s",
EXPIRY_MONTH_NONE => "Empty expiry month entered",
EXPIRY_MONTH_NUMERIC => "Expiry month must be numeric: %s",
EXPIRY_MONTH_INVALID => "Invalid expiry month entered: %s",
EXPIRY_YEAR_NONE => "Empty expiry year entered",
EXPIRY_YEAR_NUMERIC => "Expiry year must be numeric: %s",
EXPIRY_YEAR_4_DIGIT => "Expiry year must be 4 digits: %s",
EXPIRY_YEAR_INVALID => "Invalid expiry year entered: %s",
TOTAL_NONE => "No total amount entered",
TOTAL_NUMERIC => "Total amount entered is not numeric: %s",
EMAIL_NONE => "No e-mail address entered",
EMAIL_INVALID => "Invalid e-mail address '%s' entered: %s",
GENERIC_NONE => "No value entered for %s",
GENERIC_INVALID => "Invalid value '%s' for %s: %s",
MISSING_FIELDS => "The following must be set before calling %s: %s",
TYPE_INVALID => "Invalid/unsupported transaction type: %s",
AUTHORIZE_FIRST => "You must authorize before capturing",
CAPTURE_REF_NONE => "No capture reference ID entered",
CAPTURE_REF_INVALID => "Invalid capture reference ID '%s': %s",
FIELD_MISSING => "The transaction server reported missing fields: %s",
FIELD_INVALID => "The transaction server reported invalid data: %s",
TRANSACTION_INVALID => "Setup problem: Invalid store information: %s",
TRANSACTION_PROBLEM => "A transaction server error has occurred: %s",
TRANSACTION_BAD => "You attempted to capture without authorizing first: %s",
VERSION_TOO_OLD => "The current version of the software is outdated: %s",
DECLINED => "Credit card declined: %s",
ERROR => "Credit card processing error: %s",
UNKNOWN => "The transaction server returned an unrecognized response: %s"
};
sub new {
my $class = shift;
$class = ref $class if ref $class;
my $self = {};
bless $self, $class;
$self->debug("New $class object created") if $self->{_debug} and $self->{_debug} >= 2;
while (@_) {
my ($method, $value) = splice @_, 0, 2;
$self->debug("Found '$method' => '$value' in new() arguments - calling \$self->$method($value)") if $self->{_debug} and $self->{_debug} >= 2;
$self->$method($value);
}
return $self;
}
AUTOLOAD {
my ($method) = $AUTOLOAD =~ /([^:]+)$/;
if (exists $NAME_MAP{$method}) {
no strict 'refs';
*$method = sub {
my $self = shift;
if (@_) {
my $value = shift;
$self->debug("Setting '$method' to '$value'") if $self->{_debug};
defined $value or $self->warn(GENERIC_NONE => $method), return undef;
$self->{$method} = $value;
return 1;
}
$self->debug("Retrieving '$method': '$self->{$method}'") if $self->{_debug} and $self->{_debug} >= 2;
return $self->{$method};
};
}
else {
$method = "$ISA[0]::$method"; # Let GT::Base deal with it for now
}
goto &$method;
}
sub credit_card_number {
my $self = shift;
if (@_) {
my $ccnum = shift;
$self->debug("Setting 'credit_card_number' to '$ccnum'") if $self->{_debug};
unless (defined $ccnum and $ccnum =~ /\S/) {
$self->warn('CARD_NUMBER_NONE');
return undef;
}
$ccnum =~ y/ //d;
if ($ccnum =~ /\D/) {
$self->warn(CARD_NUMBER_NUMERIC => $ccnum);
return undef;
}
if (length($ccnum) < 13 or length($ccnum) > 20) {
$self->warn('CARD_NUMBER_LENGTH');
}
$self->{credit_card_number} = $ccnum;
return 1;
}
my $return = $self->{credit_card_number};
$self->debug("Retrieving 'credit_card_number': '$return'") if $self->{_debug} and $self->{_debug} >= 2;
return $return;
}
# Takes \d\d-\d\d\d\d or \d\d/\d\d\d\d,
# passes them to credit_card_expiry_month and ..._year
# Return 1 if they were set properly, undef otherwise.
# Without arguments, returns: \d\d/\d\d\d\d if month and year are set, undef
# otherwise.
sub credit_card_expiry {
my $self = shift;
if (@_ >= 2) {
my $exp = shift;
$exp =~ y/ //d;
if (my ($m, $y) = $exp =~ m|^(\d?\d)[/-](\d\d\d\d)$|) {
$self->credit_card_expiry_month($m) or return undef;
$self->credit_card_expiry_year($y) or return undef;
return 1;
}
else {
$self->warn(EXPIRY_INVALID => $exp);
return undef;
}
}
my ($m, $y) = ($self->credit_card_expiry_month, $self->credit_card_expiry_year);
return undef unless defined $m and defined $y;
return "$m/$y";
}
sub _cc_exp {
# -----------------------------------------------------------------------------
# Returns the credit card expiry in YYMM format, as this is how Moneris takes
# it.
#
my $self = shift;
my ($m, $y) = ($self->credit_card_expiry_month, $self->credit_card_expiry_year);
return substr($y, -2) . $m;
}
sub credit_card_expiry_month {
my $self = shift;
if (@_) {
my $expm = shift;
$expm =~ y/ //d;
defined $expm or $self->warn('EXPIRY_MONTH_NONE'), return undef;
$expm =~ /\D/ and $self->warn(EXPIRY_MONTH_NUMERIC => $expm), return undef;
$expm < 1 || $expm > 12 and $self->warn(EXPIRY_MONTH_INVALID => "Month '$expm' outside of 1-12 range"), return undef;
$expm = sprintf "%02d", $expm;
$self->debug("Setting 'credit_card_expiry_month' to '$expm'") if $self->{_debug};
$self->{credit_card_expiry_month} = $expm;
return 1;
}
my $return = $self->{credit_card_expiry_month};
$self->debug("Retrieving 'credit_card_expiry_month': '$return'") if $self->{_debug} and $self->{_debug} >= 2;
return $return;
}
sub credit_card_expiry_year {
my $self = shift;
if (@_) {
my $expy = shift;
$self->debug("Setting 'credit_card_expiry_year' to '$expy'") if $self->{_debug};
$expy =~ y/ //d;
defined $expy or $self->warn('EXPIRY_YEAR_NONE'), return undef;
$expy =~ /\D/ and $self->warn(EXPIRY_YEAR_NUMERIC => $expy), return undef;
length($expy) == 4 or $self->warn(EXPIRY_YEAR_4_DIGIT => $expy), return undef;
$self->{credit_card_expiry_year} = $expy;
return 1;
}
my $return = $self->{credit_card_expiry_year};
$self->debug("Retrieving 'credit_card_expiry_year': $return") if $self->{_debug} and $self->{_debug} >= 2;
return $return;
}
sub charge_total {
my $self = shift;
if (@_) {
my $total = shift;
defined $total or $self->warn('TOTAL_NONE'), return undef;
$total =~ /^(?:\d+\.?\d*|\.\d+)$/ or $self->warn(TOTAL_NUMERIC => $total), return undef;
$total = sprintf "%.2f", $total;
$self->debug("Setting 'charge_total' to '$total'") if $self->{_debug};
$self->{charge_total} = $total;
return 1;
}
my $return = $self->{charge_total};
$self->debug("Retrieving 'charge_total': '$return'") if $self->{_debug} and $self->{_debug} >= 2;
return $return;
}
sub billing_email {
my $self = shift;
if (@_) {
my $email = shift;
$self->debug("Setting 'billing_email' to '$email'") if $self->{_debug};
if (!defined $email) {
$self->warn('EMAIL_NONE');
return undef;
}
if ($email !~ /.@.+\../) {
$self->warn('EMAIL_INVALID' => $email => 'Invalid format');
return undef;
}
$self->{billing_email} = $email;
return 1;
}
my $return = $self->{billing_email};
$self->debug("Retrieving 'billing_email': '$return'") if $self->{_debug} and $self->{_debug} >= 2;
return $return;
}
sub billing_address {
my $self = shift;
my ($one, $two) = ($self->billing_address_1, $self->billing_address_2);
return unless defined $one;
return $two ? $one . "\n" . $two : $one;
}
sub test_mode {
# -----------------------------------------------------------------------------
# Test mode for Moneris involves posting to a different location
#
my $self = shift;
if (@_) {
$self->{test_mode} = !!shift;
$self->debug(($self->{test_mode} ? "Enabling" : "Disabling") . " test mode") if $self->{_debug};
return 1;
}
$self->debug("Retrieving 'test_mode': '$self->{test_mode}'") if $self->{_debug} and $self->{_debug} >= 2;
return $self->{test_mode};
}
sub capture_reference_id {
my $self = shift;
if (@_) {
my $value = shift;
$self->debug("Setting 'capture_reference_id' to '$value'") if $self->{_debug};
defined $value or $self->warn('CAPTURE_REF_NONE'), return undef;
$self->{capture_reference_id} = $value;
return 1;
}
my $return;
if ($self->{preauth_capture_reference_id}) {
$return = $self->{preauth_capture_reference_id};
$self->debug("Retrieving 'capture_reference_id': '$return' (from preauth response)") if $self->{_debug} and $self->{_debug} >= 2;
}
else {
$return = $self->{capture_reference_id};
$self->debug("Retrieving 'capture_reference_id': '$return' (manually set)") if $self->{_debug} and $self->{_debug} >= 2;
}
$return;
}
sub _xml {
# -----------------------------------------------------------------------------
# Produces the XML string to post to the Moneris eSelect server
# Takes a single argument of either 'authorize', 'capture', or 'purchase'
#
my ($self, $type) = @_;
my $xml = '<?xml version="1.0"?>';
$xml .= '<request>';
$xml .= "<store_id>$self->{account_token2}</store_id>";
$xml .= "<api_token>$self->{account_token}</api_token>";
$xml .= $self->_xml_billing($type);
$xml .= '</request>';
$xml;
}
my %_Billing = (
authorize => [
order_id => 'order_id',
amount => 'charge_total',
pan => 'credit_card_number',
expdate => '_cc_exp',
crypt_type => \7, # FIXME - 6 is "SSL - SET enabled merchant", 7 is "SSL - nonSET enabled merchant" - what is SET?
],
capture => [
order_id => 'order_id',
comp_amount => 'charge_total',
txn_number => 'capture_reference_id',
crypt_type => \7, # FIXME - see above
],
txn_type => {
authorize => 'preauth',
capture => 'completion',
sale => 'purchase'
}
);
$_Billing{sale} = $_Billing{authorize};
sub _xml_billing {
# -----------------------------------------------------------------------------
# Produces the XML content for the charge portion of the transaction. This is
# credit card information, charge amount, etc. but not billing address
# information.
#
my ($self, $type) = @_;
my $xml = "<$_Billing{txn_type}->{$type}>";
for (my $i = 0; $i < @{$_Billing{$type}}; $i += 2) {
my ($key, $meth) = @{$_Billing{$type}}[$i, $i+1];
$xml .= "<$key>" . (ref $meth ? $$meth : $self->$meth()) . "</$key>";
}
$xml .= $self->_xml_custinfo($type);
$xml .= "</$_Billing{txn_type}->{$type}>";
$xml;
}
my @_Custinfo = (
first_name => 'billing_fname',
last_name => 'billing_lname',
company_name => 'billing_company',
address => 'billing_address',
city => 'billing_city',
province => 'billing_state',
postal_code => 'billing_postal_code',
country => 'billing_country',
phone_number => 'billing_phone',
fax => 'billing_fax'
);
sub _xml_custinfo {
# -----------------------------------------------------------------------------
# Produces the XML custinfo content. This is usually the billing address
# information. Although not required by eSelect, this module does require and
# pass this information.
#
my ($self, $type) = @_;
my $xml = '<cust_info>';
if (my $email = $self->billing_email) {
$xml .= "<email>$email</email>";
}
$xml .= '<billing>';
for (my $i = 0; $i < @_Custinfo; $i += 2) {
my ($key, $meth) = @_Custinfo[$i, $i+1];
my $val = $self->$meth();
if (defined $val) {
$xml .= "<$key>$val</$key>";
}
}
$xml .= '</billing>';
$xml .= '</cust_info>';
$xml;
}
sub _process {
# -----------------------------------------------------------------------------
# Processes a transaction. Takes a single argument - the type of transaction,
# which must be with 'authorize', 'capture', or 'sale'.
#
my ($self, $type) = @_;
$type eq 'authorize' or $type eq 'capture' or $type eq 'sale'
or return $self->fatal(TYPE_INVALID => $type);
$self->{response} = undef;
$self->check($type) or return undef;
my $www = GT::WWW->new(debug => $self->{_debug});
if ($self->{test_mode}) {
$www->url(TEST_SERVER);
}
else {
$www->url(LIVE_SERVER);
}
$www->connection_timeout(TIMEOUT);
$www->post_data($self->_xml('authorize'));
$www->agent("; GT::Payment::Direct::Moneris/$VERSION");
$self->debug("Posting data to @{[$self->{test_mode} ? 'test' : 'live']} server") if $self->{_debug};
my $response = $www->post
or return $self->warn(TRANSACTION_PROBLEM => $www->error);
my $status = $response->status;
$self->debug("Server responded with status " . int($status) . " $status") if $self->{_debug};
$status or return $self->warn(TRANSACTION_PROBLEM => "Webserver returned error code: " . int($status) . " $status");
return $self->_parse_response($response->content);
}
# Attempts to authorize. You'll get back three possible values:
# 1 - Authorization successful, funds guaranteed - capture should now be performed
# 0 - Authorization declined
# undef - An error occurred
sub authorize {
my $self = shift;
$self->debug("Performing authorization") if $self->{_debug};
my $ret = $self->_process('authorize');
if ($ret) { $self->{preauth_capture_reference_id} = $self->{response}->{TransID} }
elsif (defined $ret) {
my $code = $self->{response}->{ResponseCode};
$self->warn(DECLINED => ($code and $RESPONSE{int $code} or $self->{response}->{Message}));
}
else { $self->warn(ERROR => $self->{response}->{Message}) }
return $ret;
}
sub capture {
my $self = shift;
$self->debug("Performing authorization") if $self->{_debug};
my $ret = $self->_process('capture');
if (!defined $ret) { $self->warn(ERROR => $self->{response}->{Message}) }
elsif (!$ret) {
my $code = $self->{response}->{ResponseCode};
$self->warn(DECLINED => ($code and $RESPONSE{int $code} or $self->{response}->{Message}));
}
return $ret;
}
sub sale {
my $self = shift;
$self->debug("Performing sale") if $self->{_debug};
my $ret = $self->_process('sale');
if (!defined $ret) { $self->warn(ERROR => $self->{response}->{Message}) }
elsif (!$ret) {
my $code = $self->{response}->{ResponseCode};
$self->warn(DECLINED => ($code and $RESPONSE{int $code} or $self->{response}->{Message}));
}
return $ret;
}
sub _parse_response {
my ($self, $content) = @_;
my (%r, @stack);
$self->{response} = \%r;
while ($content =~ m{<(/)?([^<>]+)>|([^<>]+)}g) {
my ($slash, $tag, $value) = ($1, $2, $3);
if ($slash) {
pop @stack;
}
elsif (defined $tag) {
push @stack, $tag;
}
elsif ($value =~ /\S/) {
$value = undef if $value eq 'null';
$r{$stack[-1]} = $value;
}
}
my $ret;
if (not defined $r{ResponseCode}) {
$ret = undef;
}
elsif ($r{ResponseCode} < 50) {
$ret = 1;
}
else {
$ret = 0;
}
$ret;
}
sub check {
# -----------------------------------------------------------------------------
# Checks that all necessary data is provided for an authorize, capture, or
# sale. Takes one argument - 'authorize', 'capture', or 'sale', though 'sale'
# is really no different from 'authorize'.
#
my ($self, $type) = @_;
$type = 'authorize' if $type eq 'sale';
$type eq 'authorize' or $type eq 'capture'
or return $self->fatal(TYPE_INVALID => $type);
my @bad;
for my $field (@{$REQUIRED{uc $type}}) {
my $value = $self->$field();
if ($field eq 'charge_total') {
push @bad, $field if $value <= 0;
}
else {
push @bad, $field if !$value;
}
}
if (@bad) {
$self->warn(MISSING_FIELDS => $type => "@bad");
return undef;
}
return 1;
}
sub receipt {
# -----------------------------------------------------------------------------
# After a successful sale, you can call this to get a list of Key => Value
# pairs that make up a rough receipt. The keys are ordered, so reading them
# into an array probably makes more sense than a hash.
#
my $self = shift;
my $r = $self->{response} or return;
my @receipt;
my $code = $r->{ResponseCode};
push @receipt,
"Order ID" => $self->order_id,
"Amount" => $r->{TransAmount},
"Status" => ($code and $RESPONSE{int $code} or $self->{response}->{Message}),
"Transaction Type" => $r->{TransType},
"Date" => $r->{TransDate},
"Auth Code" => $r->{AuthCode},
"Response Code" => $code,
"Response Message" => $r->{Message},
"ISO Code" => $r->{ISO},
"Reference Number" => $r->{ReferenceNum},
"Cardholder Name" => $self->billing_fname . " " . $self->billing_lname;
return @receipt;
}
1;

View File

@ -0,0 +1,317 @@
# ====================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
# GT::Payment::Remote::2CheckOut
# Author: Jason Rhinelander
# CVS Info : 087,071,086,086,085
# $Id: 2CheckOut.pm,v 1.5 2006/08/22 20:39:04 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
# ====================================================================
#
# Description:
# 2CheckOut payment processing.
#
package GT::Payment::Remote::2CheckOut;
use strict;
use Carp;
use GT::MD5 'md5_hex';
require Exporter;
use vars qw/@EXPORT_OK/;
@EXPORT_OK = qw/process/;
sub process {
# -----------------------------------------------------------------------------
shift if $_[0] and UNIVERSAL::isa($_[0], __PACKAGE__);
my %opts = @_;
$opts{param} and UNIVERSAL::isa($opts{param}, 'GT::CGI') or croak 'Usage: ->process(param => $gtcgi, ...)';
my $in = $opts{param};
ref $opts{on_valid} eq 'CODE'
or croak 'Usage: ->process(on_valid => \&CODEREF, ...)';
defined $opts{password} and length $opts{password} or croak 'Usage: ->process(password => "password", ...)';
defined $opts{sellerid} and length $opts{sellerid} or croak 'Usage: ->process(sellerid => "sellerid", ...)';
$opts{password} eq 'tango' and croak 'Usage: ->process(password => "something other than \'tango\'", ...)';
my $order_number = $in->param('order_number');
# Check that the "secret word" (password) combined with the other information
# actually checks out.
my $str = $opts{password} . $opts{sellerid} . $order_number . $in->param('total');
my $md5 = md5_hex($str);
if (lc $md5 eq lc $in->param('key')) {
$opts{on_valid}->();
}
# If demo mode is enabled, then the order number is set to 1 in the md5:
# https://www.2checkout.com/documentation/UsersGuide2/chapter6/md5-hash.html
elsif ($opts{demo}) {
$str = $opts{password} . $opts{sellerid} . 1 . $in->param('total');
$md5 = md5_hex($str);
if (lc $md5 eq lc $in->param('key')) {
$opts{on_valid}->();
}
}
return;
}
1;
__END__
=head1 NAME
GT::Payment::Remote::2CheckOut - 2CheckOut payment handling
=head1 CAVEATS
2CheckOut has a pretty weak automated payment system - the security of the
entire automated payment process hinges on your "Secret Word" (Admin -> Account
Details -> Return -> Secret Word (near the bottom of the page)) - without it,
there is no security at all. Another weakness in the system is that if your
server is not reachable for whatever reason, the payment information would be
lost. Payment providers like 2CheckOut and WorldPay would do well to learn
from payment systems like that of PayPal - whatever can be said about other
aspects of PayPal, they do have one of the nicest payment systems around - both
from a developer and user's point of view.
Because of the security issue with not using the "Secret Word", this module
requires that the secret word be used, even if other 2CheckOut systems may not.
Additionally, the default secret word of "tango" is not allowed.
=head1 SYNOPSIS
use GT::Payment::Remote::2CheckOut;
use GT::CGI;
my $in = new GT::CGI;
GT::Payment::Remote::2CheckOut->process(
param => $in,
on_valid => \&valid,
sellerid => "1234",
password => "Some Good Secret Word"
);
sub valid {
# Update database - the payment has been made successfully.
}
=head1 DESCRIPTION
This module is designed to handle 2CheckOut payment processing.
=head1 REQUIREMENTS
GT::CGI and GT::MD5.
=head1 FUNCTIONS
This module has only one function: process() does the work of actually
figuring out what to do with a postback.
=head2 process
process() is the only function provided by this module. It can be called as
either a function or class method, and takes a hash (not hash reference) of
arguments as described below.
process() should be called for 2CheckOut initiated postbacks. This can be set
up in your main .cgi by looking for 2CheckOut-specific CGI parameters
('cart_order_id' is a good one to look for) or by making a seperate .cgi file
exclusively for handling 2CheckOut postbacks.
Additionally, it is strongly advised that database connection, authenticate,
etc. be performed before calling process() to ensure that the payment is
recorded successfully. 2CheckOut will not attempt to repost the form data if
your script produces an error, and the error will be shown to the customer.
=over 4
=item param
param takes a GT::CGI object from which 2CheckOut postback variables are read.
=item on_valid
on_valid takes a code reference as value. The code reference will be called
when a successful payment has been made. Inside this code reference you are
responsible for setting a "paid" status for the order in question. The
C<cart_order_id> CGI variable will have whatever cart_order_id you provided.
=item sellerid
This should be passed to seller number. This is needed, along with the
password field below, to verify that the posted payment is a genuine 2CheckOut
payment.
=item password
This is a "Secret Word" that the admin must set in the 2CheckOut admin area
(under Look & Feel -> Secret Word). This field must be set in the admin, and
passed in here. Note that the default value, "tango", is not allowed. Without
this password, 2CheckOut postbacks should not be considered secure.
=item demo
Whether or not to initiate and accept demo transactions.
=back
=head1 INSTRUCTIONS
To implement 2CheckOut payment processing, there are a number of steps required
in addition to this module. Basically, this module handles only the postback
stage of the 2CheckOut payment process.
=head2 Directing customers to 2CheckOut
This is done by creating a web form containing the following variables. Your
form, first of all, should post to
C<https://www.2checkout.com/2co/buyer/purchase>. See
C<https://www.2checkout.com/documentation/UsersGuide2/third_party_carts/2co-system-parameters.html>
for a complete and up-to-date list of parameters that can be passed to 2CheckOut.
Required fields are as follows:
=over 4
=item * sid
Your 2CheckOut account number
=item * total
The total amount to be billed, in DD.CC format.
=item * cart_order_id
A unique order id, which you should store to track the payment.
=back
The following parameters *may* be passed in, and will be available in the
postback:
=over 4
=item * card_holder_name
=item * street_address
=item * city
=item * state
=item * zip
=item * country
=item * phone
The card holder's details.
=item * email
The card holder's email address.
=item * ship_name
=item * ship_street_address
=item * ship_city
=item * ship_state
=item * ship_zip
=item * ship_country
Shipping info - however, according to 2CheckOut, you must indicate that you
want to take that you want to take down a seperate shipping and billing address
on the L<Shipping Details page|https://sellers.2checkout.com/cgi-bin/sellersarea/shipdetails.2c>.
=item * demo
Should be set to 'Y' if you want demo mode, omitted for regular transactions.
=back
In the postback CGI, you'll get back all of the billing and shipping variables
listed above, plus:
=over 4
=item * order_number
2CheckOut order number
=item * cart_order_id
=item * cart_id
Your order number, passed back. Both variables are the same.
=back
=head2 Postback
Before 2CheckOut postback notification can occur, you must set up the postback
(in 2CheckOut terminology, "Routine"). This can be set from the Admin ->
Shopping Cart -> Cart Details. You need to enable the payment routine, and
set it to a CGI that you manage.
=head2 Putting it all together
The typical way to implement all of this is as follows:
=over 4
=item 1 Get necessary merchant information (sid and secret keyword)
=item 2 Once the customer has selected what to purchase, generate a
cart_order_id (a random MD5 hex string works well), and store it somewhere
(i.e. in the database).
=item 3 Make a form with all the necessary fields that
L<submits to 2CheckOut|/"Directing customers to 2CheckOut">.
=item 4 Set up the L<C<on_valid>|/"on_valid"> callback. If using a dedicated
CGI script for 2CheckOut callbacks, it should just call process(); otherwise,
check for the CGI parameter 'cart_order_id' and if present, call process().
=item 5 For a valid payment, do whatever you need to do for a valid payment,
and store some record of the payment having been made (storing at least the
cart_order_id and the order_number is strongly recommended). Use the CGI
parameter 'cart_order_id' to locate the order (i.e. in the database).
=back
=head1 SEE ALSO
L<http://www.2checkout.com> - 2CheckOut website.
L<http://www.support.2checkout.com/deskpro/faq.php> - 2CheckOut knowledgebase
=head1 MAINTAINER
Jason Rhinelander
=head1 COPYRIGHT
Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
http://www.gossamer-threads.com/
=head1 VERSION
Revision: $Id: 2CheckOut.pm,v 1.5 2006/08/22 20:39:04 brewt Exp $
=cut

View File

@ -0,0 +1,573 @@
# ====================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
# GT::Payment::Remote::PayPal
# Author: Jason Rhinelander
# CVS Info : 087,071,086,086,085
# $Id: PayPal.pm,v 1.8 2006/04/08 03:42:05 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
# ====================================================================
#
# Description:
# PayPal IPN payment processing.
# IPN information: (PayPal login required)
# https://www.paypal.com/cgi-bin/webscr?cmd=p/acc/ipn-info
#
# Net::SSLeay is required. Windows (ActivePerl) Net::SSLeay packages are
# available through Gossamer Threads.
#
package GT::Payment::Remote::PayPal;
use strict;
use Carp;
use GT::WWW;
use GT::WWW::https;
# Usage:
# process(
# param => $GT_CGI_OBJ,
# on_valid => \&CODEREF, # Called when everything checks out
# on_pending => \&CODEREF, # Optional - another IPN request will come in when no longer pending
# on_failed => \&CODEREF, # "The payment has failed. This will only happen if the payment was made from your customer's bank account"
# on_denied => \&CODEREF, # "You, the merchant, denied the payment. This will only happen if the payment was previously pending due to one of the "pending reasons" below"
# on_invalid => \&CODEREF, # This request did NOT come from PayPal
# on_recurring => \&CODEREF, # A recurring payment
# on_recurring_signup => \&CODEREF, # A recurring payment signup
# on_recurring_cancel => \&CODEREF, # A recurring payment cancellation
# on_recurring_failed => \&CODEREF, # A subscription payment failure
# on_recurring_eot => \&CODEREF, # A subscription "end of term" notification
# on_recurring_modify => \&CODEREF, # A subscription modification notification
# duplicate => \&CODEREF, # Check to make sure this isn't a duplicate (1 = okay, 0/undef = duplicate)
# email => \&CODEREF, # Called with the specified e-mail - check it against the primary e-mail account, return 1 for valid, 0/undef for error
# on_error => \&CODEREF # Optional
# )
# Only on_error is optional. on_valid will be called if the request is valid,
# on_invalid is invalid, and on_error if an error occurs (such as an HTTP error,
# connection problem, etc.)
sub process {
shift if $_[0] and UNIVERSAL::isa($_[0], __PACKAGE__);
my %opts = @_;
$opts{param} and UNIVERSAL::isa($opts{param}, 'GT::CGI') or croak 'Usage: ->process(param => $gtcgi, ...)';
my $in = $opts{param};
for (qw/on_valid on_failed on_denied duplicate email/) {
ref $opts{$_} eq 'CODE' or croak "Usage: ->process($_ => \&CODEREF, ...)";
}
for (qw/on_error on_pending on_invalid on_recurring on_recurring_signup on_recurring_cancel
on_recurring_failed on_recurring_eot on_recurring_modify/) {
!$opts{$_} or ref $opts{$_} eq 'CODE' or croak "Usage: ->process($_ => \\&CODEREF, ...) (optional)";
}
my $sandbox = $opts{sandbox} ? 'sandbox.' : '';
my $wwws = GT::WWW->new("https://www.${sandbox}paypal.com/cgi-bin/webscr");
my @param;
for my $p ($in->param) {
for my $v ($in->param($p)) {
push @param, $p, $v;
}
}
# PayPal says:
# You will also need to append a variable named "cmd" with the value
# "_notify-validate" (e.g. cmd=_notify-validate) to the POST string.
$wwws->parameters(@param, cmd => '_notify-validate');
my $result = $wwws->post;
my $status;
# PayPal says:
# PayPal will respond to the post with a single word, "VERIFIED" or
# "INVALID", in the body of the response. When you receive a VERIFIED
# response, you need to:
#
# * Check that the "payment_status" is "completed"
# * If the "payment_status" is "completed", check the "txn_id" against
# the previous PayPal transaction you have processed to ensure it is
# not a duplicate.
# * After you have checked the "payment_status" and "txn_id", make sure
# the "receiver_email" is an email address registered in your PayPal
# account
# * Once you have completed the above checks, you may update your
# database based on the information provided.
if ($result) {
my $status = "$result";
unless ($status eq 'VERIFIED') {
$opts{on_invalid}->($status) if $opts{on_invalid};
return;
}
# For certain txn_types payment_status and txn_id aren't available
my $txn_type = $in->param('txn_type');
if ($txn_type =~ /^subscr_(?:signup|cancel|failed|eot|modify)$/) {
if ($txn_type eq 'subscr_signup') {
$opts{on_recurring_signup}->() if $opts{on_recurring_signup};
}
elsif ($txn_type eq 'subscr_cancel') {
$opts{on_recurring_cancel}->() if $opts{on_recurring_cancel};
}
elsif ($txn_type eq 'subscr_failed') {
$opts{on_recurring_failed}->() if $opts{on_recurring_failed};
}
elsif ($txn_type eq 'substr_eot') {
$opts{on_recurring_eot}->() if $opts{on_recurring_eot};
}
elsif ($txn_type eq 'substr_modify') {
$opts{on_recurring_modify}->() if $opts{on_recurring_modify};
}
return;
}
# * Check that the "payment_status" is "completed" [sic; should be "Completed"]
unless ((my $status = $in->param('payment_status')) eq 'Completed') {
if ($status eq 'Pending') {
$opts{on_pending}->() if $opts{on_pending};
}
elsif ($status eq 'Failed') {
$opts{on_failed}->();
}
elsif ($status eq 'Denied') {
$opts{on_denied}->();
}
elsif ($status eq 'Refunded') {
$opts{on_refund}->() if $opts{on_refund};
}
elsif ($opts{on_error}) {
$opts{on_error}->("PayPal sent invalid/unknown payment_status value: '$status'");
}
return;
}
my $txn_id = $in->param('txn_id');
return unless $txn_id;
# * If the "payment_status" is "completed", check the "txn_id" against
# the previous PayPal transaction you have processed to ensure it is
# not a duplicate.
$opts{duplicate}->($txn_id) or return;
# * After you have checked the "payment_status" and "txn_id", make sure
# the "receiver_email" is an email address registered in your PayPal
# account
$opts{email}->($in->param('receiver_email')) or return; # Ignore if the e-mail addresses don't match
if ($txn_type eq 'subscr_payment') {
$opts{on_recurring}->() if $opts{on_recurring};
}
else {
$opts{on_valid}->();
}
}
elsif ($opts{on_error}) {
if (defined $result) {
my $http_status = $result->status;
$opts{on_error}->("Server returned a non-okay status: " . int($http_status) . " $http_status");
}
else {
$opts{on_error}->("Connection error: " . $wwws->error);
}
}
return;
}
1;
__END__
=head1 NAME
GT::Payment::Remote::PayPal - PayPal payment handling
=head1 SYNOPSIS
use GT::Payment::Remote::PayPal;
use GT::CGI;
my $in = new GT::CGI;
GT::Payment::Remote::PayPal->process(
param => $in,
on_valid => \&valid,
on_pending => \&pending,
on_failed => \&failed,
on_denied => \&denied,
on_invalid => \&invalid,
on_recurring => \&recurring,
on_recurring_signup => \&r_signup,
on_recurring_cancel => \&r_cancel,
on_recurring_failed => \&r_failed,
on_recurring_eot => \&r_eot,
on_recurring_modify => \&r_modify,
duplicate => \&duplicate,
email => \&email,
on_error => \&error
);
sub valid {
# Update database - the payment has been made successfully.
}
sub pending {
# Optional; store a "payment pending" status if you wish. This is optional
# because another postback will be made with a completed, failed, or denied
# status.
}
failed {
# According to PayPal IPN documentation: "The payment has failed. This
# will only happen if the payment was made from your customer's bank
# account."
# Store a "payment failed" status for the order
}
sub denied {
# According to PayPal IPN documentation: "You, the merchant, denied the
# payment. This will only happen if the payment was previously pending due
# to one of the "pending reasons" [in pending_reason]"
}
sub invalid {
# This means the request did NOT come from PayPal. You should log the
# request for follow up.
}
sub recurring {
# This means a recurring payment has been made successfully. Update
# database.
}
sub r_signup {
# This means a recurring signup has been made (NOT a payment, just a
# signup).
}
sub r_cancel {
# The user has cancelled their recurring payment
}
sub r_failed {
# A recurring payment has failed (probably declined).
}
sub r_eot {
# A recurring payment has come to its natural conclusion. This only
# applies to payments with a set number of payments.
}
sub r_modify {
# Something has been modified regarding the recurring payment
}
sub duplicate {
# Check to see if the payment has already been made. If it _has_ been
# made, you should return undef, otherwise return 1 to indicate that this
# is not a duplicate postback. The "txn_id" value is passed in, but is
# also available through $in->param('txn_id').
}
sub email {
# This will be called with an e-mail address. You should check to make
# sure that the e-mail address entered is the same as the one on the PayPal
# account. Return true (1) if everything checks out, undef otherwise.
}
sub error {
# An error message is passed in here. This is called when a error such as
# a connection problem or HTTP problem occurs.
}
=head1 DESCRIPTION
This module is designed to handle PayPal payment processing using PayPal's IPN
system. It does very little other than generating and sending a proper
response to the PayPal server, and calling the provided code reference(s).
It is strongly recommended that you familiarize yourself with the PayPal
"Single Item Purchases Manual" and "IPN Manual" listed in the L</"SEE ALSO">
section of this document.
=head1 REQUIREMENTS
GT::WWW with the https protocol, which in turn requires Net::SSLeay. PPM's are
available from Gossamer Threads for the latest Windows releases of ActiveState
Perl 5.6.1 and 5.8.0.
=head1 process
process() is the only function/method provided by this module. It can be
called as either a function or class method, and takes a hash (not hash
reference) of arguments as described below. This module requires GT::WWW's
https interface, which in turn requires Net::SSLeay.
process() should be called for PayPal initiated requests. This can be set up
in your main CGI by looking for PayPal-specific CGI parameters ('txn_type' is a
good one to look for) or by making a seperate .cgi file exclusively for
handling IPN postbacks.
Additionally, it is strongly advised that database connection, authenticate,
etc. be performed before calling process() to ensure that the payment is
recorded successfully. If your CGI script has an error, PayPal will retry the
postback again
Except where indicated, all arguments are required.
=head2 param
param takes a GT::CGI object from which PayPal IPN variables are read.
=head2 on_valid
on_valid takes a code reference as value. The code reference will be called
when a successful payment has been made. Inside this code reference you are
responsible for setting a "paid" status for the order in question.
See the PayPal IPN documentation listed below for information on how to
identify an order.
=head2 on_pending
on_pending is called when PayPal sends information on a "Pending" payment.
This parameter is optional, due to the fact that a "Pending" status means that
another notification (either "Completed", "Failed", or "Denied") will be made.
It is, however, recommended that when a Pending payment is encountered, a note
be stored in your application that manual intervention is probably required.
According to PayPal documentation, there are a few cases where this will
happen, which can be obtained from the "pending_reason" CGI input variable.
The possible values and what each means follows (this comes straight from the
PayPal documentation).
=over 4
=item "echeck"
The payment is pending because it was made by an eCheck, which has not yet
cleared.
=item "multi_currency"
You do not have a balance in the currency sent, and you do not have your
Payment Receiving Preferences set to automatically convert and accept this
payment. You must manually accept or deny this payment.
=item "intl"
The payment is pending because you, the merchant, hold an international account
and do not have a withdrawal mechanism. You must manually accept or deny this
payment from your Account Overview.
=item "verify"
The payment is pending because you, the merchant, are not yet verified. You
must verify your account before you can accept this payment.
=item "address"
The payment is pending because your customer did not include a confirmed
shipping address and you, the merchant, have your Payment Receiving Preferences
set such that you want to manually accept or deny each of these payments. To
change your preference, go to the "Preferences" section of your "Profile."
=item "upgrade"
The payment is pending because it was made via credit card and you, the
merchant, must upgrade your account to Business or Premier status in order to
receive the funds.
=item "unilateral"
The payment is pending because it was made to an email address that is not yet
registered or confirmed.
=item "other"
The payment is pending for an "other" reason. For more information, contact
customer service.
=back
=head2 on_failed
Takes a code reference to call in the event of a failed payment notification.
A failed payment "will only happen if the payment was made from your customer's
bank account."
You should record a failed payment in your application.
=head2 on_denied
This code reference is called when a "Denied" payment notification is received.
"This will only happen if the payment was previously pending due to one of the
'pending reasons'" above.
You should record a failed or denied payment in your application.
=head2 on_invalid
This code reference will be called when an invalid request is made. This
usually means that the request B<did not> come from PayPal. According to
PayPal, "if you receive an 'INVALID' notification, it should be treated as
suspicious and investigated." Thus it is strongly recommended that a record of
the invalid request be made.
=head2 duplicate
This code reference is required to prevent duplicate payments. It is called
for potentially successful requests to ensure that it is not a duplicate
postback. It is passed the "txn_id" CGI parameter, which is the
PayPal-generated transaction ID. You should check this parameter against your
order database. If you have already recorded this payment as successfully
made, should should return C<undef> from this function, to indicate that the
duplicate check failed. If the transaction ID is okay (i.e. is not a
duplicate) return 1 to continue.
=head2 recurring
A successful recurring payment has been made. You should set a "paid" status
for the item in question.
=head2 recurring_signup
=head2 recurring_cancel
=head2 recurring_failed
=head2 recurring_eot
=head2 recurring_modify
These are called when various things have happened to the subscription. In
particular, signup refers to a new subscription, cancel refers to a cancelled
subscription, failed refers to a failed payment, eot refers to a subscription
that ended naturally (i.e. an end was set when the subscription was initially
made), and modify is called when a payment has been modified.
=head2 email
This code reference, like duplicate, is called to ensure that the payment was
sent to the correct account. An e-mail address is passed in which must be the
same as the primary account's e-mail address. If it is the same, return C<1>.
If it is I<not> the same, you should return C<undef> and store a note asking
the user to check that the PayPal e-mail address they have provided is the
correct, primary, PayPal e-mail address.
=head2 on_error
This code reference is optional, but recommended. It is called when a
non-PayPal generated error occurs - such as a failure to connect to PayPal. It
is recommended that you provide this code reference and log any errors that
occur. The error message is passed in.
=head1 INSTRUCTIONS
To implement PayPal payment processing, there are a number of steps required in
addition to this module. Basically, this module handles only the postback
stage of the PayPal IPN process.
Full PayPal single item, subscription, and IPN documentation is available at
the URL's listed in the L<SEE ALSO|/"SEE ALSO"> section.
=head2 Directing customers to PayPal
This is done by creating a web form containing the following variables. Your
form, first of all, must post to C<https://www.paypal.com/cgi-bin/webscr>.
Your form should contains various PayPal parameters, as outlined in the PayPal
manuals linked to in the L<SEE ALSO|/"SEE ALSO"> section.
Of particular note is the "notify_url" option, which should be used to specify
a postback URL for PayPal IPN postbacks.
The below is simply a list of the required fields, and only those fields that
are absolutely required are described. For descriptions of each field, check
the PayPal Single Item Purchases Manual.
=over 4
=item cmd
Must be set to "_xclick".
=item business
Your PayPal ID (e-mail address). Must be confirmed and linked to your Verified
Business or Premier account.
=item item_name
=item item_number
=item image_url
=item no_shipping
=item return
Although optional, this is highly recommend - takes a URL to bring the buyer
back to after purchasing. If not specified, they'll remain at PayPal.
=item rm
Return method for the L<return|/return> option. If "1", a GET request without
the transaction variables will be made, if "2" a POST request WITH the transaction
variables will be made.
=item cancel_return
=item no_note
=item cn
=item cs
=item on0
=item on1
=item os0
=item os1
=item quantity
The quantity of items being purchased. If omitted, defaults to 1 and will not
be shown in the payment flow.
=item undefined_quantity
"If set to "1", the user will be able to edit the quantity. This means your
customer will see a field next to quantity which they must complete. This is
optional; if omitted or set to "0", the quantity will not be editable by the
user. Instead, it will default to 1"
=item shipping
=back
=head2 IPN
Before PayPal payment notification can occur, you must instruct the user to
enable Instant Payment Notification (IPN) on their PayPal account. The
postback URL should be provided and handled by you either by detecting a PayPal
request in your main .cgi script (recommended), or through the use of an
additional .cgi script exclusively for PayPal IPN.
If adding to your existing script, it is recommended to look for the 'txn_type'
CGI parameter, which will be set for PayPal IPN postbacks.
Once IPN has been set up, you have to set up your application to direct users
to PayPal in order to initiate a PayPal payment.
=head1 SEE ALSO
L<https://www.paypal.com/html/single_item.pdf> - Single Item Purchases Manual
L<https://www.paypal.com/html/subscriptions.pdf> - Subscriptions and Recurring
Payments Manual
L<https://www.paypal.com/html/ipn.pdf> - IPN Manual
=head1 MAINTAINER
Jason Rhinelander
=head1 COPYRIGHT
Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
http://www.gossamer-threads.com/
=head1 VERSION
Revision: $Id: PayPal.pm,v 1.8 2006/04/08 03:42:05 brewt Exp $
=cut

View File

@ -0,0 +1,466 @@
# ====================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
# GT::Payment::Remote::WorldPay
# Author: Jason Rhinelander
# CVS Info : 087,071,086,086,085
# $Id: WorldPay.pm,v 1.9 2006/08/22 23:03:14 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
# ====================================================================
#
# Description:
# WorldPay "Select Junior" payment processing.
#
#
# One major shortcoming of WorldPay is that its callback system is quite weak.
# It won't try to inform you very hard - it tries once, but if it doesn't
# connect it gives up and doesn't try again, making it entirely possible and
# likely that you will have to manually add missing payments at some point.
#
package GT::Payment::Remote::WorldPay;
use strict;
use Carp;
require Exporter;
use vars qw/@ISA @EXPORT_OK/;
@ISA = qw/Exporter/;
@EXPORT_OK = qw/process md5_signature/;
sub process {
# -----------------------------------------------------------------------------
shift if $_[0] and UNIVERSAL::isa($_[0], __PACKAGE__);
my %opts = @_;
$opts{param} and UNIVERSAL::isa($opts{param}, 'GT::CGI') or croak 'Usage: ->process(param => $gtcgi, ...)';
my $in = $opts{param};
ref $opts{on_valid} eq 'CODE'
or ref $opts{on_recurring} eq 'CODE'
or croak 'Usage: ->process(on_valid => \&CODEREF, ...)';
defined $opts{password} and length $opts{password} or croak 'Usage: ->process(password => "password", ...)';
for (qw/on_valid on_recurring on_cancel on_invalid_password on_recurring_failed on_recurring_cancelled/) {
!$opts{$_} or ref $opts{$_} eq 'CODE' or croak "Usage: ->process($_ => \\&CODEREF, ...)";
}
my $callbackpw = $in->param('callbackPW');
unless ($callbackpw and $callbackpw eq $opts{password}) {
$opts{on_invalid_password}->() if $opts{on_invalid_password};
return;
}
my $trans_status = $in->param('transStatus');
# The transaction was a testMode transaction, but testMode is not enabled.
if ($in->param('testMode') and not $opts{test_mode}) {
return;
}
if ($in->param('futurePayId')) {
if ($trans_status eq 'Y') {
$opts{on_recurring}->() if $opts{on_recurring};
}
elsif ($trans_status eq 'N') {
$opts{on_recurring_failed}->() if $opts{on_recurring_failed};
}
elsif ($in->param('futurePayStatusChange') eq 'Customer Cancelled') {
$opts{on_recurring_cancelled}->() if $opts{on_recurring_cancelled};
}
}
else {
if (uc $trans_status eq 'Y') { $opts{on_valid}->() if $opts{on_valid} }
elsif (uc $trans_status eq 'C') { $opts{on_cancel}->() if $opts{on_cancel} }
}
return;
}
sub md5_signature {
# -----------------------------------------------------------------------------
shift if $_[0] and UNIVERSAL::isa($_[0], __PACKAGE__);
require GT::MD5;
return GT::MD5::md5_hex(join ":", @_);
}
1;
__END__
=head1 NAME
GT::Payment::Remote::WorldPay - WorldPay payment handling
=head1 CAVEATS
One thing to note about WorldPay is that its security system is a little weak -
you can't trust a callback post as actually being genuine, unless you use the
callback password feature - and even at that it is not a terribly secure
solution. In this regard, other payment provides have much cleaner transaction
systems. Another shortcoming of WorldPay is that its callback system is
somewhat weak - it won't try to inform you very hard: it tries once, but if it
doesn't connect it gives up and doesn't try again, making it entirely possible
and likely that you will have to manually add (or confirm) missing payments at
some point, so supporting at least manual payment approval of initiated
payments is absolutely required.
=head1 SYNOPSIS
use GT::Payment::Remote::WorldPay;
use GT::CGI;
my $in = new GT::CGI;
GT::Payment::Remote::WorldPay->process(
param => $in,
on_valid => \&valid,
on_cancel => \&cancel,
on_recurring => \&recurring,
on_recurring_failed => \&recurring_failed,
on_recurring_cancelled => \&recurring_cancelled,
password => "123",
on_invalid_password => \&invalid_pw
);
sub valid {
# Update database - the payment has been made successfully.
}
sub cancel {
# Update database - the user has clicked the "Cancel" button, thereby
# cancelling the payment. You should take note of the cancellation.
}
sub on_recurring {
# Update database - a recurring payment has been made successfully.
}
sub on_recurring_failed {
# Update database - a recurring payment has failed.
}
sub on_recurring_cancelled {
# Update database - either the customer or the merchant has cancelled
# this recurring payment
}
sub on_invalid_password {
# Perhaps make a record - a payment callback was received without a
# valid password
}
=head1 DESCRIPTION
This module is designed to handle WorldPay payment processing using WorldPay's
"Select Junior" system and callback.
=head1 REQUIREMENTS
GT::CGI is the only requirement, however GT::MD5 is required in order to use
the md5_signature function.
=head1 FUNCTIONS
This module has only two functions. process() does the work of actually
figuring out what to do with a postback, and md5_signature() is used to
generate an MD5 signature for payment verification and security purposes. Both
functions can be imported into your package, and can be called as either method
or function.
=head2 process
process() is the main function provided by this module. It can be called as
either a function or class method, and takes a hash (not hash reference) of
arguments as described below.
process() should be called for WorldPay initiated postbacks. This can be set
up in your main CGI by looking for WorldPay-specific CGI parameters
('transStatus' is a good one to look for) or by making a seperate .cgi file
exclusively for handling WorldPay postbacks.
Additionally, it is strongly advised that database connection, authenticate,
etc. be performed before calling process() to ensure that the payment is
recorded successfully. WorldPay will not attempt to repost the form data if
your script produces an error, and the error will be shown to the customer.
The L<C<param>|/"param"> argument, either L<C<on_valid>|/"on_valid"> or
L<C<on_recurring>|/"on_recurring">, and the L<C<password>|/"password"> options
are required. Using L<MD5 signing|/"MD5 signing"> as well is strongly advised.
=over 4
=item param
param takes a GT::CGI object from which WorldPay postback variables are read.
=item on_valid
on_valid takes a code reference as value. The code reference will be called
when a successful payment has been made. Inside this code reference you are
responsible for setting a "paid" status for the order in question.
=item on_cancel
Takes a code reference to call in the event of the customer clicking the
"cancel" button. Note that this is not sent if the user closes their browser,
but only if they click "cancel."
You should record a cancelled payment in your application.
=item password
This is a password that the customer should set in the WorldPay Customer
Management System, and provide to you. Without this password, WorldPay
postbacks should not be considered secure.
=item on_invalid_password
This code reference will be called when the correct password is not present in
the postback request. This will also be called if no password is provided.
=item on_recurring
=item on_recurring_failed
=item on_recurring_cancelled
In order to support recurring payments, you must at least define
C<on_recurring>. C<on_recurring> is called when a successful recurring payment
has been made. C<on_recurring_failed> is called for a failed recurring payment
(e.g. credit card declined). See
L<the Recurring charges section|/"Recurring charges"> for more details.
Bear in mind that if you do not set up the on_recurring callback, recurring
payments will be ignored.
=back
=head2 md5_signature
The md5_signature() function takes a password (this must be set for the
WorldPay account), and a list of values and generates an appropriate WorldPay
MD5 signature, which should be included as the "signature" field. See
L<the MD5 signing section|/"MD5 signing"> for more details.
=head1 INSTRUCTIONS
To implement WorldPay payment processing, there are a number of steps required
in addition to this module. Basically, this module handles only the postback
stage of the WorldPay payment process.
Full WorldPay "Select Junior" information is available from the "Select Junior
Integration Guide" available from www.worldpay.com.
=head2 Directing customers to WorldPay
This is done by creating a web form containing the following variables. Your
form, first of all, must make a C<post> request to
C<https://select.worldpay.com/wcc/purchase>.
Required fields are as follows:
=over 4
=item instId
Your WorldPay Installation ID. Example: C<1234>
=item currency
The currency of the purchase. Example: C<GBP>
=item desc
A description of the purchase. Example: C<Blue T-Shirt, Medium>
=item cartId
A reference you assign to help you identify the purchase. Example: C<10a0491>.
=item amount
The total cost of the purchase. Example: C<25.35>
=back
=head2 Recurring charges
Additionally, in order to set up recurring payments, the WorldPay account must
have "FuturePay" enabled, and then you need to use the following parameters.
The below parameters are used for the "Regular FuturePay Agreements" - there is
also "Limited FuturePay Agreements" in which a maximum overall charge is set.
For more information, see L<Repear Billing With FuturePay|/"SEE ALSO">.
=over 4
=item futurePayType
Should contain the value "regular", unless using "Limited FuturePay Agreements,"
which will work but is not described here.
=item option
Should contain either 0, 1, or 2. 0 means the payment amount is fixed and
cannot be changed. 1 means the payment is fixed, but can be changed to another
amount at any point. 2 means the payment amount must be set before each
recurring payment.
=item startDate
Value in the format: "yyyy-mm-dd". This should be the date on which the first
future payment should be taken. Note that this is _NOT_ and CANNOT be today,
but must be a value in the future. If using option 2, this value must be at
least 2 weeks in the future.
=item startDelayUnit
One digit: 1: day, 2: week, 3: month, 4: year. Only used if startDate is
B<not> set. If using option 2, this value must be at least 2 weeks in the
future.
=item startDelayMult
The actual delay is obtained by multiplying this value by startDelayUnit. So,
to start in three weeks, this would be "3", and startDelayUnit would be "2".
Again, this is not used if startDate is specified. Must be >= 1 if set.
=item noOfPayments
This number of payments that will be made. Leave as 0 or unset for unlimited.
=item intervalUnit
One digit: 1: day, 2: week, 3: month, 4: year. The unit of interval between
payments. This must be set unless noOfPayments is 1. If using option 1 or
option 2, the minimum interval is 2 weeks.
=item intervalMult
The interval between payments is determined by this value multiplied by
intervalUnit. So, to make payments every 1 month, this would be "1", and
intervalUnit would be "3". Must be >= 1.
=item normalAmount
This must be set for option 0 and option 1, but cannot be set for option 2.
=item initialAmount
This can be used for option 0 or option 1, but cannot be set for option 2. If
set, this overrides the amount of the first payment.
=back
For FuturePay (recurring) payments, you still pass the required fields as
normal, except for the amount field: amount can be passed as 0 or a value - if
a value is specified, this will be treated as an immediate payment. So, for
example, if you wanted to charge someone a monthly subscription of $10 starting
today you would pass the following variables:
instId=1234 # (the merchant's installation reference here)
amount=10
cartId=8456a9264q314 # (Some random ID here that you generate)
currency=USD # (Whatever currency they are charging in goes here)
desc=Subscription For Something Cool # (Description of subscription)
option=0
normalAmount=10
startDelayUnit=3
startDelayMult=1
intervalUnit=3
intervalMult=1
=head2 MD5 signing
Additionally, using WorldPay's MD5 signature feature is strongly recommended.
To enable this feature, provide a field "signatureFields", containing fields
separated by ":". Although any fields can be used, "amount:currency:cartId" is
recommended. Then, call:
my $md5 = GT::Payment::Remote::WorldPay::md5_signature(
$password, $amount, $currency, $cartId
);
$password should be a password provided by the user and known only to the user
and WorldPay. The value returned should be passed as the "signature" variable.
This MD5 protection causes WorldPay to reject any faked payment requests and so
is reasonably secure.
=head2 Postback
Before WorldPay postback notification can occur, you must instruct the user to
enable the callback facility in the Customer Management System. Additionally,
it is recommended that a proper URL to your CGI be specified there, or else
pass along a "MC_callback" variable that points to the script _WITHOUT_ a
leading http:// or https://. (e.g. MC_callback=www.example.com/callback.cgi).
Note that a WorldPay limitation prevents the callback protocol (http://) from
being changed dynamically - whatever protocol is set for your callback URL in
the Customer Management System will be used with the dynamic callback URL.
=head2 Putting it all together
The typical way to implement all of this is as follows:
=over 4
=item 1 Get necessary merchant information (instId, currency, callback
password, and MD5 password).
=item 2 Once the customer has selected what to purchase, generate a cartId (a
random MD5 hex string works well - but I<do not> use the MD5 signature!), and
L<generate the MD5 signature|/"MD5 signing">.
=item 3 Store the cartId somewhere (i.e. in the database).
=item 4 Make a form with all the necessary fields that
L<submits to WorldPay|/"Directing customers to WorldPay">.
=item 5 Set up the necessary callbacks (at least L<C<on_valid>|/"on_valid"> and
L<C<on_valid>|/"on_cancel">). If using a dedicated CGI script for WorldPay
callbacks, it should just call process(); otherwise, check for the CGI
parameter 'transStatus' and if present, call process().
=item 6 For a valid payment, do whatever you need to do for a valid payment,
and store some record of the payment having been made (storing at least the
cartId, the transId, and the futurePayId is strongly recommended). Use the CGI
parameter 'cartId' to locate the order (i.e. in the database). It's
recommended that you check Appendix A of the "Select Junior Integration Guide"
for all available parameters.
=back
=head1 SEE ALSO
L<http://support.worldpay.com> - WorldPay Knowledge Base, containing many
useful WorldPay manuals and instructions.
L<http://support.worldpay.com/kb/integration_guides/junior/integration/help/sjig.html>
- Select Junior Integration Guide, from which this documentation and module is
primarily derived.
L<http://support.worldpay.com/kb/product_guides/futurepay/repeatbilling.html> -
Repeat Billing with FuturePay.
=head1 MAINTAINER
Jason Rhinelander
=head1 COPYRIGHT
Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
http://www.gossamer-threads.com/
=head1 VERSION
Revision: $Id: WorldPay.pm,v 1.9 2006/08/22 23:03:14 brewt Exp $
This module is designed for version 4.4 of the Select Junior payment
integration.
=cut