1 package Business::OnlinePayment::vSecureProcessing;
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11 #use Net::SSLeay qw(post_http post_https make_headers make_form);
12 use vars qw($VERSION $DEBUG @ISA $me);
14 @ISA = qw(Business::OnlinePayment::HTTPS);
17 $me = 'Business::OnlinePayment::vSecureProcessing';
19 # mapping out all possible endpoints
20 # but this version will only be building out "charge", "void", & "credit"
21 my %payment_actions = (
23 path => '/vsg2/processpayment',
24 process => 'ProcessPayment',
25 fields => [qw/ Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf AccountNumber ExpirationMonth ExpirationYear Cvv CardHolderFirstName CardHolderLastName AvsZip AvsStreet IndustryType ApplicationId Recurring /]
28 path => '/vsg2/processvoid',
29 process => 'ProcessVoid',
30 fields => [qw( Amount AccountNumber ExpirationMonth ExpirationYear ReferenceNumber TransactionDate IndustryType ApplicationId )]
33 path => '/vsg2/processrefund',
34 process => 'ProcessRefund',
35 fields => [qw( Amount AccountNumber ExpirationMonth ExpirationYear ApplicationId )]
38 path => '/vsg2/processauth',
40 'authorize_cancel' => {
41 path => '/vsg2/processauthcancel',
44 path => '/vsg2/processcaptureonly',
47 path => '/vsg2/createtoken',
50 path => '/vsg2/deletetoken',
53 path => '/vsg2/querytoken',
55 'update_exp_date' => {
56 path => '/vsg2/updateexpiration',
59 path => '/vsg2/updatetoken',
64 my %action_mapping = (
65 'normal authorization' => 'charge',
67 'authorization only' => 'authorize',
68 'post authorization' => 'capture',
69 'reverse authorization' => 'authorize_cancel'
77 # inistialize standard B::OP attributes
79 $self->$_( '' ) for qw/authorization
87 # B::OP creates the following accessors:
88 # server, port, path, test_transaction, transaction_type,
89 # server_response, is_success, authorization,
90 # result_code, error_message,
93 env platform userid gid tid appid action reference_number cvv_response
94 avs_response risk_score txn_amount txn_date response_code
97 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
101 $self->server($options{'server'});
103 $self->gid($options{'gid'});
105 $self->tid($options{'tid'});
107 $self->platform($options{'platform'});
109 $self->appid($options{'appid'});
111 $self->env((defined($options{'env'})) ? $options{'env'} : 'live'); # 'live'/'test'
119 my ($self,$content) = @_;
120 my %content = $self->content();
123 no warnings 'uninitialized';
125 # strip non-digits from card number
126 my $card_number = '';
127 if ( $content{card_number} ) {
128 $content{card_number} =~ s/\D//g;
131 # separate month and year values for expiry_date
132 if ( $content{expiration} ) {
133 ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration};
134 $content{exp_month} = sprintf "%02d", $content{exp_month};
135 $content{exp_year} = substr($content{exp_year},0,2) if ($content{exp_year} > 99);
138 if (!$content{'first_name'} || !$content{'last_name'} && $content{'name'}) {
139 ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2);
142 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
143 $content{'street_number'} = $1;
146 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
147 $self->content(%content);
150 sub process_content {
152 $self->clean_content();
153 my %content = $self->content();
154 $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
155 $self->path($payment_actions{ $self->action }{path})
156 unless length($self->path);
157 $self->appid($content{appid}) if (!$self->appid && $content{appid});
163 # inistialize standard B::OP attributes
164 $self->is_success(0);
165 $self->$_( '' ) for qw/authorization
170 # clean and process the $self->content info
171 $self->process_content();
172 my %content = $self->content;
173 my $action = $self->action();
175 my @acceptable_actions = ('charge', 'refund', 'void');
177 unless ( grep { $action eq $_ } @acceptable_actions ) {
178 croak "'$action' is not supported at this time.";
181 # fill in the xml vars
184 Platform => $self->platform,
185 UserId => $self->userid,
191 Amount => $content{'amount'},
192 Trk1 => ($content{'track1'}) ? $content{'track1'} : '',
193 Trk2 => ($content{'track2'}) ? $content{'track2'} : '',
194 TypeOfSale => ($content{'description'}) ? $content{'description'} : '',
195 Cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
196 Cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
198 AccountNumber => ($content{'card_number'}) ? $content{'card_number'} : '',
199 ExpirationMonth => $content{'exp_month'},
200 ExpirationYear => $content{'exp_year'},
201 Cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
202 CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '',
203 CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '',
204 AvsZip => ($content{'zip'}) ? $content{'zip'} : '',
205 AvsStreet => ($content{'street_number'}) ? $content{'street_number'} : '',
207 IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
208 IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
210 ApplicationId => $self->appid(),
211 Recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
212 ReferenceNumber => ($content{'ref_num'}) ? $content{'ref_num'} : '',
213 Token => ($content{'token'}) ? $content{'token'} : '',
214 Receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
215 TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : ''
217 # we won't be using level2 nor level3. So I'm leaving them out for now.
220 # create the list of required fields based on the action
221 my @required_fields = qw/ Amount /;
222 if ($action eq 'charge') {
223 push(@required_fields, $_) foreach (qw/ AccountNumber Cvv ExpirationMonth ExpirationYear /);
224 }elsif ($action eq 'void') {
225 push(@required_fields, $_) foreach (qw/ ReferenceNumber TransactionDate /);
226 }elsif ($action eq 'refund') {
227 push(@required_fields, $_) foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /);
230 # check the requirements are met.
232 foreach my $field (@required_fields) {
233 push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
235 if (scalar(@missing_fields)) {
236 croak "Missing required fields: ".join(', ', @missing_fields);
239 my $process_action = $action;
240 $process_action =~ s/\b([a-z])/\u$1/g;
241 $process_action = 'Process'.$process_action;
243 my $writer = new XML::Writer( OUTPUT => \$xml_data,
249 $writer->startTag('Request');
250 $writer->startTag('MerchantData');
251 foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
252 $writer->dataElement( $key, $xml_vars->{auth}{$key} );
254 $writer->endTag('MerchantData');
255 $writer->startTag($payment_actions{ $self->action }{process});
256 foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) {
257 next if (!$xml_vars->{payment}{$key});
258 if (ref $xml_vars->{payment}{$key} eq '') {
259 $writer->dataElement( $key, $xml_vars->{payment}{$key});
261 $writer->startTag($key);
262 foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
263 $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} );
265 $writer->endTag($key);
268 $writer->endTag($payment_actions{ $self->action }{process});
269 $writer->endTag('Request');
272 warn "XML:\n$xml_data\n" if $DEBUG > 2;
274 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
275 # opts for B:OP:HTTPS::https_post
276 my $opts = { headers => {}};
277 $opts->{'Content-Type'} =
278 $opts->{headers}->{'Content-Type'} =
279 "multipart/form-data, boundary=$boundary";
283 "Content-Disposition: form-data; name=\"param\"\n\n".
287 # conform to RFC standards
288 $content =~ s/\n/\r\n/gs;
290 my ( $page, $server_response, %headers ) = $self->https_post( $opts, $content );
292 # store the server response.
293 $self->server_response($server_response);
294 # parse the result page.
295 $self->parse_response($page);
297 if (!$self->is_success() && !$self->error_message() ) {
299 #additional logging information, possibly too sensitive for an error msg
300 # (vSecureProcessing seems to have a failure mode where they return the full
301 # original request including card number)
302 $self->error_message(
303 "(HTTPS response: ".$server_response.") ".
305 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
306 "(Raw HTTPS content: ".$page.")"
309 my $response_code = $self->response_code() || '';
310 if ($response_code) {
311 $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
313 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
320 # read $self->server_response and decipher any errors
325 if ($self->server_response =~ /^200/) {
326 my $response = XMLin($page);
327 $self->result_code($response->{Status}); # 0 /1
328 $self->response_code($response->{ResponseCode}); # see documentation for translation
329 $self->avs_response($response->{AvsResponse}); # Y / N
330 $self->cvv_response($response->{CvvResponse}); # P / F
331 $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
332 $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
333 $self->reference_number($response->{ReferenceNumber});
335 $self->is_success($self->result_code() eq '0' ? 1 : 0);
336 if ($self->is_success()) {
337 $self->authorization($response->{AuthIdentificationResponse});
339 # fill in error_message if there is is an error
340 if ( !$self->is_success && exists($response->{AdditionalResponseData})) {
341 $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{AdditionalResponseData});
342 }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
343 $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
347 $self->is_success(0);
348 $self->error_message('Error communicating with vSecureProcessing server');
360 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
364 use Business::OnlinePayment;
365 my %processor_info = (
367 gid => 12345678901234567890,
370 url => 'www.####.com'
373 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
377 action => 'Normal Authorization',
378 description => 'Business::OnlinePayment test',
380 customer_id => 'tfb',
381 name => 'Tofu Beast',
382 address => '123 Anystreet',
386 card_number => '4007000000027',
387 expiration => '09/02',
388 cvv2 => '1234', #optional
392 if($tx->is_success()) {
393 print "Card processed successfully: ".$tx->authorization."\n";
395 print "Card was rejected: ".$tx->error_message."\n";
400 For detailed information see L<Business::OnlinePayment>.
402 =head1 METHODS AND FUNCTIONS
404 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
408 Returns the response error code.
412 Returns the response error description text.
414 =head2 server_response
416 Returns the complete response from the server.
418 =head1 Handling of content(%content) data:
422 The following actions are valid
428 =head1 Setting vSecureProcessing parameters from content(%content)
430 The following rules are applied to map data to vSecureProcessing parameters
431 from content(%content):
433 # param => $content{<key>}
434 AccountNumber => 'card_number',
436 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
437 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
440 CardHolderFirstName => 'first_name',
441 CardHolderLastName => 'last_name',
443 AvsStreet => 'address',
447 IndustryType => 'IndustryInfo',
453 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
455 See http://www.vsecureprocessing.com/ for more information.
459 Original author: Alex Brelsfoard
461 Current maintainer: Alex Brelsfoard
465 Copyright (c) 2015 Freeside Internet Services, Inc.
469 This program is free software; you can redistribute it and/or modify it under
470 the same terms as Perl itself.
474 Need a complete, open-source back-office and customer self-service solution?
475 The Freeside software includes support for credit card and electronic check
476 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
477 trouble ticketing, and customer signup and self-service web interfaces.
479 http://freeside.biz/freeside/
483 perl(1). L<Business::OnlinePayment>.