From: Jonathan Prykop Date: Fri, 8 Apr 2016 11:55:51 +0000 (-0500) Subject: RT#41568: Add ACH support to B::OP::ElavonVirtualMerchant X-Git-Url: http://git.freeside.biz/gitweb/?a=commitdiff_plain;h=de665999ca86c3c730c836c938edfb810aa50caf;p=Business-OnlinePayment-ElavonVirtualMerchant.git RT#41568: Add ACH support to B::OP::ElavonVirtualMerchant --- diff --git a/ElavonVirtualMerchant.pm b/ElavonVirtualMerchant.pm index 438c696..29ed092 100644 --- a/ElavonVirtualMerchant.pm +++ b/ElavonVirtualMerchant.pm @@ -1,11 +1,13 @@ package Business::OnlinePayment::ElavonVirtualMerchant; -use base qw(Business::OnlinePayment::viaKLIX); +use base qw(Business::OnlinePayment::HTTPS); use strict; -use vars qw( $VERSION %maxlength ); +use vars qw( $VERSION $DEBUG %maxlength ); +use Carp; $VERSION = '0.03'; $VERSION = eval $VERSION; +$DEBUG = 0; =head1 NAME @@ -15,9 +17,9 @@ Business::OnlinePayment::ElavonVirtualMerchant - Elavon Virtual Merchant backend use Business::OnlinePayment::ElavonVirtualMerchant; - my $tx = new Business::OnlinePayment("ElavonVirtualMerchant", { default_ssl_userid => 'whatever' }); + my $tx = new Business::OnlinePayment("ElavonVirtualMerchant", { default_ssl_user_id => 'whatever' }); $tx->content( - type => 'VISA', + type => 'CC', login => 'testdrive', password => '', #password or transaction key action => 'Normal Authorization', @@ -33,7 +35,7 @@ Business::OnlinePayment::ElavonVirtualMerchant - Elavon Virtual Merchant backend zip => '84058', card_number => '4007000000027', expiration => '09/02', - cvv2 => '1234', #optional + cvv2 => '1234', ); $tx->submit(); @@ -45,21 +47,50 @@ Business::OnlinePayment::ElavonVirtualMerchant - Elavon Virtual Merchant backend =head1 DESCRIPTION -This module lets you use the Elavon (formerly Nova Information Systems) Virtual Merchant real-time payment gateway, a successor to viaKlix, from an application that uses the Business::OnlinePayment interface. +This module lets you use the Elavon (formerly Nova Information Systems) Converge +(formerly Virtual Merchant, a successor of viaKlix) real-time payment gateway +from an application that uses the Business::OnlinePayment interface. -You need an account with Elavon. Elavon uses a three-part set of credentials to allow you to configure multiple 'virtual terminals'. Since Business::OnlinePayment only passes a login and password with each transaction, you must pass the third item, the user_id, to the constructor. +You need an account with Elavon. Elavon uses a three-part set of credentials to +allow you to configure multiple 'virtual terminals'. Since Business::OnlinePayment +only passes a login and password with each transaction, you must pass the third item, +default_ssl_user_id, to the constructor. You may pass defaults for other Converge +request fields to the constructor by prepending the field names with default_. -Elavon offers a number of transaction types, including electronic gift card operations and 'PINless debit'. Of these, only credit card transactions fit the Business::OnlinePayment model. +Converge offers a number of transaction types. Of these, only credit card sale +(ccsale), credit card refund (cccredit) and echeck sale (ecspurchase) transactions +are currently supported. -Since the Virtual Merchant API is just a newer version of the viaKlix API, this module subclasses Business::OnlinePayment::viaKlix. +=head1 SUBROUTINES -This module does not use Elavon's XML encoding as this doesn't appear to offer any benefit over the standard encoding. +=cut -=head1 SUBROUTINES +=head2 debug LEVEL + +Get/set debug level + +=cut + +sub debug { + my $self = shift; + + if (@_) { + my $level = shift || 0; + if ( ref($self) ) { + $self->{"__DEBUG"} = $level; + } + else { + $DEBUG = $level; + } + $Business::OnlinePayment::HTTPS::DEBUG = $level; + } + return ref($self) ? ( $self->{"__DEBUG"} || $DEBUG ) : $DEBUG; +} =head2 set_defaults -Sets defaults for the Virtual Merchant gateway URL. +Sets defaults for the Converge gateway URL +and initializes internal data structures. =cut @@ -67,12 +98,30 @@ sub set_defaults { my $self = shift; my %opts = @_; - $self->SUPER::set_defaults(%opts); # standard B::OP methods/data $self->server("www.myvirtualmerchant.com"); $self->port("443"); $self->path("/VirtualMerchant/process.do"); + $self->build_subs(qw( + order_number avs_code cvv2_response + response_page response_code response_headers + )); + + # module specific data + if ( $opts{debug} ) { + $self->debug( $opts{debug} ); + delete $opts{debug}; + } + + my %_defaults = (); + foreach my $key (keys %opts) { + $key =~ /^default_(\w*)$/ or next; + $_defaults{$1} = $opts{$key}; + delete $opts{$key}; + } + $self->{_defaults} = \%_defaults; + } =head2 _map_fields @@ -86,32 +135,111 @@ sub _map_fields { my %content = $self->content(); - #ACTION MAP - my %actions = ( - 'normal authorization' => 'CCSALE', # Authorization/Settle transaction - 'credit' => 'CCCREDIT', # Credit (refund) - ); + if (uc($self->transaction_type) eq 'ECHECK') { - $content{'ssl_transaction_type'} = $actions{ lc( $content{'action'} ) } - || $content{'action'}; + $content{'ssl_transaction_type'} = 'ECSPURCHASE'; - # TYPE MAP - my %types = ( - 'visa' => 'CC', - 'mastercard' => 'CC', - 'american express' => 'CC', - 'discover' => 'CC', - 'cc' => 'CC', - ); + } else { # or credit card, or non-supported type (support checked during submit) + + #ACTION MAP + my %actions = ( + 'normal authorization' => 'CCSALE', # Authorization/Settle transaction + 'credit' => 'CCCREDIT', # Credit (refund) + ); + + $content{'ssl_transaction_type'} = $actions{ lc( $content{'action'} ) } + || $content{'action'}; + + # TYPE MAP + my %types = ( + 'visa' => 'CC', + 'mastercard' => 'CC', + 'american express' => 'CC', + 'discover' => 'CC', + 'cc' => 'CC', + ); - $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'}; + $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'}; - $self->transaction_type( $content{'type'} ); + $self->transaction_type( $content{'type'} ); + + } # end credit card # stuff it back into %content $self->content(%content); } +=head2 _revmap_fields + +Accepts I<%map> and sets the content field specified +by map keys to be the value of the content field +specified by map values, e.g. + + ssl_merchant_id => 'login' + +will set ssl_merchant_id to the current value of login. + +Values may also be references to strings, e.g. + + ssl_exp_date => \$expdate_mmyy, + +will set ssl_exp_date to the value of $expdate_mmyy. + +=cut + +sub _revmap_fields { + my ( $self, %map ) = @_; + my %content = $self->content(); + foreach ( keys %map ) { + $content{$_} = + ref( $map{$_} ) + ? ${ $map{$_} } + : $content{ $map{$_} }; + } + $self->content(%content); +} + +=head2 expdate_mmyy + +Accepts I<$expiration>. Returns mmyy normalized value, +or original value if it couldn't be normalized. + +=cut + +sub expdate_mmyy { + my $self = shift; + my $expiration = shift; + my $expdate_mmyy; + if ( defined($expiration) and $expiration =~ /^(\d+)\D+\d*(\d{2})$/ ) { + my ( $month, $year ) = ( $1, $2 ); + $expdate_mmyy = sprintf( "%02d", $month ) . $year; + } + return defined($expdate_mmyy) ? $expdate_mmyy : $expiration; +} + +=head2 required_fields + +Accepts I<@fields> and makes sure each of those fields +have been set in content. + +=cut + +sub required_fields { + my($self,@fields) = @_; + + my @missing; + my %content = $self->content(); + foreach(@fields) { + next + if (exists $content{$_} && defined $content{$_} && $content{$_}=~/\S+/); + push(@missing, $_); + } + + Carp::croak("missing required field(s): " . join(", ", @missing) . "\n") + if(@missing); + +} + =head2 submit Maps data from Business::OnlinePayment name space to Elavon's, checks that all required fields @@ -145,26 +273,53 @@ sub submit { $self->_map_fields(); my %content = $self->content; + warn "INITIAL PARAMETERS:\n" . join("\n", map{ "$_ => $content{$_}" } keys(%content)) if $self->debug; my %required; - $required{CC_CCSALE} = [ qw( ssl_transaction_type ssl_merchant_id ssl_pin - ssl_amount ssl_card_number ssl_exp_date - ssl_cvv2cvc2_indicator - ) ]; + my @alwaysrequired = qw( + ssl_transaction_type + ssl_merchant_id + ssl_pin + ssl_user_id + ssl_amount + ); + $required{CC_CCSALE} = [ @alwaysrequired, qw( + ssl_card_number + ssl_exp_date + ssl_cvv2cvc2_indicator + ), + ]; $required{CC_CCCREDIT} = $required{CC_CCSALE}; + $required{ECHECK_ECSPURCHASE} = [ @alwaysrequired, + qw( + ssl_aba_number + ssl_bank_account_number + ssl_bank_account_type + ssl_agree + ), + ]; my %optional; - $optional{CC_CCSALE} = [ qw( ssl_user_id ssl_salestax ssl_cvv2cvc2 + # these are actually each sometimes required, depending on account type & settings, + # but we can let converge handle error messages for that + my @alwaysoptional = qw( + ssl_first_name + ssl_last_name + ssl_company + ssl_email + ); + $optional{CC_CCSALE} = [ @alwaysoptional, qw( ssl_salestax ssl_cvv2cvc2 ssl_description ssl_invoice_number - ssl_customer_code ssl_company ssl_first_name - ssl_last_name ssl_avs_address ssl_address2 + ssl_customer_code + ssl_avs_address ssl_address2 ssl_city ssl_state ssl_avs_zip ssl_country - ssl_phone ssl_email ssl_ship_to_company + ssl_phone ssl_ship_to_company ssl_ship_to_first_name ssl_ship_to_last_name ssl_ship_to_address1 ssl_ship_to_city ssl_ship_to_state ssl_ship_to_zip ssl_ship_to_country ) ]; $optional{CC_CCCREDIT} = $optional{CC_CCSALE}; + $optional{ECHECK_ECSPURCHASE} = [ @alwaysoptional ]; my $type_action = $self->transaction_type(). '_'. $content{ssl_transaction_type}; unless ( exists($required{$type_action}) ) { @@ -174,18 +329,26 @@ sub submit { return; } - my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} ); - my $zip = $content{'zip'}; - $zip =~ s/[^[:alnum:]]//g; + $self->_revmap_fields( + ssl_merchant_id => 'login', + ssl_pin => 'password', + ssl_amount => 'amount', + ssl_first_name => 'first_name', + ssl_last_name => 'last_name', + ssl_company => 'company', + ssl_email => 'email', + ); + + if (uc($self->transaction_type) eq 'CC') { - my $cvv2indicator = $content{"cvv2"} ? 1 : 9; # 1 = Present, 9 = Not Present + my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} ); + my $zip = $content{'zip'}; + $zip =~ s/[^[:alnum:]]//g; - $self->_revmap_fields( + my $cvv2indicator = $content{"cvv2"} ? 1 : 9; # 1 = Present, 9 = Not Present - ssl_merchant_id => 'login', - ssl_pin => 'password', + $self->_revmap_fields( - ssl_amount => 'amount', ssl_card_number => 'card_number', ssl_exp_date => \$expdate_mmyy, # MMYY from 'expiration' ssl_cvv2cvc2_indicator => \$cvv2indicator, @@ -194,16 +357,12 @@ sub submit { ssl_invoice_number => 'invoice_number', ssl_customer_code => 'customer_id', - ssl_first_name => 'first_name', - ssl_last_name => 'last_name', - ssl_company => 'company', ssl_avs_address => 'address', ssl_city => 'city', ssl_state => 'state', ssl_avs_zip => \$zip, # 'zip' with non-alnums removed ssl_country => 'country', ssl_phone => 'phone', - ssl_email => 'email', ssl_ship_to_first_name => 'ship_first_name', ssl_ship_to_last_name => 'ship_last_name', @@ -214,27 +373,52 @@ sub submit { ssl_ship_to_zip => 'ship_zip', ssl_ship_to_country => 'ship_country', - ); + ); + + } else { # ECHECK + my $account_type; + if (uc($content{'account_type'}) =~ 'PERSONAL') { + $account_type = 0; + } elsif (uc($content{'account_type'}) =~ 'BUSINESS') { + $account_type = 1; + } else { + $self->error_message("Unrecognized account type: ".$content{'account_type'}); + $self->is_success(0); + return; + } + + $self->_revmap_fields( + ssl_aba_number => 'routing_code', + ssl_bank_account_number => 'account_number', + ssl_bank_account_type => \$account_type, + ssl_agree => \'1', + ); + + } + + # set defaults for anything that hasn't been set yet + %content = $self->content; + foreach ( keys ( %{($self->{_defaults})} ) ) { + $content{$_} ||= $self->{_defaults}->{$_}; + } + $self->content(%content); + + # truncate long rows & validate required fields my %params = $self->get_fields( @{$required{$type_action}}, @{$optional{$type_action}}, ); - $params{$_} = substr($params{$_},0,$maxlength{$_}) foreach grep exists($maxlength{$_}), keys %params; + $self->required_fields(@{$required{$type_action}}); - foreach ( keys ( %{($self->{_defaults})} ) ) { - $params{$_} = $self->{_defaults}->{$_} unless exists($params{$_}); - } - + # some final non-overridable parameters $params{ssl_test_mode}='true' if $self->test_transaction; - $params{ssl_show_form}='false'; $params{ssl_result_format}='ASCII'; - - $self->required_fields(@{$required{$type_action}}); - warn join("\n", map{ "$_ => $params{$_}" } keys(%params)) if $self->debug > 1; + # send request + warn "POST PARAMETERS:\n" . join("\n", map{ "$_ => $params{$_}" } keys(%params)) if $self->debug; my ( $page, $resp, %resp_headers ) = $self->https_post( %params ); @@ -242,15 +426,17 @@ sub submit { $self->response_page( $page ); $self->response_headers( \%resp_headers ); - warn "$page\n" if $self->debug > 1; + warn "RESPONSE FROM SERVER:\n$page\n" if $self->debug; # $page should contain key/value pairs my $status =''; my %results = map { s/\s*$//; split '=', $_, 2 } grep { /=/ } split '^', $page; - # AVS and CVS values may be set on success or failure - $self->avs_code( $results{ssl_avs_response} ); - $self->cvv2_response( $results{ ssl_cvv2_response } ); + if (uc($self->transaction_type) eq 'CC') { + # AVS and CVS values may be set on success or failure + $self->avs_code( $results{ssl_avs_response} ); + $self->cvv2_response( $results{ ssl_cvv2_response } ); + } $self->result_code( $status = $results{ errorCode } || $results{ ssl_result } ); $self->order_number( $results{ ssl_txn_id } ); $self->authorization( $results{ ssl_approval_code } ); @@ -269,21 +455,20 @@ __END__ =head1 SEE ALSO -Business::OnlinePayment, Business::OnlinePayment::viaKlix, Elavon Virtual Merchant Developers' Guide - -=head1 AUTHOR - -Richard Siddall, Eelavon@elirion.netE +L, L, Elavon Converge Developers' Guide =head1 BUGS Duplicates code to handle deprecated 'type' codes. -Method for passing raw card track data is not documented by Elavon. +Only provides a small selection of possible transaction types. =head1 COPYRIGHT AND LICENSE -Copyright (C) 2009 by Richard Siddall. This module is largely based on Business::OnlinePayment::viaKlix by Jeff Finucane. +Copyright (C) 2016 Freeside Internet Services. + +Based on the original ElavonVirtualMerchant module by Richard Siddall, +which was largely based on Business::OnlinePayment::viaKlix by Jeff Finucane. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or,