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);
17 'info_compat' => '0.01',
18 'gateway_name' => 'vSecure Processing',
19 'gateway_url' => 'http://www.vsecureprocessing.com/',
20 'module_version' => $VERSION,
21 'supported_types' => [qw( CC )],
23 'test_transaction' => 1,
25 'supported_actions' => [
26 'Normal Authorization',
27 #'Authorization Only',
28 #'Post Authorization',
29 'Reverse Authorization',
35 # mapping out all possible endpoints
36 # but this version will only be building out "charge", "void", & "credit"
37 my %payment_actions = (
39 path => '/vsg2/processpayment',
40 process => 'ProcessPayment',
42 Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf3 AccountNumber
43 ExpirationMonth ExpirationYear Cvv
44 CardHolderFirstName CardHolderLastName AvsZip AvsStreet
45 IndustryType ApplicationId Recurring
49 path => '/vsg2/processvoid',
50 process => 'ProcessVoid',
52 Amount Cf1 Cf2 Cf3 AccountNumber
53 ExpirationMonth ExpirationYear ReferenceNumber
54 TransactionDate IndustryType ApplicationId
58 path => '/vsg2/processrefund',
59 process => 'ProcessRefund',
61 Amount Cf1 Cf2 Cf3 AccountNumber
62 ExpirationMonth ExpirationYear ApplicationId
66 path => '/vsg2/processauth',
68 'authorize_cancel' => {
69 path => '/vsg2/processauthcancel',
72 path => '/vsg2/processcaptureonly',
75 path => '/vsg2/createtoken',
78 path => '/vsg2/deletetoken',
81 path => '/vsg2/querytoken',
83 'update_exp_date' => {
84 path => '/vsg2/updateexpiration',
87 path => '/vsg2/updatetoken',
92 my %action_mapping = (
93 'normal authorization' => 'charge',
95 'authorization only' => 'authorize',
96 'post authorization' => 'capture',
97 'reverse authorization' => 'authorize_cancel'
105 # inistialize standard B::OP attributes
106 $self->is_success(0);
107 $self->$_( '' ) for qw/authorization
114 # B::OP creates the following accessors:
115 # server, path, test_transaction, transaction_type,
116 # server_response, is_success, authorization,
117 # result_code, error_message,
119 $self->build_subs(qw/
121 action reference_number cvv2_response avs_code response_code
122 risk_score txn_amount txn_date partial_auth_amount
125 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
127 $self->server('svr1.vsecureprocessing.com');
129 $self->tid($options{'tid'});
131 $self->platform($options{'platform'});
133 $self->appid($options{'appid'});
140 my ($self,$content) = @_;
141 my %content = $self->content();
144 no warnings 'uninitialized';
146 # strip non-digits from card number
147 my $card_number = '';
148 if ( $content{card_number} ) {
149 $content{card_number} =~ s/\D//g;
152 if ($content{'description'} && length($content{'description'}) >20) {
153 $content{'description'} = substr($content{'description'},0,20);
156 # separate month and year values for expiry_date
157 if ( $content{expiration} ) {
158 ($content{exp_month}, $content{exp_year}) =
159 split /\//, $content{expiration};
160 $content{exp_month} = sprintf "%02d", $content{exp_month};
161 $content{exp_year} = substr($content{exp_year},0,2)
162 if ($content{exp_year} > 99);
165 if ( !$content{'first_name'}
166 || !$content{'last_name'} && $content{'name'}
169 ($content{'first_name'}, $content{'last_name'}) =
170 split(' ', $content{'name'}, 2);
173 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
174 $content{'street_number'} = $1;
177 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
178 $self->content(%content);
181 sub process_content {
183 $self->clean_content();
184 my %content = $self->content();
185 $self->action( ($action_mapping{lc $content{'action'}})
186 ? $action_mapping{lc $content{'action'}}
187 : lc $content{'action'}
189 $self->path($payment_actions{ $self->action }{path})
190 unless length($self->path);
191 $self->appid($content{appid}) if (!$self->appid && $content{appid});
197 # inistialize standard B::OP attributes
198 $self->is_success(0);
199 $self->$_( '' ) for qw/authorization
204 # clean and process the $self->content info
205 $self->process_content();
206 my %content = $self->content;
207 my $action = $self->action();
209 if ( $self->test_transaction ) {
210 $self->server('dvrotsos2.kattare.com');
213 my @acceptable_actions = ('charge', 'refund', 'void');
215 unless ( grep { $action eq $_ } @acceptable_actions ) {
216 croak "'$action' is not supported at this time.";
219 # fill in the xml vars
222 Platform => $self->platform,
223 UserId => $content{'login'},
224 GID => $content{'password'},
225 Tid => $self->tid || '01',
229 Amount => $content{'amount'},
230 Trk1 => ($content{'track1'}) ? $content{'track1'} : '',
231 Trk2 => ($content{'track2'}) ? $content{'track2'} : '',
232 TypeOfSale => ($content{'description'}) ? $content{'description'} : '',
233 Cf1 => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
234 Cf2 => ($content{'customer_id'}) ? $content{'customer_id'} : '',
236 AccountNumber => ($content{'card_number'}) ? $content{'card_number'} : '',
237 ExpirationMonth => $content{'exp_month'},
238 ExpirationYear => $content{'exp_year'},
239 Cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
240 CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '',
241 CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '',
242 AvsZip => ($content{'zip'}) ? $content{'zip'} : '',
243 AvsStreet => ($content{'street_number'}) ? $content{'street_number'} : '',
245 # IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
246 # IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
248 ApplicationId => $self->appid(),
249 Recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
250 ReferenceNumber => ($content{'ref_num'}) ? $content{'ref_num'} : '',
251 Token => ($content{'token'}) ? $content{'token'} : '',
252 Receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
253 TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : ''
255 # we won't be using level2 nor level3. So I'm leaving them out for now.
258 # create the list of required fields based on the action
259 my @required_fields = qw/ Amount /;
260 if ($action eq 'charge') {
261 push @required_fields, $_
262 foreach (qw/ AccountNumber ExpirationMonth ExpirationYear /);
263 }elsif ($action eq 'void') {
264 push @required_fields, $_
265 foreach (qw/ ReferenceNumber /);
266 }elsif ($action eq 'refund') {
267 push @required_fields, $_
268 foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /);
271 # check the requirements are met.
273 foreach my $field (@required_fields) {
274 push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
276 if (scalar(@missing_fields)) {
277 croak "Missing required fields: ".join(', ', @missing_fields);
280 my $process_action = $action;
281 $process_action =~ s/\b([a-z])/\u$1/g;
282 $process_action = 'Process'.$process_action;
284 my $writer = new XML::Writer( OUTPUT => \$xml_data,
290 $writer->startTag('Request');
291 $writer->startTag('MerchantData');
292 foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
293 $writer->dataElement( $key, $xml_vars->{auth}{$key} );
295 $writer->endTag('MerchantData');
296 $writer->startTag($payment_actions{ $self->action }{process});
297 foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) {
298 next if (!$xml_vars->{payment}{$key});
299 if (ref $xml_vars->{payment}{$key} eq '') {
300 $writer->dataElement( $key, $xml_vars->{payment}{$key});
302 $writer->startTag($key);
303 foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
304 $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} );
306 $writer->endTag($key);
309 $writer->endTag($payment_actions{ $self->action }{process});
310 $writer->endTag('Request');
313 warn "XML:\n$xml_data\n" if $DEBUG > 2;
315 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
316 # opts for B:OP:HTTPS::https_post
317 my $opts = { headers => {}};
318 $opts->{'Content-Type'} =
319 $opts->{headers}->{'Content-Type'} =
320 "multipart/form-data, boundary=$boundary";
324 "Content-Disposition: form-data; name=\"param\"\n\n".
328 # conform to RFC standards
329 $content =~ s/\n/\r\n/gs;
331 my ( $page, $server_response, %headers ) =
332 $self->https_post( $opts, $content );
334 # store the server response.
335 $self->server_response($server_response);
336 # parse the result page.
337 $self->parse_response($page);
339 if ( $self->is_success && $self->response_code == 10 ) { #partial auth
341 if ( $content{'partial_auth'} ) {
343 $self->partial_auth_amount( $self->txn_amount );
347 #XXX reverse auth if i was an auth only...
348 my $void = new Business::OnlinePayment(
350 map { $_ -> $self->$_() } qw( platform appid tid )
355 'amount' => $self->txn_amount,
356 'test_transaction' => $self->test_transaction,
357 'authorization' => $self->authorization,
358 map { $_ => $content{$_} } qw( login password card_number expiration )
363 if ( !$void->is_success ) {
364 #XXX now what??? at least this is better than return is_success 0 or 1
365 die "Couldn't void partial auth";
367 $self->is_success(0);
374 if (!$self->is_success() && !$self->error_message() ) {
376 #additional logging information, possibly too sensitive for an error
377 $self->error_message(
378 "(HTTPS response: ".$server_response.") ".
380 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
381 "(Raw HTTPS content: ".$page.")"
384 my $response_code = $self->response_code() || '';
385 if ($response_code) {
386 $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
388 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
395 # read $self->server_response and decipher any errors
400 if ($self->server_response =~ /^200/) {
401 my $response = XMLin($page);
402 warn "Response:\n".Dumper($response)."\n" if $DEBUG > 2;
403 $self->result_code($response->{Status}); # 0 /1
404 $self->response_code($response->{ResponseCode}); # see documentation for translation
405 $self->avs_code($response->{AvsResponse}); # Y / N
407 #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress
408 $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/
409 ? $response->{CvvResponse}
413 $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
414 $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
415 $self->reference_number($response->{ReferenceNumber});
417 $self->is_success($self->result_code() eq '0' ? 1 : 0);
418 if ($self->is_success()) {
419 $self->authorization($response->{ReferenceNumber});
420 } else { # fill in error_message if there is is an error
421 $self->error_message( 'Error '.$response->{ResponseCode}.': '.
422 ( $response->{AdditionalResponseData}
423 || $response->{Receipt}
424 || $response->{ResultCode}
430 die 'Error communicating with vSecureProcessing server (server sent response: '. $self->server_response. ')';
441 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
445 use Business::OnlinePayment;
446 my %processor_info = (
447 platform => 'vsecure_platform',
448 appid => 'vsecure_appid',
449 tid => '54', #optional, defaults to 01
452 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
454 login => 'vsecure@user.id',
455 password => '12345678901234567890', #vsecure gid
458 action => 'Normal Authorization',
459 description => 'Business::OnlinePayment test',
461 customer_id => 'tfb',
462 name => 'Tofu Beast',
463 address => '123 Anystreet',
467 card_number => '4007000000027',
468 expiration => '09/02',
469 cvv2 => '1234', #optional
473 if($tx->is_success()) {
474 print "Card processed successfully: ".$tx->authorization."\n";
476 print "Card was rejected: ".$tx->error_message."\n";
481 For detailed information see L<Business::OnlinePayment>.
483 =head1 METHODS AND FUNCTIONS
485 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
489 Returns the response error code.
493 Returns the response error description text.
495 =head2 server_response
497 Returns the complete response from the server.
499 =head1 Handling of content(%content) data:
503 The following actions are valid
509 =head1 Setting vSecureProcessing parameters from content(%content)
511 The following rules are applied to map data to vSecureProcessing parameters
512 from content(%content):
514 # param => $content{<key>}
515 AccountNumber => 'card_number',
517 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
518 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
521 CardHolderFirstName => 'first_name',
522 CardHolderLastName => 'last_name',
524 AvsStreet => 'address',
526 Cf1 => 'invoice_number',
527 Cf2 => 'customer_id',
528 IndustryType => 'IndustryInfo',
534 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
535 Version: 140901 (September 1, 2014).
537 See http://www.vsecureprocessing.com/ for more information.
541 Original author: Alex Brelsfoard
543 Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>
547 Copyright (c) 2015 Freeside Internet Services, Inc.
551 This program is free software; you can redistribute it and/or modify it under
552 the same terms as Perl itself.
556 Need a complete, open-source back-office and customer self-service solution?
557 The Freeside software includes support for credit card and electronic check
558 processing with vSecureProcessing and over 60 other gateways, invoicing,
559 integrated trouble ticketing, and customer signup and self-service web
562 http://freeside.biz/freeside/
566 perl(1). L<Business::OnlinePayment>.