# ==================================================================== # 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;