package Business::OnlinePayment::vSecureProcessing; use strict; use vars qw($VERSION $DEBUG @ISA); use Carp; use XML::Writer; use XML::Simple; use Data::Dumper; use Business::OnlinePayment; use Business::OnlinePayment::HTTPS; @ISA = qw(Business::OnlinePayment::HTTPS); $DEBUG = 0; $VERSION = '0.10'; sub _info { return { 'info_compat' => '0.01', 'gateway_name' => 'vSecure Processing', 'gateway_url' => 'http://www.vsecureprocessing.com/', 'module_version' => $VERSION, 'supported_types' => [qw( CC )], 'token_support' => 0, 'test_transaction' => 1, 'partial_auth' => 1, 'supported_actions' => [ 'Normal Authorization', 'Authorization Only', #'Post Authorization', 'Reverse Authorization', 'Void', 'Credit', ], }; } # mapping out all possible endpoints # but this version will only be building out "charge", "void", & "credit" my %payment_actions = ( 'charge' => { path => '/vsg2/processpayment', process => 'ProcessPayment', fields => [qw/ Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf3 AccountNumber ExpirationMonth ExpirationYear Cvv CardHolderFirstName CardHolderLastName AvsZip AvsStreet IndustryType ApplicationId Recurring /], }, 'void' => { path => '/vsg2/processvoid', process => 'ProcessVoid', fields => [qw( Amount Cf1 Cf2 Cf3 AccountNumber ExpirationMonth ExpirationYear ReferenceNumber TransactionDate IndustryType ApplicationId )], }, 'refund' => { path => '/vsg2/processrefund', process => 'ProcessRefund', fields => [qw( Amount Cf1 Cf2 Cf3 AccountNumber ExpirationMonth ExpirationYear ApplicationId )], }, 'authorize' => { path => '/vsg2/processauth', process => 'ProcessAuth', fields => [qw( Amount TypeOfSale Cf1 Cf2 Cf3 AccountNumber ExpirationMonth ExpirationYear Cvv CardHolderFirstName CardHolderLastName AvsZip AvsStreet IndustryType ApplicationId )], }, 'authorize_cancel' => { path => '/vsg2/processauthcancel', process => 'ProcessAuthCancel', fields => [qw( Amount AccountNumber ExpirationMonth ExpirationYear ReferenceNumber ResponseCode TransactionDate IndustryType ApplicationId )], }, 'capture' => { path => '/vsg2/processcaptureonly', }, 'create_token' => { path => '/vsg2/createtoken', }, 'delete_token' => { path => '/vsg2/deletetoken', }, 'query_token' => { path => '/vsg2/querytoken', }, 'update_exp_date' => { path => '/vsg2/updateexpiration', }, 'update_token' => { path => '/vsg2/updatetoken', }, ); my %action_mapping = ( 'normal authorization' => 'charge', 'credit' => 'refund', 'authorization only' => 'authorize', 'post authorization' => 'capture', 'reverse authorization' => 'authorize_cancel' # void => void ); sub set_defaults { my $self = shift; my %options = @_; # inistialize standard B::OP attributes $self->is_success(0); $self->$_( '' ) for qw/authorization result_code error_message server path server_response/; # B::OP creates the following accessors: # server, path, test_transaction, transaction_type, # server_response, is_success, authorization, # result_code, error_message, response_code $self->build_subs( qw( platform tid appid action reference_number cvv2_response avs_code risk_score txn_amount txn_date partial_auth_amount ) ); $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG; $self->server('svr1.vsecureprocessing.com'); $self->tid($options{'tid'}); $self->platform($options{'platform'}); $self->appid($options{'appid'}); $self->port(443); } sub clean_content { my ($self,$content) = @_; my %content = $self->content(); { no warnings 'uninitialized'; # strip non-digits from card number my $card_number = ''; if ( $content{card_number} ) { $content{card_number} =~ s/\D//g; } if ($content{'description'} && length($content{'description'}) >20) { $content{'description'} = substr($content{'description'},0,20); } # separate month and year values for expiry_date if ( $content{expiration} ) { ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration}; $content{exp_month} = sprintf "%02d", $content{exp_month}; $content{exp_year} = substr($content{exp_year},0,2) if ($content{exp_year} > 99); } if ( !$content{'first_name'} || !$content{'last_name'} && $content{'name'} ) { ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2); } if ($content{'address'} =~ m/[\D ]*(\d+)\D/) { $content{'street_number'} = $1; } } warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2); $self->content(%content); } sub process_content { my $self = shift; $self->clean_content(); my %content = $self->content(); $self->action( ($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'} ); $self->path($payment_actions{ $self->action }{path}) unless length($self->path); $self->appid($content{appid}) if (!$self->appid && $content{appid}); } sub submit { my $self = shift; # inistialize standard B::OP attributes $self->is_success(0); $self->$_( '' ) for qw/authorization result_code error_message server_response/; # clean and process the $self->content info $self->process_content(); my %content = $self->content; my $action = $self->action(); if ( $self->test_transaction ) { $self->server('dvrotsos2.kattare.com'); } my @acceptable_actions = ('charge', 'refund', 'void', 'authorize', 'authorize_cancel'); unless ( grep { $action eq $_ } @acceptable_actions ) { croak "'$action' is not supported at this time."; } # fill in the xml vars my $xml_vars = { auth => { Platform => $self->platform, UserId => $content{'login'}, GID => $content{'password'}, Tid => $self->tid || '01', }, payment => { Amount => $content{'amount'}, Trk1 => ($content{'track1'}) ? $content{'track1'} : '', Trk2 => ($content{'track2'}) ? $content{'track2'} : '', TypeOfSale => ($content{'description'}) ? $content{'description'} : '', Cf1 => ($content{'invoice_number'}) ? $content{'invoice_number'} : '', Cf2 => ($content{'customer_id'}) ? $content{'customer_id'} : '', Cf3 => '', AccountNumber => ($content{'card_number'}) ? $content{'card_number'} : '', ExpirationMonth => $content{'exp_month'}, ExpirationYear => $content{'exp_year'}, Cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '', CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '', CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '', AvsZip => ($content{'zip'}) ? $content{'zip'} : '', AvsStreet => ($content{'street_number'}) ? $content{'street_number'} : '', # IndustryType => { # IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '', # IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : '' # }, ApplicationId => $self->appid(), Recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0, ReferenceNumber => ($content{'authorization'}) ? $content{'authorization'} : '', Token => ($content{'token'}) ? $content{'token'} : '', Receipt => ($content{'receipt'}) ? $content{'receipt'} : '', TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : '', # note that B::OP response_code is the http response & message, ie '500 Internal Server Error' # whereas ResponseCode is a 2-digit ISO code indicating result of transaction ResponseCode => ($content{'result_code'}) ? $content{'result_code'} : '', } # we won't be using level2 nor level3. So I'm leaving them out for now. }; # create the list of required fields based on the action my @required_fields = qw/ Amount /; if ($action eq 'charge') { push @required_fields, $_ foreach (qw/ AccountNumber ExpirationMonth ExpirationYear /); }elsif ($action eq 'void') { push @required_fields, $_ foreach (qw/ ReferenceNumber /); }elsif ($action eq 'refund') { push @required_fields, $_ foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /); } # check the requirements are met. my @missing_fields; foreach my $field (@required_fields) { push(@missing_fields, $field) if (!$xml_vars->{payment}{$field}); } if (scalar(@missing_fields)) { croak "Missing required fields: ".join(', ', @missing_fields); } my $xml_data; my $writer = new XML::Writer( OUTPUT => \$xml_data, DATA_MODE => 0, DATA_INDENT => 0, ENCODING => 'utf-8', ); $writer->xmlDecl(); $writer->startTag('Request'); $writer->startTag('MerchantData'); foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) { $writer->dataElement( $key, $xml_vars->{auth}{$key} ); } $writer->endTag('MerchantData'); $writer->startTag($payment_actions{ $self->action }{process}); foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) { next if (!$xml_vars->{payment}{$key}); if (ref $xml_vars->{payment}{$key} eq '') { $writer->dataElement( $key, $xml_vars->{payment}{$key}); }else { $writer->startTag($key); foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) { $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} ); } $writer->endTag($key); } } $writer->endTag($payment_actions{ $self->action }{process}); $writer->endTag('Request'); $writer->end(); warn "XML:\n$xml_data\n" if $DEBUG > 2; my $boundary = sprintf('FormBoundary%06d', int(rand(1000000))); # opts for B:OP:HTTPS::https_post my $opts = { headers => {}}; $opts->{'Content-Type'} = $opts->{headers}->{'Content-Type'} = "multipart/form-data, boundary=$boundary"; my $content = "--$boundary\n". "Content-Disposition: form-data; name=\"param\"\n\n". $xml_data."\n". "--$boundary--\n"; # conform to RFC standards $content =~ s/\n/\r\n/gs; my ( $page, $server_response, %headers ) = $self->https_post( $opts, $content ); # store the server response. $self->server_response($server_response); # parse the result page. $self->parse_response($page); if ( $self->is_success && $self->result_code == 10 ) { #partial auth if ( $content{'partial_auth'} ) { $self->partial_auth_amount( $self->txn_amount ); } else { #XXX reverse auth if i was an auth only... my $void = new Business::OnlinePayment( 'vSecureProcessing', map { $_ -> $self->$_() } qw( platform appid tid ) ); $void->content( 'action' => 'Void', 'amount' => $self->txn_amount, 'test_transaction' => $self->test_transaction, 'authorization' => $self->authorization, map { $_ => $content{$_} } qw( login password card_number expiration ) ); $void->submit; if ( !$void->is_success ) { #XXX now what??? at least this is better than return is_success 0 or 1 die "Couldn't void partial auth"; } else { $self->is_success(0); } } } if (!$self->is_success() && !$self->error_message() ) { if ( $DEBUG ) { #additional logging information, possibly too sensitive for an error $self->error_message( "(HTTPS response: ".$server_response.") ". "(HTTPS headers: ". join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ". "(Raw HTTPS content: ".$page.")" ); } else { my $result_code = $self->result_code() || ''; if ($result_code) { $self->error_message(qq|Error code ${result_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|); }else { $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)'); } } } } # read $self->server_response and decipher any errors sub parse_response { my $self = shift; my $page = shift; if ($self->server_response =~ /^200/) { my $response = XMLin($page); warn "Response:\n".Dumper($response)."\n" if $DEBUG > 2; $self->is_success($response->{Status} ? 0 : 1); $self->result_code($response->{ResponseCode}); # NOT response_code, which is http response $self->avs_code($response->{AvsResponse}); # Y / N #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/ ? $response->{CvvResponse} : '' ); $self->txn_date($response->{TransactionDate}); # MMDDhhmmss $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100 $self->reference_number($response->{ReferenceNumber}); # overlap with authorization, # not used by anything or documented??? if ($self->is_success()) { $self->authorization($response->{ReferenceNumber}); } else { # fill in error_message if there is is an error $self->error_message( 'Error '.$response->{ResponseCode}.': '. ( $response->{AdditionalResponseData} || $response->{Receipt} || $response->{ResultCode} ) ); } } else { die 'Error communicating with vSecureProcessing server (server sent response: '. $self->server_response. ')'; } } 1; __END__ =head1 NAME Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment =head1 SYNOPSIS use Business::OnlinePayment; my %processor_info = ( platform => 'vsecure_platform', appid => 'vsecure_appid', tid => '54', #optional, defaults to 01 ); my $tx = new Business::OnlinePayment( "vSecureProcessing", %processor_info); $tx->content( login => 'vsecure@user.id', password => '12345678901234567890', #vsecure gid type => 'CC', action => 'Normal Authorization', description => 'Business::OnlinePayment test', amount => '49.95', customer_id => 'tfb', name => 'Tofu Beast', address => '123 Anystreet', city => 'Anywhere', state => 'UT', zip => '84058', card_number => '4007000000027', expiration => '09/02', cvv2 => '1234', #optional ); $tx->submit(); if($tx->is_success()) { print "Card processed successfully: ".$tx->authorization."\n"; } else { print "Card was rejected: ".$tx->error_message."\n"; } =head1 DESCRIPTION For detailed information see L. =head1 METHODS AND FUNCTIONS See L for the complete list. The following methods either override the methods in L or provide additional functions. =head2 result_code Returns the response error code. =head2 error_message Returns the response error description text. =head2 server_response Returns the complete response from the server. =head1 Handling of content(%content) data: =head2 action The following actions are valid normal authorization credit void =head1 Setting vSecureProcessing parameters from content(%content) The following rules are applied to map data to vSecureProcessing parameters from content(%content): # param => $content{} AccountNumber => 'card_number', Cvv => 'cvv2', ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration' ExpirationYear => \( $year ), # YY from MM/YY of 'expiration' Trk1 => 'track1', Trk2 => 'track2', CardHolderFirstName => 'first_name', CardHolderLastName => 'last_name', Amount => 'amount' AvsStreet => 'address', AvsZip => 'zip', Cf1 => 'invoice_number', Cf2 => 'customer_id', IndustryType => 'IndustryInfo', =head1 NOTE =head1 COMPATIBILITY Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014). See http://www.vsecureprocessing.com/ for more information. =head1 AUTHORS Original author: Alex Brelsfoard Current maintainer: Ivan Kohler =head1 COPYRIGHT Copyright (c) 2015 Freeside Internet Services, Inc. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 ADVERTISEMENT Need a complete, open-source back-office and customer self-service solution? The Freeside software includes support for credit card and electronic check processing with vSecureProcessing and over 60 other gateways, invoicing, integrated trouble ticketing, and customer signup and self-service web interfaces. http://freeside.biz/freeside/ =head1 SEE ALSO perl(1). L. =cut