# Vend::Payment::AuthorizeNet - Interchange AuthorizeNet support # # Connection routine for AuthorizeNet version 3 using the 'ADC Direct Response' # method. # # Copyright (C) 2003-2011 Interchange Development Group, https://www.interchangecommerce.org/ # Copyright (C) 1999-2002 Red Hat, Inc. # # Authors: # mark@summersault.com # Mike Heins # Jeff Nappi # Paul Delys # webmaster@nameastar.net # Ray Desjardins # Nelson H Ferrari # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this program; if not, write to the Free # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, # MA 02110-1301 USA. package Vend::Payment::AuthorizeNet; =head1 NAME Vend::Payment::AuthorizeNet - Interchange AuthorizeNet Support =head1 SYNOPSIS &charge=authorizenet or [charge mode=authorizenet param1=value1 param2=value2] =head1 PREREQUISITES Net::SSLeay or LWP::UserAgent and Crypt::SSLeay Only one of these need be present and working. =head1 DESCRIPTION The Vend::Payment::AuthorizeNet module implements the authorizenet() routine for use with Interchange. It is compatible on a call level with the other Interchange payment modules. To enable this module, place this directive in C: Require module Vend::Payment::AuthorizeNet This I be in interchange.cfg or a file included from it. Make sure CreditCardAuto is off (default in Interchange demos). The mode can be named anything, but the C parameter must be set to C. To make it the default payment gateway for all credit card transactions in a specific catalog, you can set in C: Variable MV_PAYMENT_MODE authorizenet It uses several of the standard settings from Interchange payment. Any time we speak of a setting, it is obtained either first from the tag/call options, then from an Interchange order Route named for the mode, then finally a default global payment variable, For example, the C parameter would be specified by: [charge mode=authorizenet id=YourAuthorizeNetID] or Route authorizenet id YourAuthorizeNetID or Variable MV_PAYMENT_ID YourAuthorizeNetID The active settings are: =over 4 =item id Your Authorize.net account ID, supplied by Authorize.net when you sign up. Global parameter is MV_PAYMENT_ID. =item secret Your Authorize.net account password, supplied by Authorize.net when you sign up. Global parameter is MV_PAYMENT_SECRET. This may not be needed for actual charges. =item host The Authorize.net host to be used. The default is secure.authorize.net Authorize.net recommends you use their Akamai host, and to do so you set (as in the example above): Route authorizenet host secure2.authorize.net =item referer A valid referring url (match this with your setting on secure.authorize.net). Global parameter is MV_PAYMENT_REFERER. =item transaction The type of transaction to be run. Valid values are: Interchange AuthorizeNet ---------------- ----------------- auth AUTH_ONLY return CREDIT reverse PRIOR_AUTH_CAPTURE sale AUTH_CAPTURE settle CAPTURE_ONLY void VOID =item remap This remaps the form variable names to the ones needed by Authorize.net. See the C heading in the Interchange documentation for use. =item test Set this to C if you wish to operate in test mode, i.e. set the Authorize.net C query parameter to TRUE. Examples: Route authorizenet test TRUE or Variable MV_PAYMENT_TEST TRUE or [charge mode=authorizenet test=TRUE] =back =head2 Troubleshooting Try the instructions above, then enable test mode. A test order should complete. Disable test mode, then test in various Authorize.net error modes by using the credit card number 4222 2222 2222 2222. Then try a sale with the card number C<4111 1111 1111 1111> and a valid expiration date. The sale should be denied, and the reason should be in [data session payment_error]. If nothing works: =over 4 =item * Make sure you "Require"d the module in interchange.cfg: Require module Vend::Payment::AuthorizeNet =item * Make sure either Net::SSLeay or Crypt::SSLeay and LWP::UserAgent are installed and working. You can test to see whether your Perl thinks they are: perl -MNet::SSLeay -e 'print "It works\n"' or perl -MLWP::UserAgent -MCrypt::SSLeay -e 'print "It works\n"' If either one prints "It works." and returns to the prompt you should be OK (presuming they are in working order otherwise). =item * Check the error logs, both catalog and global. =item * Make sure you set your payment parameters properly. =item * Try an order, then put this code in a page: [calc] my $string = $Tag->uneval( { ref => $Session->{payment_result} }); $string =~ s/{/{\n/; $string =~ s/,/,\n/g; return $string; [/calc] That should show what happened. =item * If all else fails, consultants are available to help with integration for a fee. See https://www.interchangecommerce.org/ for mailing lists and other information. =back =head1 BUGS There is actually nothing *in* Vend::Payment::AuthorizeNet. It changes packages to Vend::Payment and places things there. =head1 AUTHORS Mark Stosberg . Based on original code by Mike Heins . =head1 CREDITS Jeff Nappi Paul Delys webmaster@nameastar.net Ray Desjardins Nelson H. Ferrari =cut BEGIN { my $selected; eval { package Vend::Payment; require Net::SSLeay; import Net::SSLeay qw(post_https make_form make_headers); $selected = "Net::SSLeay"; }; $Vend::Payment::Have_Net_SSLeay = 1 unless $@; unless ($Vend::Payment::Have_Net_SSLeay) { eval { package Vend::Payment; require LWP::UserAgent; require HTTP::Request::Common; require Crypt::SSLeay; import HTTP::Request::Common qw(POST); $selected = "LWP and Crypt::SSLeay"; }; $Vend::Payment::Have_LWP = 1 unless $@; } unless ($Vend::Payment::Have_Net_SSLeay or $Vend::Payment::Have_LWP) { die __PACKAGE__ . " requires Net::SSLeay or Crypt::SSLeay"; } ::logGlobal("%s payment module initialized, using %s", __PACKAGE__, $selected) unless $Vend::Quiet or ! $Global::VendRoot; } package Vend::Payment; use strict; sub authorizenet { my ($user, $amount) = @_; my $opt; my $secret; my $shipping; my $salestax; if(ref $user) { $opt = $user; $user = $opt->{id} || undef; $secret = $opt->{secret} || undef; } else { $opt = {}; } my $actual; if($opt->{actual}) { $actual = $opt->{actual}; } else { my (%actual) = map_actual(); $actual = \%actual; } #::logDebug("actual map result: " . ::uneval($actual)); if (! $user ) { $user = charge_param('id') or return ( MStatus => 'failure-hard', MErrMsg => errmsg('No account id'), ); } $secret = charge_param('secret') if ! $secret; if (! defined $opt->{accept_for_review}) { $opt->{accept_for_review} = charge_param('accept_for_review'); } $opt->{host} ||= 'secure.authorize.net'; $opt->{script} ||= '/gateway/transact.dll'; $opt->{port} ||= 443; $opt->{method} ||= charge_param('method') || 'CC'; my $precision = $opt->{precision} || 2; my $referer = $opt->{referer} || charge_param('referer'); my @override = qw/ order_id auth_code mv_credit_card_exp_month mv_credit_card_exp_year mv_credit_card_number /; for(@override) { next unless defined $opt->{$_}; $actual->{$_} = $opt->{$_}; } ## Authorizenet does things a bit different, ensure we are OK $actual->{mv_credit_card_exp_month} =~ s/\D//g; $actual->{mv_credit_card_exp_month} =~ s/^0+//; $actual->{mv_credit_card_exp_year} =~ s/\D//g; $actual->{mv_credit_card_exp_year} =~ s/\d\d(\d\d)/$1/; $actual->{mv_credit_card_number} =~ s/\D//g; my $exp = sprintf '%02d%02d', $actual->{mv_credit_card_exp_month}, $actual->{mv_credit_card_exp_year}; # Using mv_payment_mode for compatibility with older versions, probably not # necessary. $opt->{transaction} ||= 'sale'; my $transtype = $opt->{transaction}; my %type_map = ( AUTH_ONLY => 'AUTH_ONLY', CAPTURE_ONLY => 'CAPTURE_ONLY', CREDIT => 'CREDIT', PRIOR_AUTH_CAPTURE => 'PRIOR_AUTH_CAPTURE', VOID => 'VOID', auth => 'AUTH_ONLY', authorize => 'AUTH_ONLY', mauthcapture => 'AUTH_CAPTURE', mauthonly => 'AUTH_ONLY', return => 'CREDIT', settle_prior => 'PRIOR_AUTH_CAPTURE', sale => 'AUTH_CAPTURE', settle => 'CAPTURE_ONLY', void => 'VOID', ); if (defined $type_map{$transtype}) { $transtype = $type_map{$transtype}; } my %allowed_map = ( CC => { AUTH_CAPTURE => 1, AUTH_ONLY => 1, CAPTURE_ONLY => 1, CREDIT => 1, VOID => 1, PRIOR_AUTH_CAPTURE => 1, }, ECHECK => { AUTH_CAPTURE => 1, CREDIT => 1, VOID => 1, }, ); if(! $allowed_map{$opt->{method}}) { ::logDebug("Unknown Authorizenet method $opt->{method}"); } elsif(! $allowed_map{$opt->{method}}{$transtype}) { ::logDebug("Unknown Authorizenet transtype $transtype for $opt->{method}"); } $amount = $opt->{total_cost} if $opt->{total_cost}; if(! $amount) { $amount = Vend::Interpolate::total_cost(); $amount = Vend::Util::round_to_frac_digits($amount,$precision); } $shipping = $opt->{shipping} if $opt->{shipping}; if(! $shipping) { $shipping = Vend::Interpolate::tag_shipping(); $shipping = Vend::Util::round_to_frac_digits($shipping,$precision); } $salestax = $opt->{salestax} if $opt->{salestax}; if(! $salestax) { $salestax = Vend::Interpolate::salestax(); $salestax = Vend::Util::round_to_frac_digits($salestax,$precision); } my $order_id = gen_order_id($opt); #::logDebug("auth_code=$actual->{auth_code} order_id=$opt->{order_id}"); my $accttype = $actual->{check_accttype}; my $echeck_type = 'WEB'; if($accttype eq 'Business Checking') { $echeck_type = 'CCD'; } my %echeck_params = ( x_bank_aba_code => $actual->{check_routing}, x_bank_acct_num => $actual->{check_account}, x_bank_acct_type => $actual->{check_accttype}, x_bank_name => $actual->{check_bankname}, x_bank_acct_name => $actual->{check_acctname}, x_echeck_type => $echeck_type, x_Method => 'ECHECK', ); my $tax_exempt = ($salestax > 0) ? 'FALSE' : 'TRUE'; my %query = ( x_Test_Request => $opt->{test} || charge_param('test'), x_First_Name => $actual->{b_fname}, x_Last_Name => $actual->{b_lname}, x_Company => $actual->{b_company}, x_Address => $actual->{b_address}, x_City => $actual->{b_city}, x_State => $actual->{b_state}, x_Zip => $actual->{b_zip}, x_Country => $actual->{b_country}, x_Ship_To_First_Name => $actual->{fname}, x_Ship_To_Last_Name => $actual->{lname}, x_Ship_To_Company => $actual->{company}, x_Ship_To_Address => $actual->{address}, x_Ship_To_City => $actual->{city}, x_Ship_To_State => $actual->{state}, x_Ship_To_Zip => $actual->{zip}, x_Ship_To_Country => $actual->{country}, x_Email => $actual->{email}, x_Phone => $actual->{phone_day}, x_Type => $transtype, x_Amount => $amount, x_Tax => $salestax, x_Freight => $shipping, x_PO_Num => $actual->{po_number}, x_Tax_Exempt => $tax_exempt, x_Method => 'CC', x_Card_Num => $actual->{mv_credit_card_number}, x_Exp_Date => $exp, x_Card_Code => $actual->{cvv2} || $actual->{mv_credit_card_cvv2}, x_Customer_IP => $Vend::Session->{ohost}, x_Trans_ID => $actual->{order_id}, x_Auth_Code => $actual->{auth_code}, x_Invoice_Num => $actual->{mv_order_number} || $actual->{mv_transaction_id}, x_Password => $secret, x_Login => $user, x_Version => '3.1', x_ADC_URL => 'FALSE', x_ADC_Delim_Data => 'TRUE', x_ADC_Delim_Character => "\037", ); my @query; my @only_cc = qw/ x_Card_Num x_Exp_Date x_Card_Code /; if($opt->{use_transaction_key}) { $query{x_Tran_Key} = delete $query{x_Password}; } if($opt->{method} eq 'ECHECK') { for (@only_cc) { delete $query{$_}; } for(keys %echeck_params) { $query{$_} = $echeck_params{$_}; } } for (keys %query) { my $key = $_; my $val = $query{$key}; $val =~ s/["\$\n\r]//g; $val =~ s/\$//g; my $len = length($val); if($val =~ /[&=]/) { $key .= "[$len]"; } push @query, "$key=$val"; } my %no_trans_id = ( AUTH_CAPTURE => 1, AUTH_ONLY => 1, ); ## As of Jan 31, 2014 AuthorizeNet errors out with x_Trans_ID on transaction type that ## doesn't need it delete $query{x_Trans_ID} if $no_trans_id{ $query{x_Type} }; my $gwl = Vend::Payment::AuthorizeNet -> new({ Enabled => charge_param('gwl_enabled'), LogTable => charge_param('gwl_table'), Source => charge_param('gwl_source'), }) ; { my %munged_query = %query; $munged_query{x_Card_Num} =~ s/^(\d{4})(.*)/$1 . ('X' x length($2))/e if defined $munged_query{x_Card_Num}; $munged_query{$_} = 'X' for grep { defined $munged_query{$_} } qw/x_Password x_Tran_Key/; $munged_query{$_} =~ s/./X/g for grep { defined $munged_query{$_} } qw/x_Card_Code x_bank_aba_code x_bank_acct_num/ ; #::logDebug("Authorizenet query: " . ::uneval(\%munged_query)); $gwl->request(\%munged_query); } $opt->{extra_headers} = { Referer => $referer }; $gwl->start; my $thing = post_data($opt, \%query); $gwl->stop; my $page = $thing->{result_page}; my $response = $thing->{status_line}; # Minivend names are on the left, Authorize.Net on the right my %result_map = ( qw/ pop.status x_response_code pop.error-message x_response_reason_text order-id x_trans_id pop.order-id x_trans_id pop.auth-code x_auth_code pop.avs_code x_avs_code pop.avs_zip x_zip pop.avs_addr x_address pop.cvv2_resp_code x_cvv2_resp_code / ); #::logDebug(qq{\nauthorizenet page: $page response: $response\n}); my %result; @result{ qw/ x_response_code x_response_subcode x_response_reason_code x_response_reason_text x_auth_code x_avs_code x_trans_id x_invoice_num x_description x_amount x_method x_type x_cust_id x_first_name x_last_name x_company x_address x_city x_state x_zip x_country x_phone x_fax x_email x_ship_to_first_name x_ship_to_last_name x_ship_to_company x_ship_to_address x_ship_to_city x_ship_to_state x_ship_to_zip x_ship_to_country x_tax x_duty x_freight x_tax_exempt x_po_num x_MD5_hash x_cvv2_resp_code / } = split (/\037/,$page); $gwl->response(\%result); #::logDebug(qq{authorizenet response_reason_text=$result{x_response_reason_text} response_code: $result{x_response_code}}); for (keys %result_map) { $result{$_} = $result{$result_map{$_}} if defined $result{$result_map{$_}}; } if ($result{x_response_code} == 1) { $result{MStatus} = 'success'; $result{'order-id'} ||= $opt->{order_id}; } elsif ($opt->{accept_for_review} && $result{x_response_code} == 4) { $result{MStatus} = 'success'; $result{'order-id'} ||= $opt->{order_id}; } else { $result{MStatus} = 'failure'; delete $result{'order-id'}; # NOTE: A lot more AVS codes could be checked for here. if ($result{x_avs_code} eq 'N') { my $msg = $opt->{message_avs} || q{You must enter the correct billing address of your credit card. The bank returned the following error: %s}; $result{MErrMsg} = errmsg($msg, $result{x_response_reason_text}); } else { my $msg = $opt->{message_declined} || "Authorizenet error: %s. Please call in your order or try again."; $result{MErrMsg} = errmsg($msg, $result{x_response_reason_text}); } } #::logDebug(qq{authorizenet result=} . uneval(\%result)); return (%result); } package Vend::Payment::AuthorizeNet; use Vend::Payment::GatewayLog; use base qw/Vend::Payment::GatewayLog/; # log_it() must be overridden. sub log_it { my $self = shift; my $request = $self->request; unless ($request) { ::logDebug('Nothing to write to %s: no request present', $self->table); return; } unless ($self->response) { if ($Vend::Payment::Global_Timeout) { my $msg = errmsg('No response. Global timeout triggered'); ::logDebug($msg); $self->response({ x_response_code => -2, x_response_reason_text => $Vend::Payment::Global_Timeout, }); } else { my $msg = errmsg('No response. Reason unknown'); ::logDebug($msg); $self->response({ x_response_code => -3, x_response_reason_text => $msg, }); } } my $response = $self->response; my %fields = ( trans_type => $request->{x_Type} || 'x', processor => 'authorizenet', catalog => $Vend::Cfg->{CatalogName}, result_code => $response->{x_response_code} || '', result_subcode => $response->{x_response_subcode} || '', reason_code => $response->{x_response_reason_code} || '', response_msg => $response->{x_response_reason_text} || '', request_id => $response->{x_trans_id} || '', order_number => $response->{x_invoice_num} || $request->{x_Invoice_Num} || '', request_duration => $self->duration, request_date => $self->timestamp, email => $request->{x_Email} || $response->{x_email} || '', request => ::uneval($request) || '', response => ::uneval($response) || '', session_id => $::Session->{id} || '', request_source => $self->source, amount => $request->{x_Amount} || '', host_ip => $::Session->{shost} || $::Session->{ohost} || '', username => $::Session->{username} || '', cart_md5 => '', ); if (@$Vend::Items) { my $dump = Data::Dumper -> new($Vend::Items) -> Indent(0) -> Terse(1) -> Deepcopy(1) -> Sortkeys(1) ; $fields{cart_md5} = Digest::MD5::md5_hex($dump->Dump); } $self->write(\%fields); } 1;