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
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',
zip => '84058',
card_number => '4007000000027',
expiration => '09/02',
- cvv2 => '1234', #optional
+ cvv2 => '1234',
);
$tx->submit();
=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
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
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
$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}) ) {
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,
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',
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 );
$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 } );
=head1 SEE ALSO
-Business::OnlinePayment, Business::OnlinePayment::viaKlix, Elavon Virtual Merchant Developers' Guide
-
-=head1 AUTHOR
-
-Richard Siddall, E<lt>elavon@elirion.netE<gt>
+L<Business::OnlinePayment>, L<Business::OnlinePayment::HTTPS>, 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,