1 package Business::OnlinePayment::vSecureProcessing;
4 use vars qw($VERSION $DEBUG @ISA);
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
12 @ISA = qw(Business::OnlinePayment::HTTPS);
16 # mapping out all possible endpoints
17 # but this version will only be building out "charge", "void", & "credit"
18 my %payment_actions = (
20 path => '/vsg2/processpayment',
21 process => 'ProcessPayment',
23 Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf AccountNumber
24 ExpirationMonth ExpirationYear Cvv
25 CardHolderFirstName CardHolderLastName AvsZip AvsStreet
26 IndustryType ApplicationId Recurring
30 path => '/vsg2/processvoid',
31 process => 'ProcessVoid',
33 Amount AccountNumber ExpirationMonth ExpirationYear ReferenceNumber
34 TransactionDate IndustryType ApplicationId
38 path => '/vsg2/processrefund',
39 process => 'ProcessRefund',
41 Amount AccountNumber ExpirationMonth ExpirationYear ApplicationId
45 path => '/vsg2/processauth',
47 'authorize_cancel' => {
48 path => '/vsg2/processauthcancel',
51 path => '/vsg2/processcaptureonly',
54 path => '/vsg2/createtoken',
57 path => '/vsg2/deletetoken',
60 path => '/vsg2/querytoken',
62 'update_exp_date' => {
63 path => '/vsg2/updateexpiration',
66 path => '/vsg2/updatetoken',
71 my %action_mapping = (
72 'normal authorization' => 'charge',
74 'authorization only' => 'authorize',
75 'post authorization' => 'capture',
76 'reverse authorization' => 'authorize_cancel'
84 # inistialize standard B::OP attributes
86 $self->$_( '' ) for qw/authorization
93 # B::OP creates the following accessors:
94 # server, path, test_transaction, transaction_type,
95 # server_response, is_success, authorization,
96 # result_code, error_message,
100 action reference_number cvv2_response avs_code response_code
101 risk_score txn_amount txn_date
104 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
106 $self->server('svr1.vsecureprocessing.com');
108 $self->tid($options{'tid'});
110 $self->platform($options{'platform'});
112 $self->appid($options{'appid'});
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}) =
138 split /\//, $content{expiration};
139 $content{exp_month} = sprintf "%02d", $content{exp_month};
140 $content{exp_year} = substr($content{exp_year},0,2)
141 if ($content{exp_year} > 99);
144 if ( !$content{'first_name'}
145 || !$content{'last_name'} && $content{'name'}
148 ($content{'first_name'}, $content{'last_name'}) =
149 split(' ', $content{'name'}, 2);
152 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
153 $content{'street_number'} = $1;
156 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
157 $self->content(%content);
160 sub process_content {
162 $self->clean_content();
163 my %content = $self->content();
164 $self->action( ($action_mapping{lc $content{'action'}})
165 ? $action_mapping{lc $content{'action'}}
166 : lc $content{'action'}
168 $self->path($payment_actions{ $self->action }{path})
169 unless length($self->path);
170 $self->appid($content{appid}) if (!$self->appid && $content{appid});
176 # inistialize standard B::OP attributes
177 $self->is_success(0);
178 $self->$_( '' ) for qw/authorization
183 # clean and process the $self->content info
184 $self->process_content();
185 my %content = $self->content;
186 my $action = $self->action();
188 if ( $self->test_transaction ) {
189 $self->server('dvrotsos2.kattare.com');
192 my @acceptable_actions = ('charge', 'refund', 'void');
194 unless ( grep { $action eq $_ } @acceptable_actions ) {
195 croak "'$action' is not supported at this time.";
198 # fill in the xml vars
201 Platform => $self->platform,
202 UserId => $content{'login'},
203 GID => $content{'password'},
204 Tid => $self->tid || '01',
208 Amount => $content{'amount'},
209 Trk1 => ($content{'track1'}) ? $content{'track1'} : '',
210 Trk2 => ($content{'track2'}) ? $content{'track2'} : '',
211 TypeOfSale => ($content{'description'}) ? $content{'description'} : '',
212 Cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
213 Cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
215 AccountNumber => ($content{'card_number'}) ? $content{'card_number'} : '',
216 ExpirationMonth => $content{'exp_month'},
217 ExpirationYear => $content{'exp_year'},
218 Cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
219 CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '',
220 CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '',
221 AvsZip => ($content{'zip'}) ? $content{'zip'} : '',
222 AvsStreet => ($content{'street_number'}) ? $content{'street_number'} : '',
224 # IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
225 # IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
227 ApplicationId => $self->appid(),
228 Recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
229 ReferenceNumber => ($content{'ref_num'}) ? $content{'ref_num'} : '',
230 Token => ($content{'token'}) ? $content{'token'} : '',
231 Receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
232 TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : ''
234 # we won't be using level2 nor level3. So I'm leaving them out for now.
237 # create the list of required fields based on the action
238 my @required_fields = qw/ Amount /;
239 if ($action eq 'charge') {
240 push @required_fields, $_
241 foreach (qw/ AccountNumber ExpirationMonth ExpirationYear /);
242 }elsif ($action eq 'void') {
243 push @required_fields, $_
244 foreach (qw/ ReferenceNumber /);
245 }elsif ($action eq 'refund') {
246 push @required_fields, $_
247 foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /);
250 # check the requirements are met.
252 foreach my $field (@required_fields) {
253 push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
255 if (scalar(@missing_fields)) {
256 croak "Missing required fields: ".join(', ', @missing_fields);
259 my $process_action = $action;
260 $process_action =~ s/\b([a-z])/\u$1/g;
261 $process_action = 'Process'.$process_action;
263 my $writer = new XML::Writer( OUTPUT => \$xml_data,
269 $writer->startTag('Request');
270 $writer->startTag('MerchantData');
271 foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
272 $writer->dataElement( $key, $xml_vars->{auth}{$key} );
274 $writer->endTag('MerchantData');
275 $writer->startTag($payment_actions{ $self->action }{process});
276 foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) {
277 next if (!$xml_vars->{payment}{$key});
278 if (ref $xml_vars->{payment}{$key} eq '') {
279 $writer->dataElement( $key, $xml_vars->{payment}{$key});
281 $writer->startTag($key);
282 foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
283 $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} );
285 $writer->endTag($key);
288 $writer->endTag($payment_actions{ $self->action }{process});
289 $writer->endTag('Request');
292 warn "XML:\n$xml_data\n" if $DEBUG > 2;
294 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
295 # opts for B:OP:HTTPS::https_post
296 my $opts = { headers => {}};
297 $opts->{'Content-Type'} =
298 $opts->{headers}->{'Content-Type'} =
299 "multipart/form-data, boundary=$boundary";
303 "Content-Disposition: form-data; name=\"param\"\n\n".
307 # conform to RFC standards
308 $content =~ s/\n/\r\n/gs;
310 my ( $page, $server_response, %headers ) =
311 $self->https_post( $opts, $content );
313 # store the server response.
314 $self->server_response($server_response);
315 # parse the result page.
316 $self->parse_response($page);
318 if (!$self->is_success() && !$self->error_message() ) {
320 #additional logging information, possibly too sensitive for an error
321 $self->error_message(
322 "(HTTPS response: ".$server_response.") ".
324 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
325 "(Raw HTTPS content: ".$page.")"
328 my $response_code = $self->response_code() || '';
329 if ($response_code) {
330 $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
332 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
339 # read $self->server_response and decipher any errors
344 if ($self->server_response =~ /^200/) {
345 my $response = XMLin($page);
346 warn "Response:\n".Dumper($response)."\n" if $DEBUG > 2;
347 $self->result_code($response->{Status}); # 0 /1
348 $self->response_code($response->{ResponseCode}); # see documentation for translation
349 $self->avs_code($response->{AvsResponse}); # Y / N
351 #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress
352 $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/
353 ? $response->{CvvResponse}
357 $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
358 $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
359 $self->reference_number($response->{ReferenceNumber});
361 $self->is_success($self->result_code() eq '0' ? 1 : 0);
362 if ($self->is_success()) {
363 $self->authorization($response->{ReferenceNumber});
364 } else { # fill in error_message if there is is an error
365 $self->error_message( 'Error '.$response->{ResponseCode}.': '.
366 ( $response->{AdditionalResponseData}
367 || $response->{Receipt}
368 || $response->{ResultCode}
374 die 'Error communicating with vSecureProcessing server (server sent response: '. $self->server_response. ')';
385 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
389 use Business::OnlinePayment;
390 my %processor_info = (
391 platform => 'vsecure_platform',
392 appid => 'vsecure_appid',
393 tid => '54', #optional, defaults to 01
396 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
398 login => 'vsecure@user.id',
399 password => '12345678901234567890', #vsecure gid
402 action => 'Normal Authorization',
403 description => 'Business::OnlinePayment test',
405 customer_id => 'tfb',
406 name => 'Tofu Beast',
407 address => '123 Anystreet',
411 card_number => '4007000000027',
412 expiration => '09/02',
413 cvv2 => '1234', #optional
417 if($tx->is_success()) {
418 print "Card processed successfully: ".$tx->authorization."\n";
420 print "Card was rejected: ".$tx->error_message."\n";
425 For detailed information see L<Business::OnlinePayment>.
427 =head1 METHODS AND FUNCTIONS
429 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
433 Returns the response error code.
437 Returns the response error description text.
439 =head2 server_response
441 Returns the complete response from the server.
443 =head1 Handling of content(%content) data:
447 The following actions are valid
453 =head1 Setting vSecureProcessing parameters from content(%content)
455 The following rules are applied to map data to vSecureProcessing parameters
456 from content(%content):
458 # param => $content{<key>}
459 AccountNumber => 'card_number',
461 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
462 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
465 CardHolderFirstName => 'first_name',
466 CardHolderLastName => 'last_name',
468 AvsStreet => 'address',
472 IndustryType => 'IndustryInfo',
478 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
479 Version: 140901 (September 1, 2014).
481 See http://www.vsecureprocessing.com/ for more information.
485 Original author: Alex Brelsfoard
487 Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>
491 Copyright (c) 2015 Freeside Internet Services, Inc.
495 This program is free software; you can redistribute it and/or modify it under
496 the same terms as Perl itself.
500 Need a complete, open-source back-office and customer self-service solution?
501 The Freeside software includes support for credit card and electronic check
502 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
503 trouble ticketing, and customer signup and self-service web interfaces.
505 http://freeside.biz/freeside/
509 perl(1). L<Business::OnlinePayment>.