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);
18 'info_compat' => '0.01',
19 'gateway_name' => 'vSecure Processing',
20 'gateway_url' => 'http://www.vsecureprocessing.com/',
21 'module_version' => $VERSION,
22 'supported_types' => [qw( CC )],
24 'test_transaction' => 1,
26 'supported_actions' => [
27 'Normal Authorization',
29 #'Post Authorization',
30 'Reverse Authorization',
37 # mapping out all possible endpoints
38 # but this version will only be building out "charge", "void", & "credit"
39 my %payment_actions = (
41 path => '/vsg2/processpayment',
42 process => 'ProcessPayment',
44 Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf3 AccountNumber
45 ExpirationMonth ExpirationYear Cvv
46 CardHolderFirstName CardHolderLastName AvsZip AvsStreet
47 IndustryType ApplicationId Recurring
51 path => '/vsg2/processvoid',
52 process => 'ProcessVoid',
54 Amount Cf1 Cf2 Cf3 AccountNumber
55 ExpirationMonth ExpirationYear ReferenceNumber
56 TransactionDate IndustryType ApplicationId
60 path => '/vsg2/processrefund',
61 process => 'ProcessRefund',
63 Amount Cf1 Cf2 Cf3 AccountNumber
64 ExpirationMonth ExpirationYear ApplicationId
68 path => '/vsg2/processauth',
69 process => 'ProcessAuth',
71 Amount TypeOfSale Cf1 Cf2 Cf3 AccountNumber
72 ExpirationMonth ExpirationYear Cvv
73 CardHolderFirstName CardHolderLastName AvsZip AvsStreet
74 IndustryType ApplicationId
78 'authorize_cancel' => {
79 path => '/vsg2/processauthcancel',
80 process => 'ProcessAuthCancel',
83 ExpirationMonth ExpirationYear
84 ReferenceNumber ResponseCode TransactionDate
85 IndustryType ApplicationId
89 path => '/vsg2/processcaptureonly',
92 path => '/vsg2/createtoken',
95 path => '/vsg2/deletetoken',
98 path => '/vsg2/querytoken',
100 'update_exp_date' => {
101 path => '/vsg2/updateexpiration',
104 path => '/vsg2/updatetoken',
109 my %action_mapping = (
110 'normal authorization' => 'charge',
111 'credit' => 'refund',
112 'authorization only' => 'authorize',
113 'post authorization' => 'capture',
114 'reverse authorization' => 'authorize_cancel'
122 # inistialize standard B::OP attributes
123 $self->is_success(0);
124 $self->$_( '' ) for qw/authorization
131 # B::OP creates the following accessors:
132 # server, path, test_transaction, transaction_type,
133 # server_response, is_success, authorization,
134 # result_code, error_message, response_code
136 $self->build_subs( qw(
138 action reference_number cvv2_response avs_code
139 risk_score txn_amount txn_date partial_auth_amount
142 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
144 $self->server('svr1.vsecureprocessing.com');
146 $self->tid($options{'tid'});
148 $self->platform($options{'platform'});
150 $self->appid($options{'appid'});
157 my ($self,$content) = @_;
158 my %content = $self->content();
161 no warnings 'uninitialized';
163 # strip non-digits from card number
164 my $card_number = '';
165 if ( $content{card_number} ) {
166 $content{card_number} =~ s/\D//g;
169 if ($content{'description'} && length($content{'description'}) >20) {
170 $content{'description'} = substr($content{'description'},0,20);
173 # separate month and year values for expiry_date
174 if ( $content{expiration} ) {
175 ($content{exp_month}, $content{exp_year}) =
176 split /\//, $content{expiration};
177 $content{exp_month} = sprintf "%02d", $content{exp_month};
178 $content{exp_year} = substr($content{exp_year},0,2)
179 if ($content{exp_year} > 99);
182 if ( !$content{'first_name'}
183 || !$content{'last_name'} && $content{'name'}
186 ($content{'first_name'}, $content{'last_name'}) =
187 split(' ', $content{'name'}, 2);
190 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
191 $content{'street_number'} = $1;
194 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
195 $self->content(%content);
198 sub process_content {
200 $self->clean_content();
201 my %content = $self->content();
202 $self->action( ($action_mapping{lc $content{'action'}})
203 ? $action_mapping{lc $content{'action'}}
204 : lc $content{'action'}
206 $self->path($payment_actions{ $self->action }{path})
207 unless length($self->path);
208 $self->appid($content{appid}) if (!$self->appid && $content{appid});
214 # inistialize standard B::OP attributes
215 $self->is_success(0);
216 $self->$_( '' ) for qw/authorization
221 # clean and process the $self->content info
222 $self->process_content();
223 my %content = $self->content;
224 my $action = $self->action();
226 if ( $self->test_transaction ) {
227 $self->server('dvrotsos2.kattare.com');
230 my @acceptable_actions = ('charge', 'refund', 'void', 'authorize', 'authorize_cancel');
232 unless ( grep { $action eq $_ } @acceptable_actions ) {
233 croak "'$action' is not supported at this time.";
236 # fill in the xml vars
239 Platform => $self->platform,
240 UserId => $content{'login'},
241 GID => $content{'password'},
242 Tid => $self->tid || '01',
246 Amount => $content{'amount'},
247 Trk1 => ($content{'track1'}) ? $content{'track1'} : '',
248 Trk2 => ($content{'track2'}) ? $content{'track2'} : '',
249 TypeOfSale => ($content{'description'}) ? $content{'description'} : '',
250 Cf1 => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
251 Cf2 => ($content{'customer_id'}) ? $content{'customer_id'} : '',
253 AccountNumber => ($content{'card_number'}) ? $content{'card_number'} : '',
254 ExpirationMonth => $content{'exp_month'},
255 ExpirationYear => $content{'exp_year'},
256 Cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
257 CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '',
258 CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '',
259 AvsZip => ($content{'zip'}) ? $content{'zip'} : '',
260 AvsStreet => ($content{'street_number'}) ? $content{'street_number'} : '',
262 # IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
263 # IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
265 ApplicationId => $self->appid(),
266 Recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
267 ReferenceNumber => ($content{'authorization'}) ? $content{'authorization'} : '',
268 Token => ($content{'token'}) ? $content{'token'} : '',
269 Receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
270 TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : '',
271 # note that B::OP response_code is the http response & message, ie '500 Internal Server Error'
272 # whereas ResponseCode is a 2-digit ISO code indicating result of transaction
273 ResponseCode => ($content{'result_code'}) ? $content{'result_code'} : '',
275 # we won't be using level2 nor level3. So I'm leaving them out for now.
278 # create the list of required fields based on the action
279 my @required_fields = qw/ Amount /;
280 if ($action eq 'charge') {
281 push @required_fields, $_
282 foreach (qw/ AccountNumber ExpirationMonth ExpirationYear /);
283 }elsif ($action eq 'void') {
284 push @required_fields, $_
285 foreach (qw/ ReferenceNumber /);
286 }elsif ($action eq 'refund') {
287 push @required_fields, $_
288 foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /);
291 # check the requirements are met.
293 foreach my $field (@required_fields) {
294 push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
296 if (scalar(@missing_fields)) {
297 croak "Missing required fields: ".join(', ', @missing_fields);
301 my $writer = new XML::Writer( OUTPUT => \$xml_data,
307 $writer->startTag('Request');
308 $writer->startTag('MerchantData');
309 foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
310 $writer->dataElement( $key, $xml_vars->{auth}{$key} );
312 $writer->endTag('MerchantData');
313 $writer->startTag($payment_actions{ $self->action }{process});
314 foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) {
315 next if (!$xml_vars->{payment}{$key});
316 if (ref $xml_vars->{payment}{$key} eq '') {
317 $writer->dataElement( $key, $xml_vars->{payment}{$key});
319 $writer->startTag($key);
320 foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
321 $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} );
323 $writer->endTag($key);
326 $writer->endTag($payment_actions{ $self->action }{process});
327 $writer->endTag('Request');
330 warn "XML:\n$xml_data\n" if $DEBUG > 2;
332 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
333 # opts for B:OP:HTTPS::https_post
334 my $opts = { headers => {}};
335 $opts->{'Content-Type'} =
336 $opts->{headers}->{'Content-Type'} =
337 "multipart/form-data, boundary=$boundary";
341 "Content-Disposition: form-data; name=\"param\"\n\n".
345 # conform to RFC standards
346 $content =~ s/\n/\r\n/gs;
348 my ( $page, $server_response, %headers ) =
349 $self->https_post( $opts, $content );
351 # store the server response.
352 $self->server_response($server_response);
353 # parse the result page.
354 $self->parse_response($page);
356 if ( $self->is_success && $self->result_code == 10 ) { #partial auth
358 if ( $content{'partial_auth'} ) {
360 $self->partial_auth_amount( $self->txn_amount );
364 #XXX reverse auth if i was an auth only...
365 my $void = new Business::OnlinePayment(
367 map { $_ -> $self->$_() } qw( platform appid tid )
372 'amount' => $self->txn_amount,
373 'test_transaction' => $self->test_transaction,
374 'authorization' => $self->authorization,
375 map { $_ => $content{$_} } qw( login password card_number expiration )
380 if ( !$void->is_success ) {
381 #XXX now what??? at least this is better than return is_success 0 or 1
382 die "Couldn't void partial auth";
384 $self->is_success(0);
391 if (!$self->is_success() && !$self->error_message() ) {
393 #additional logging information, possibly too sensitive for an error
394 $self->error_message(
395 "(HTTPS response: ".$server_response.") ".
397 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
398 "(Raw HTTPS content: ".$page.")"
401 my $result_code = $self->result_code() || '';
403 $self->error_message(qq|Error code ${result_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
405 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
412 # read $self->server_response and decipher any errors
417 if ($self->server_response =~ /^200/) {
418 my $response = XMLin($page);
419 warn "Response:\n".Dumper($response)."\n" if $DEBUG > 2;
420 $self->is_success($response->{Status} ? 0 : 1);
421 $self->result_code($response->{ResponseCode}); # NOT response_code, which is http response
422 $self->avs_code($response->{AvsResponse}); # Y / N
424 #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress
425 $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/
426 ? $response->{CvvResponse}
430 $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
431 $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
432 $self->reference_number($response->{ReferenceNumber}); # overlap with authorization,
433 # not used by anything or documented???
435 if ($self->is_success()) {
436 $self->authorization($response->{ReferenceNumber});
437 } else { # fill in error_message if there is is an error
438 $self->error_message( 'Error '.$response->{ResponseCode}.': '.
439 ( $response->{AdditionalResponseData}
440 || $response->{Receipt}
441 || $response->{ResultCode}
447 die 'Error communicating with vSecureProcessing server (server sent response: '. $self->server_response. ')';
458 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
462 use Business::OnlinePayment;
463 my %processor_info = (
464 platform => 'vsecure_platform',
465 appid => 'vsecure_appid',
466 tid => '54', #optional, defaults to 01
469 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
471 login => 'vsecure@user.id',
472 password => '12345678901234567890', #vsecure gid
475 action => 'Normal Authorization',
476 description => 'Business::OnlinePayment test',
478 customer_id => 'tfb',
479 name => 'Tofu Beast',
480 address => '123 Anystreet',
484 card_number => '4007000000027',
485 expiration => '09/02',
486 cvv2 => '1234', #optional
490 if($tx->is_success()) {
491 print "Card processed successfully: ".$tx->authorization."\n";
493 print "Card was rejected: ".$tx->error_message."\n";
498 For detailed information see L<Business::OnlinePayment>.
500 =head1 METHODS AND FUNCTIONS
502 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
506 Returns the response error code.
510 Returns the response error description text.
512 =head2 server_response
514 Returns the complete response from the server.
516 =head1 Handling of content(%content) data:
520 The following actions are valid
526 =head1 Setting vSecureProcessing parameters from content(%content)
528 The following rules are applied to map data to vSecureProcessing parameters
529 from content(%content):
531 # param => $content{<key>}
532 AccountNumber => 'card_number',
534 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
535 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
538 CardHolderFirstName => 'first_name',
539 CardHolderLastName => 'last_name',
541 AvsStreet => 'address',
543 Cf1 => 'invoice_number',
544 Cf2 => 'customer_id',
545 IndustryType => 'IndustryInfo',
551 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
552 Version: 140901 (September 1, 2014).
554 See http://www.vsecureprocessing.com/ for more information.
558 Original author: Alex Brelsfoard
560 Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>
564 Copyright (c) 2015 Freeside Internet Services, Inc.
568 This program is free software; you can redistribute it and/or modify it under
569 the same terms as Perl itself.
573 Need a complete, open-source back-office and customer self-service solution?
574 The Freeside software includes support for credit card and electronic check
575 processing with vSecureProcessing and over 60 other gateways, invoicing,
576 integrated trouble ticketing, and customer signup and self-service web
579 http://freeside.biz/freeside/
583 perl(1). L<Business::OnlinePayment>.