diff options
author | jeff <jeff> | 2007-11-20 03:03:08 +0000 |
---|---|---|
committer | jeff <jeff> | 2007-11-20 03:03:08 +0000 |
commit | 511331906b11bc8f104f49b164a0a4b262099b0d (patch) | |
tree | 813b37c784438b6d99e2f854f702e1006fa09cce /AuthorizeNet | |
parent | 63544739784abac4d9740323b609e554d58584e6 (diff) |
add ARB (recurring authorizations/subscriptions) support
Diffstat (limited to 'AuthorizeNet')
-rw-r--r-- | AuthorizeNet/AIM.pm | 315 | ||||
-rw-r--r-- | AuthorizeNet/ARB.pm | 342 |
2 files changed, 657 insertions, 0 deletions
diff --git a/AuthorizeNet/AIM.pm b/AuthorizeNet/AIM.pm new file mode 100644 index 0000000..7e4e963 --- /dev/null +++ b/AuthorizeNet/AIM.pm @@ -0,0 +1,315 @@ +package Business::OnlinePayment::AuthorizeNet::AIM; + +use strict; +use Carp; +use Business::OnlinePayment::AuthorizeNet; +use Net::SSLeay qw/make_form post_https make_headers/; +use Text::CSV_XS; +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); + +require Exporter; + +@ISA = qw(Exporter Business::OnlinePayment::AuthorizeNet); +@EXPORT = qw(); +@EXPORT_OK = qw(); +$VERSION = '3.18'; + +sub set_defaults { + my $self = shift; + + $self->server('secure.authorize.net') unless $self->server; + $self->port('443') unless $self->port; + $self->path('/gateway/transact.dll') unless $self->path; + + $self->build_subs(qw( order_number md5 avs_code cvv2_response + cavv_response + )); +} + +sub map_fields { + my($self) = @_; + + my %content = $self->content(); + + # ACTION MAP + my %actions = ('normal authorization' => 'AUTH_CAPTURE', + 'authorization only' => 'AUTH_ONLY', + 'credit' => 'CREDIT', + 'post authorization' => 'PRIOR_AUTH_CAPTURE', + 'void' => 'VOID', + ); + $content{'action'} = $actions{lc($content{'action'})} || $content{'action'}; + + # TYPE MAP + my %types = ('visa' => 'CC', + 'mastercard' => 'CC', + 'american express' => 'CC', + 'discover' => 'CC', + 'check' => 'ECHECK', + ); + $content{'type'} = $types{lc($content{'type'})} || $content{'type'}; + $self->transaction_type($content{'type'}); + + # ACCOUNT TYPE MAP + my %account_types = ('personal checking' => 'CHECKING', + 'personal savings' => 'SAVINGS', + 'business checking' => 'CHECKING', + 'business savings' => 'SAVINGS', + ); + $content{'account_type'} = $account_types{lc($content{'account_type'})} + || $content{'account_type'}; + + $content{'referer'} = defined( $content{'referer'} ) + ? make_headers( 'Referer' => $content{'referer'} ) + : ""; + + if (length $content{'password'} == 15) { + $content{'transaction_key'} = delete $content{'password'}; + } + + # stuff it back into %content + $self->content(%content); +} + +sub remap_fields { + my($self,%map) = @_; + + my %content = $self->content(); + foreach(keys %map) { + $content{$map{$_}} = $content{$_}; + } + $self->content(%content); +} + +sub get_fields { + my($self,@fields) = @_; + + my %content = $self->content(); + my %new = (); + foreach( grep defined $content{$_}, @fields) { $new{$_} = $content{$_}; } + return %new; +} + +sub submit { + my($self) = @_; + + $self->map_fields(); + $self->remap_fields( + type => 'x_Method', + login => 'x_Login', + password => 'x_Password', + transaction_key => 'x_Tran_Key', + action => 'x_Type', + description => 'x_Description', + amount => 'x_Amount', + currency => 'x_Currency_Code', + invoice_number => 'x_Invoice_Num', + order_number => 'x_Trans_ID', + auth_code => 'x_Auth_Code', + customer_id => 'x_Cust_ID', + customer_ip => 'x_Customer_IP', + last_name => 'x_Last_Name', + first_name => 'x_First_Name', + company => 'x_Company', + address => 'x_Address', + city => 'x_City', + state => 'x_State', + zip => 'x_Zip', + country => 'x_Country', + ship_last_name => 'x_Ship_To_Last_Name', + ship_first_name => 'x_Ship_To_First_Name', + ship_company => 'x_Ship_To_Company', + ship_address => 'x_Ship_To_Address', + ship_city => 'x_Ship_To_City', + ship_state => 'x_Ship_To_State', + ship_zip => 'x_Ship_To_Zip', + ship_country => 'x_Ship_To_Country', + phone => 'x_Phone', + fax => 'x_Fax', + email => 'x_Email', + email_customer => 'x_Email_Customer', + card_number => 'x_Card_Num', + expiration => 'x_Exp_Date', + cvv2 => 'x_Card_Code', + check_type => 'x_Echeck_Type', + account_name => 'x_Bank_Acct_Name', + account_number => 'x_Bank_Acct_Num', + account_type => 'x_Bank_Acct_Type', + bank_name => 'x_Bank_Name', + routing_code => 'x_Bank_ABA_Code', + customer_org => 'x_Customer_Organization_Type', + customer_ssn => 'x_Customer_Tax_ID', + license_num => 'x_Drivers_License_Num', + license_state => 'x_Drivers_License_State', + license_dob => 'x_Drivers_License_DOB', + recurring_billing => 'x_Recurring_Billing', + ); + + my $auth_type = $self->{_content}->{transaction_key} + ? 'transaction_key' + : 'password'; + + my @required_fields = ( qw(type action login), $auth_type ); + + unless ( $self->{_content}->{action} eq 'VOID' ) { + + if ($self->transaction_type() eq "ECHECK") { + + push @required_fields, qw( + amount routing_code account_number account_type bank_name + account_name + ); + + if (defined $self->{_content}->{customer_org} and + length $self->{_content}->{customer_org} + ) { + push @required_fields, qw( customer_org customer_ssn ); + } else { + push @required_fields, qw(license_num license_state license_dob); + } + + } elsif ($self->transaction_type() eq 'CC' ) { + + if ( $self->{_content}->{action} eq 'PRIOR_AUTH_CAPTURE' ) { + if ( $self->{_content}->{order_number} ) { + push @required_fields, qw( amount order_number ); + } else { + push @required_fields, qw( amount card_number expiration ); + } + } elsif ( $self->{_content}->{action} eq 'CREDIT' ) { + push @required_fields, qw( amount order_number card_number ); + } else { + push @required_fields, qw( + amount last_name first_name card_number expiration + ); + } + } else { + Carp::croak( "AuthorizeNet can't handle transaction type: ". + $self->transaction_type() ); + } + + } + + $self->required_fields(@required_fields); + + my %post_data = $self->get_fields(qw/ + x_Login x_Password x_Tran_Key x_Invoice_Num + x_Description x_Amount x_Cust_ID x_Method x_Type x_Card_Num x_Exp_Date + x_Card_Code x_Auth_Code x_Echeck_Type x_Bank_Acct_Num + x_Bank_Account_Name x_Bank_ABA_Code x_Bank_Name x_Bank_Acct_Type + x_Customer_Organization_Type x_Customer_Tax_ID x_Customer_IP + x_Drivers_License_Num x_Drivers_License_State x_Drivers_License_DOB + x_Last_Name x_First_Name x_Company + x_Address x_City x_State x_Zip + x_Country + x_Ship_To_Last_Name x_Ship_To_First_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_Phone x_Fax x_Email x_Email_Customer x_Country + x_Currency_Code x_Trans_ID/); + + $post_data{'x_Test_Request'} = $self->test_transaction() ? 'TRUE' : 'FALSE'; + + #deal with perl-style bool + if ( $post_data{'x_Email_Customer'} + && $post_data{'x_Email_Customer'} !~ /^FALSE$/i ) { + $post_data{'x_Email_Customer'} = 'TRUE'; + } else { + $post_data{'x_Email_Customer'} = 'FALSE'; + } + + $post_data{'x_ADC_Delim_Data'} = 'TRUE'; + $post_data{'x_delim_char'} = ','; + $post_data{'x_encap_char'} = '"'; + $post_data{'x_ADC_URL'} = 'FALSE'; + $post_data{'x_Version'} = '3.1'; + + my $pd = make_form(%post_data); + my $s = $self->server(); + my $p = $self->port(); + my $t = $self->path(); + my $r = $self->{_content}->{referer}; + my($page,$server_response,%headers) = post_https($s,$p,$t,$r,$pd); + #escape NULL (binary 0x00) values + $page =~ s/\x00/\^0/g; + + #trim 'ip_addr="1.2.3.4"' added by eProcessingNetwork Authorize.Net compat + $page =~ s/,ip_addr="[\d\.]+"$//; + + my $csv = new Text::CSV_XS({ binary=>1, escape_char=>'' }); + $csv->parse($page); + my @col = $csv->fields(); + + $self->server_response($page); + $self->avs_code($col[5]); + $self->order_number($col[6]); + $self->md5($col[37]); + $self->cvv2_response($col[38]); + $self->cavv_response($col[39]); + + if($col[0] eq "1" ) { # Authorized/Pending/Test + $self->is_success(1); + $self->result_code($col[0]); + if ($col[4] =~ /^(.*)\s+(\d+)$/) { #eProcessingNetwork extra bits.. + $self->authorization($2); + } else { + $self->authorization($col[4]); + } + } else { + $self->is_success(0); + $self->result_code($col[2]); + $self->error_message($col[3]); + unless ( $self->result_code() ) { #additional logging information + #$page =~ s/\x00/\^0/g; + $self->error_message($col[3]. + " DEBUG: No x_response_code from server, ". + "(HTTPS response: $server_response) ". + "(HTTPS headers: ". + join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ". + "(Raw HTTPS content: $page)" + ); + } + } +} + +1; +__END__ + +=head1 NAME + +Business::OnlinePayment::AuthorizeNet::AIM - AuthorizeNet AIM backend for Business::OnlinePayment + +=head1 AUTHOR + +Jason Kohles, jason@mediabang.com + +Ivan Kohler <ivan-authorizenet@420.am> updated it for Authorize.Net protocol +3.0/3.1 and is the current maintainer. Please send patches as unified diffs +(diff -u). + +Jason Spence <jspence@lightconsulting.com> contributed support for separate +Authorization Only and Post Authorization steps and wrote some docs. +OST <services@ostel.com> paid for it. + +T.J. Mather <tjmather@maxmind.com> sent a number of CVV2 patches. + +Mike Barry <mbarry@cos.com> sent in a patch for the referer field. + +Yuri V. Mkrtumyan <yuramk@novosoft.ru> sent in a patch to add the void action. + +Paul Zimmer <AuthorizeNetpm@pzimmer.box.bepress.com> sent in a patch for +card-less post authorizations. + +Daemmon Hughes <daemmon@daemmonhughes.com> sent in a patch for "transaction +key" authentication as well support for the recurring_billing flag and the md5 +method that returns the MD5 hash which is returned by the gateway. + +Steve Simitzis contributed a patch for better compatibility with +eProcessingNetwork's AuthorizeNet compatability mode. + +=head1 SEE ALSO + +perl(1). L<Business::OnlinePayment> L<Business::OnlinePayment::AuthorizeNet>. + +=cut + diff --git a/AuthorizeNet/ARB.pm b/AuthorizeNet/ARB.pm new file mode 100644 index 0000000..16fddae --- /dev/null +++ b/AuthorizeNet/ARB.pm @@ -0,0 +1,342 @@ +package Business::OnlinePayment::AuthorizeNet::ARB; + +use strict; +use Carp; +use Business::OnlinePayment::AuthorizeNet; +use Business::OnlinePayment::HTTPS; +use XML::Simple; +use XML::Writer; +use Tie::IxHash; +use vars qw($VERSION $DEBUG @ISA $me); + +@ISA = qw(Business::OnlinePayment::AuthorizeNet Business::OnlinePayment::HTTPS); +$VERSION = '0.01'; +$DEBUG = 0; +$me='Business::OnlinePayment::AuthorizeNet::ARB'; + +sub set_defaults { + my $self = shift; + + $self->server('api.authorize.net') unless $self->server; + $self->port('443') unless $self->port; + $self->path('/xml/v1/request.api') unless $self->path; + + $self->build_subs(qw( order_number md5 avs_code cvv2_response + cavv_response + )); +} + +sub map_fields { + my($self) = @_; + + my %content = $self->content(); + + # ACTION MAP + my %actions = ('recurring authorization' + => 'ARBCreateSubscriptionRequest', + 'modify recurring authorization' + => 'ARBUpdateSubscriptionRequest', + 'cancel recurring authorization' + => 'ARBCancelSubscriptionRequest', + ); + $content{'action'} = $actions{lc($content{'action'})} || $content{'action'}; + + # TYPE MAP + my %types = ('visa' => 'CC', + 'mastercard' => 'CC', + 'american express' => 'CC', + 'discover' => 'CC', + 'check' => 'ECHECK', + ); + $content{'type'} = $types{lc($content{'type'})} || $content{'type'}; + $self->transaction_type($content{'type'}); + + # ACCOUNT TYPE MAP + my %account_types = ('personal checking' => 'checking', + 'personal savings' => 'savings', + 'business checking' => 'businessChecking', + 'business savings' => 'savings', + ); + $content{'account_type'} = $account_types{lc($content{'account_type'})} + || $content{'account_type'}; + + # MASSAGE EXPIRATION + $content{'expdate_yyyymm'} = $self->expdate_yyyymm($content{'expiration'}); + + # stuff it back into %content + $self->content(%content); + +} + +sub revmap_fields { + my $self = shift; + tie my(%map), 'Tie::IxHash', @_; + my %content = $self->content(); + map { + my $value; + if ( ref( $map{$_} ) eq 'HASH' ) { + $value = $map{$_} if ( keys %{ $map{$_} } ); + }elsif( exists( $content{ $map{$_} } ) ) { + $value = $content{ $map{$_} }; + } + + if (defined($value)) { + ($_ => $value); + }else{ + (); + } + } (keys %map); +} + +sub expdate_yyyymm { + my $self = shift; + my $expiration = shift; + my $expdate_yyyymm; + if ( defined($expiration) and $expiration =~ /^(\d{1,2})\D+(\d{2})$/ ) { + my ( $month, $year ) = ( $1, $2 ); + $expdate_yyyymm = sprintf( "20%02d-%02d", $year, $month ); + } + return defined($expdate_yyyymm) ? $expdate_yyyymm : $expiration; +}; + +sub _xmlwrite { + my ($self, $writer, $item, $value) = @_; + $writer->startTag($item); + if ( ref( $value ) eq 'HASH' ) { + foreach ( keys ( %$value ) ) { + $self->_xmlwrite($writer, $_, $value->{$_}); + } + }else{ + $writer->characters($value); + } + $writer->endTag($item); +} + +sub submit { + my($self) = @_; + + $self->map_fields(); + + my @required_fields = qw(action login password); + + if ( $self->{_content}->{action} eq 'ARBCreateSubscriptionRequest' ) { + push @required_fields, + qw( type interval start periods amount first_name last_name ); + + if ($self->transaction_type() eq "ECHECK") { + push @required_fields, + qw( amount routing_code account_number account_type account_name + check_type + ); + } elsif ($self->transaction_type() eq 'CC' ) { + push @required_fields, qw( card_number expiration ); + } + }elsif ( $self->{_content}->{action} eq 'ARBUpdateSubscriptionRequest' ) { + push @required_fields, qw( subscription ); + }elsif ( $self->{_content}->{action} eq 'ARBCancelSubscriptionRequest' ) { + push @required_fields, qw( subscription ); + }else{ + croak "$me can't handle transaction type: ". + $self->{_content}->{action}. " for ". + $self->transaction_type(); + } + + $self->required_fields(@required_fields); + + tie my %merchant, 'Tie::IxHash', + $self->revmap_fields( + name => 'login', + transactionKey => 'password', + ); + + my ($length,$unit) = + ($self->{_content}->{interval} or '') =~ /^\s*(\d+)\s+(day|month)s?\s*$/; + tie my %interval, 'Tie::IxHash', ( + ($length ? (length => $length) : () ), + ($unit ? (unit => $unit.'s') : () ), + ); + + tie my %schedule, 'Tie::IxHash', + $self->revmap_fields( + interval => \%interval, + startDate => 'start', + totalOccurrences => 'periods', + trialOccurrences => 'trialperiods', + phoneNumber => 'phone', + ); + + tie my %account, 'Tie::IxHash', ( + ( defined($self->transaction_type()) + && $self->transaction_type() eq 'CC' + ) ? $self->revmap_fields( + cardNumber => 'card_number', + expirationDate => 'expdate_yyyymm', + ) + : $self->revmap_fields( + accountType => 'account_type', + routingNumber => 'routing_code', + accountNumber => 'account_number', + nameOnAccount => 'account_name', + echeckType => 'check_type', + bankName => 'bank_name', + ) + ); + + tie my %payment, 'Tie::IxHash', + $self->revmap_fields( + ( ( defined($self->transaction_type()) && # require? + $self->transaction_type() eq 'CC' + ) ? 'creditCard' + : 'bankAccount' + ) => \%account, + ); + + tie my %order, 'Tie::IxHash', + $self->revmap_fields( + invoiceNumber => 'invoice_number', + description => 'description', + ); + + tie my %drivers, 'Tie::IxHash', + $self->revmap_fields( + number => 'license_num', + state => 'license_state', + dateOfBirth => 'license_dob', + ); + + tie my %billto, 'Tie::IxHash', + $self->revmap_fields( + firstName => 'first_name', + lastName => 'last_name', + company => 'company', + address => 'address', + city => 'city', + state => 'state', + zip => 'zip', + country => 'country', + ); + + tie my %shipto, 'Tie::IxHash', + $self->revmap_fields( + firstName => 'ship_first_name', + lastName => 'ship_last_name', + company => 'ship_company', + address => 'ship_address', + city => 'ship_city', + state => 'ship_state', + zip => 'ship_zip', + country => 'ship_country', + ); + + tie my %customer, 'Tie::IxHash', + $self->revmap_fields( + type => 'customer_org', + id => 'customer_id', + email => 'email', + phoneNumber => 'phone', + faxNumber => 'fax', + driversLicense => \%drivers, + taxid => 'customer_ssn', + ); + + tie my %sub, 'Tie::IxHash', + $self->revmap_fields( + paymentSchedule => \%schedule, + amount => 'amount', + trialAmount => 'trialamount', + payment => \%payment, + order => \%order, + customer => \%customer, + billTo => \%billto, + shipTo => \%shipto, + ); + + + tie my %req, 'Tie::IxHash', + $self->revmap_fields ( + merchantAuthentication => \%merchant, + subscriptionId => 'subscription', + subscription => \%sub, + ); + + my $ns = "AnetApi/xml/v1/schema/AnetApiSchema.xsd"; + my $post_data; + my $writer = new XML::Writer( OUTPUT => \$post_data, + DATA_MODE => 1, + DATA_INDENT => 1, + ENCODING => 'utf-8', + ); + $writer->xmlDecl(); + $writer->startTag($self->{_content}->{action}, 'xmlns', $ns); + foreach ( keys ( %req ) ) { + $self->_xmlwrite($writer, $_, $req{$_}); + } + $writer->endTag($self->{_content}->{action}); + $writer->end(); + + if ($self->test_transaction()) { + $self->server('apitest.authorize.net'); + } + + warn $post_data if $DEBUG; + my($page,$server_response,%headers) = + $self->https_post( { 'Content-Type' => 'text/xml' }, $post_data); + + #trim leading (4?) characters of unknown origin not in spec + $page =~ s/^(.*?)</</; + my $garbage=$1; + warn "Trimmed $garbage from response page.\n" if $DEBUG; + + warn $page if $DEBUG; + + my $response; + my $message; + if ($server_response =~ /200/){ + $response = XMLin($page); + if (ref($response->{messages}->{message}) eq 'ARRAY') { + $message = $response->{messages}->{message}->[0]; + }else{ + $message = $response->{messages}->{message}; + } + }else{ + $response->{messages}->{resultCode} = "Server Failed"; + $message->{code} = $server_response; + } + + $self->server_response($page); + $self->order_number($response->{subscriptionId}); + $self->result_code($message->{code}); + $self->error_message($message->{text}); + + if($response->{messages}->{resultCode} eq "Ok" ) { + $self->is_success(1); + } else { + $self->is_success(0); + unless ( $self->error_message() ) { #additional logging information + $self->error_message( + "(HTTPS response: $server_response) ". + "(HTTPS headers: ". + join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ". + "(Raw HTTPS content: $page)" + ); + } + } +} + +1; +__END__ + +=head1 NAME + +Business::OnlinePayment::AuthorizeNet::ARB - AuthorizeNet ARB backend for Business::OnlinePayment + +=head1 AUTHOR + +Jeff Finucane, authorizenetarb@weasellips.com + +=head1 SEE ALSO + +perl(1). L<Business::OnlinePayment>. + +=cut + |