First pass at adding key files
This commit is contained in:
		@@ -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;
 | 
			
		||||
@@ -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;
 | 
			
		||||
		Reference in New Issue
	
	Block a user