788 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
			
		
		
	
	
			788 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
| # ====================================================================
 | |
| # 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;
 | 
