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 if ($content{'description'} && length($content{'description'}) >20) {
132 $content{'description'} = substr($content{'description'},0,20);
135 # separate month and year values for expiry_date
136 if ( $content{expiration} ) {
137 ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration};
138 $content{exp_month} = sprintf "%02d", $content{exp_month};
139 $content{exp_year} = substr($content{exp_year},0,2) if ($content{exp_year} > 99);
142 if (!$content{'first_name'} || !$content{'last_name'} && $content{'name'}) {
143 ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2);
146 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
147 $content{'street_number'} = $1;
150 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
151 $self->content(%content);
154 sub process_content {
156 $self->clean_content();
157 my %content = $self->content();
158 $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
159 $self->path($payment_actions{ $self->action }{path})
160 unless length($self->path);
161 $self->appid($content{appid}) if (!$self->appid && $content{appid});
167 # inistialize standard B::OP attributes
168 $self->is_success(0);
169 $self->$_( '' ) for qw/authorization
174 # clean and process the $self->content info
175 $self->process_content();
176 my %content = $self->content;
177 my $action = $self->action();
179 my @acceptable_actions = ('charge', 'refund', 'void');
181 unless ( grep { $action eq $_ } @acceptable_actions ) {
182 croak "'$action' is not supported at this time.";
185 # fill in the xml vars
188 Platform => $self->platform,
189 UserId => $self->userid,
195 Amount => $content{'amount'},
196 Trk1 => ($content{'track1'}) ? $content{'track1'} : '',
197 Trk2 => ($content{'track2'}) ? $content{'track2'} : '',
198 TypeOfSale => ($content{'description'}) ? $content{'description'} : '',
199 Cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
200 Cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
202 AccountNumber => ($content{'card_number'}) ? $content{'card_number'} : '',
203 ExpirationMonth => $content{'exp_month'},
204 ExpirationYear => $content{'exp_year'},
205 Cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
206 CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '',
207 CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '',
208 AvsZip => ($content{'zip'}) ? $content{'zip'} : '',
209 AvsStreet => ($content{'street_number'}) ? $content{'street_number'} : '',
211 # IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
212 # IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
214 ApplicationId => $self->appid(),
215 Recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
216 ReferenceNumber => ($content{'ref_num'}) ? $content{'ref_num'} : '',
217 Token => ($content{'token'}) ? $content{'token'} : '',
218 Receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
219 TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : ''
221 # we won't be using level2 nor level3. So I'm leaving them out for now.
224 # create the list of required fields based on the action
225 my @required_fields = qw/ Amount /;
226 if ($action eq 'charge') {
227 push(@required_fields, $_) foreach (qw/ AccountNumber Cvv ExpirationMonth ExpirationYear /);
228 }elsif ($action eq 'void') {
229 push(@required_fields, $_) foreach (qw/ ReferenceNumber /);
230 }elsif ($action eq 'refund') {
231 push(@required_fields, $_) foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /);
234 # check the requirements are met.
236 foreach my $field (@required_fields) {
237 push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
239 if (scalar(@missing_fields)) {
240 croak "Missing required fields: ".join(', ', @missing_fields);
243 my $process_action = $action;
244 $process_action =~ s/\b([a-z])/\u$1/g;
245 $process_action = 'Process'.$process_action;
247 my $writer = new XML::Writer( OUTPUT => \$xml_data,
253 $writer->startTag('Request');
254 $writer->startTag('MerchantData');
255 foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
256 $writer->dataElement( $key, $xml_vars->{auth}{$key} );
258 $writer->endTag('MerchantData');
259 $writer->startTag($payment_actions{ $self->action }{process});
260 foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) {
261 next if (!$xml_vars->{payment}{$key});
262 if (ref $xml_vars->{payment}{$key} eq '') {
263 $writer->dataElement( $key, $xml_vars->{payment}{$key});
265 $writer->startTag($key);
266 foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
267 $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} );
269 $writer->endTag($key);
272 $writer->endTag($payment_actions{ $self->action }{process});
273 $writer->endTag('Request');
276 warn "XML:\n$xml_data\n" if $DEBUG > 2;
278 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
279 # opts for B:OP:HTTPS::https_post
280 my $opts = { headers => {}};
281 $opts->{'Content-Type'} =
282 $opts->{headers}->{'Content-Type'} =
283 "multipart/form-data, boundary=$boundary";
287 "Content-Disposition: form-data; name=\"param\"\n\n".
291 # conform to RFC standards
292 $content =~ s/\n/\r\n/gs;
294 my ( $page, $server_response, %headers ) = $self->https_post( $opts, $content );
296 # store the server response.
297 $self->server_response($server_response);
298 # parse the result page.
299 $self->parse_response($page);
301 if (!$self->is_success() && !$self->error_message() ) {
303 #additional logging information, possibly too sensitive for an error msg
304 # (vSecureProcessing seems to have a failure mode where they return the full
305 # original request including card number)
306 $self->error_message(
307 "(HTTPS response: ".$server_response.") ".
309 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
310 "(Raw HTTPS content: ".$page.")"
313 my $response_code = $self->response_code() || '';
314 if ($response_code) {
315 $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
317 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
324 # read $self->server_response and decipher any errors
329 if ($self->server_response =~ /^200/) {
330 my $response = XMLin($page);
331 warn "Response:\n".Dumper($response)."\n" if $DEBUG > 2;
332 $self->result_code($response->{Status}); # 0 /1
333 $self->response_code($response->{ResponseCode}); # see documentation for translation
334 $self->avs_response($response->{AvsResponse}); # Y / N
335 $self->cvv_response($response->{CvvResponse}); # P / F
336 $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
337 $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
338 $self->reference_number($response->{ReferenceNumber});
340 $self->is_success($self->result_code() eq '0' ? 1 : 0);
341 if ($self->is_success()) {
342 $self->authorization($response->{ReferenceNumber});
344 # fill in error_message if there is is an error
345 if ( !$self->is_success && exists($response->{AdditionalResponseData})) {
346 $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{AdditionalResponseData});
347 }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
348 $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
352 $self->is_success(0);
353 $self->error_message('Error communicating with vSecureProcessing server');
365 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
369 use Business::OnlinePayment;
370 my %processor_info = (
372 gid => 12345678901234567890,
375 url => 'www.####.com'
378 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
382 action => 'Normal Authorization',
383 description => 'Business::OnlinePayment test',
385 customer_id => 'tfb',
386 name => 'Tofu Beast',
387 address => '123 Anystreet',
391 card_number => '4007000000027',
392 expiration => '09/02',
393 cvv2 => '1234', #optional
397 if($tx->is_success()) {
398 print "Card processed successfully: ".$tx->authorization."\n";
400 print "Card was rejected: ".$tx->error_message."\n";
405 For detailed information see L<Business::OnlinePayment>.
407 =head1 METHODS AND FUNCTIONS
409 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
413 Returns the response error code.
417 Returns the response error description text.
419 =head2 server_response
421 Returns the complete response from the server.
423 =head1 Handling of content(%content) data:
427 The following actions are valid
433 =head1 Setting vSecureProcessing parameters from content(%content)
435 The following rules are applied to map data to vSecureProcessing parameters
436 from content(%content):
438 # param => $content{<key>}
439 AccountNumber => 'card_number',
441 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
442 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
445 CardHolderFirstName => 'first_name',
446 CardHolderLastName => 'last_name',
448 AvsStreet => 'address',
452 IndustryType => 'IndustryInfo',
458 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
460 See http://www.vsecureprocessing.com/ for more information.
464 Original author: Alex Brelsfoard
466 Current maintainer: Alex Brelsfoard
470 Copyright (c) 2015 Freeside Internet Services, Inc.
474 This program is free software; you can redistribute it and/or modify it under
475 the same terms as Perl itself.
479 Need a complete, open-source back-office and customer self-service solution?
480 The Freeside software includes support for credit card and electronic check
481 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
482 trouble ticketing, and customer signup and self-service web interfaces.
484 http://freeside.biz/freeside/
488 perl(1). L<Business::OnlinePayment>.