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.03'; # 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 Cf AccountNumber ExpirationMonth ExpirationYear Cvv CardHolderFirstName CardHolderLastName AvsZip AvsStreet IndustryType ApplicationId Recurring /], }, 'void' => { path => '/vsg2/processvoid', process => 'ProcessVoid', fields => [qw( Amount AccountNumber ExpirationMonth ExpirationYear ReferenceNumber TransactionDate IndustryType ApplicationId )], }, 'refund' => { path => '/vsg2/processrefund', process => 'ProcessRefund', fields => [qw( Amount AccountNumber ExpirationMonth ExpirationYear ApplicationId )], }, 'authorize' => { path => '/vsg2/processauth', }, 'authorize_cancel' => { path => '/vsg2/processauthcancel', }, '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, $self->build_subs(qw/ platform tid appid action reference_number cvv_response avs_response response_code risk_score txn_amount txn_date /); $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'); 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{'UDField1'}) ? $content{'UDField1'} : '', Cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '', 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{'ref_num'}) ? $content{'ref_num'} : '', Token => ($content{'token'}) ? $content{'token'} : '', Receipt => ($content{'receipt'}) ? $content{'receipt'} : '', TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : '' } # 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 $process_action = $action; $process_action =~ s/\b([a-z])/\u$1/g; $process_action = 'Process'.$process_action; 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->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 $response_code = $self->response_code() || ''; if ($response_code) { $self->error_message(qq|Error code ${response_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->result_code($response->{Status}); # 0 /1 $self->response_code($response->{ResponseCode}); # see documentation for translation $self->avs_response($response->{AvsResponse}); # Y / N $self->cvv_response($response->{CvvResponse}); # P / F $self->txn_date($response->{TransactionDate}); # MMDDhhmmss $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100 $self->reference_number($response->{ReferenceNumber}); $self->is_success($self->result_code() eq '0' ? 1 : 0); if ($self->is_success()) { $self->authorization($response->{ReferenceNumber}); } # fill in error_message if there is is an error if ( !$self->is_success && exists($response->{AdditionalResponseData})) { $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{AdditionalResponseData}); }elsif ( !$self->is_success && exists($response->{Receipt}) ) { $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : ''); } } else { die 'Error communicating with vSecureProcessing server'; return; } } 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 => 'UDField1', Cf2 => 'UDField2', 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 50 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