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 Cf AccountNumber
43 ExpirationMonth ExpirationYear Cvv
44 CardHolderFirstName CardHolderLastName AvsZip AvsStreet
45 IndustryType ApplicationId Recurring
49 path => '/vsg2/processvoid',
50 process => 'ProcessVoid',
52 Amount AccountNumber ExpirationMonth ExpirationYear ReferenceNumber
53 TransactionDate IndustryType ApplicationId
57 path => '/vsg2/processrefund',
58 process => 'ProcessRefund',
60 Amount AccountNumber ExpirationMonth ExpirationYear ApplicationId
64 path => '/vsg2/processauth',
66 'authorize_cancel' => {
67 path => '/vsg2/processauthcancel',
70 path => '/vsg2/processcaptureonly',
73 path => '/vsg2/createtoken',
76 path => '/vsg2/deletetoken',
79 path => '/vsg2/querytoken',
81 'update_exp_date' => {
82 path => '/vsg2/updateexpiration',
85 path => '/vsg2/updatetoken',
90 my %action_mapping = (
91 'normal authorization' => 'charge',
93 'authorization only' => 'authorize',
94 'post authorization' => 'capture',
95 'reverse authorization' => 'authorize_cancel'
103 # inistialize standard B::OP attributes
104 $self->is_success(0);
105 $self->$_( '' ) for qw/authorization
112 # B::OP creates the following accessors:
113 # server, path, test_transaction, transaction_type,
114 # server_response, is_success, authorization,
115 # result_code, error_message,
117 $self->build_subs(qw/
119 action reference_number cvv2_response avs_code response_code
120 risk_score txn_amount txn_date partial_auth partial_auth_amount
123 $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
125 $self->server('svr1.vsecureprocessing.com');
127 $self->tid($options{'tid'});
129 $self->platform($options{'platform'});
131 $self->appid($options{'appid'});
138 my ($self,$content) = @_;
139 my %content = $self->content();
142 no warnings 'uninitialized';
144 # strip non-digits from card number
145 my $card_number = '';
146 if ( $content{card_number} ) {
147 $content{card_number} =~ s/\D//g;
150 if ($content{'description'} && length($content{'description'}) >20) {
151 $content{'description'} = substr($content{'description'},0,20);
154 # separate month and year values for expiry_date
155 if ( $content{expiration} ) {
156 ($content{exp_month}, $content{exp_year}) =
157 split /\//, $content{expiration};
158 $content{exp_month} = sprintf "%02d", $content{exp_month};
159 $content{exp_year} = substr($content{exp_year},0,2)
160 if ($content{exp_year} > 99);
163 if ( !$content{'first_name'}
164 || !$content{'last_name'} && $content{'name'}
167 ($content{'first_name'}, $content{'last_name'}) =
168 split(' ', $content{'name'}, 2);
171 if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
172 $content{'street_number'} = $1;
175 warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
176 $self->content(%content);
179 sub process_content {
181 $self->clean_content();
182 my %content = $self->content();
183 $self->action( ($action_mapping{lc $content{'action'}})
184 ? $action_mapping{lc $content{'action'}}
185 : lc $content{'action'}
187 $self->path($payment_actions{ $self->action }{path})
188 unless length($self->path);
189 $self->appid($content{appid}) if (!$self->appid && $content{appid});
195 # inistialize standard B::OP attributes
196 $self->is_success(0);
197 $self->$_( '' ) for qw/authorization
202 # clean and process the $self->content info
203 $self->process_content();
204 my %content = $self->content;
205 my $action = $self->action();
207 if ( $self->test_transaction ) {
208 $self->server('dvrotsos2.kattare.com');
211 my @acceptable_actions = ('charge', 'refund', 'void');
213 unless ( grep { $action eq $_ } @acceptable_actions ) {
214 croak "'$action' is not supported at this time.";
217 # fill in the xml vars
220 Platform => $self->platform,
221 UserId => $content{'login'},
222 GID => $content{'password'},
223 Tid => $self->tid || '01',
227 Amount => $content{'amount'},
228 Trk1 => ($content{'track1'}) ? $content{'track1'} : '',
229 Trk2 => ($content{'track2'}) ? $content{'track2'} : '',
230 TypeOfSale => ($content{'description'}) ? $content{'description'} : '',
231 Cf1 => ($content{'UDField1'}) ? $content{'UDField1'} : '',
232 Cf2 => ($content{'UDField2'}) ? $content{'UDField2'} : '',
234 AccountNumber => ($content{'card_number'}) ? $content{'card_number'} : '',
235 ExpirationMonth => $content{'exp_month'},
236 ExpirationYear => $content{'exp_year'},
237 Cvv => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
238 CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '',
239 CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '',
240 AvsZip => ($content{'zip'}) ? $content{'zip'} : '',
241 AvsStreet => ($content{'street_number'}) ? $content{'street_number'} : '',
243 # IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
244 # IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
246 ApplicationId => $self->appid(),
247 Recurring => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
248 ReferenceNumber => ($content{'ref_num'}) ? $content{'ref_num'} : '',
249 Token => ($content{'token'}) ? $content{'token'} : '',
250 Receipt => ($content{'receipt'}) ? $content{'receipt'} : '',
251 TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : ''
253 # we won't be using level2 nor level3. So I'm leaving them out for now.
256 # create the list of required fields based on the action
257 my @required_fields = qw/ Amount /;
258 if ($action eq 'charge') {
259 push @required_fields, $_
260 foreach (qw/ AccountNumber ExpirationMonth ExpirationYear /);
261 }elsif ($action eq 'void') {
262 push @required_fields, $_
263 foreach (qw/ ReferenceNumber /);
264 }elsif ($action eq 'refund') {
265 push @required_fields, $_
266 foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /);
269 # check the requirements are met.
271 foreach my $field (@required_fields) {
272 push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
274 if (scalar(@missing_fields)) {
275 croak "Missing required fields: ".join(', ', @missing_fields);
278 my $process_action = $action;
279 $process_action =~ s/\b([a-z])/\u$1/g;
280 $process_action = 'Process'.$process_action;
282 my $writer = new XML::Writer( OUTPUT => \$xml_data,
288 $writer->startTag('Request');
289 $writer->startTag('MerchantData');
290 foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
291 $writer->dataElement( $key, $xml_vars->{auth}{$key} );
293 $writer->endTag('MerchantData');
294 $writer->startTag($payment_actions{ $self->action }{process});
295 foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) {
296 next if (!$xml_vars->{payment}{$key});
297 if (ref $xml_vars->{payment}{$key} eq '') {
298 $writer->dataElement( $key, $xml_vars->{payment}{$key});
300 $writer->startTag($key);
301 foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
302 $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} );
304 $writer->endTag($key);
307 $writer->endTag($payment_actions{ $self->action }{process});
308 $writer->endTag('Request');
311 warn "XML:\n$xml_data\n" if $DEBUG > 2;
313 my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
314 # opts for B:OP:HTTPS::https_post
315 my $opts = { headers => {}};
316 $opts->{'Content-Type'} =
317 $opts->{headers}->{'Content-Type'} =
318 "multipart/form-data, boundary=$boundary";
322 "Content-Disposition: form-data; name=\"param\"\n\n".
326 # conform to RFC standards
327 $content =~ s/\n/\r\n/gs;
329 my ( $page, $server_response, %headers ) =
330 $self->https_post( $opts, $content );
332 # store the server response.
333 $self->server_response($server_response);
334 # parse the result page.
335 $self->parse_response($page);
337 if ( $self->is_success && $self->response_code == 10 ) { #partial auth
339 if ( $self->partial_auth ) {
341 $self->partial_auth_amount( $self->txn_amount );
345 #XXX reverse auth if i was an auth only...
346 my $void = new Business::OnlinePayment(
348 map { $_ -> $self->$_() } qw( platform appid tid )
353 'amount' => $self->txn_amount,
354 'test_transaction' => $self->test_transaction,
355 'authorization' => $self->authorization,
356 map { $_ => $content{$_} } qw( login password card_number expiration )
361 if ( !$void->is_success ) {
362 #XXX now what??? at least this is better than return is_success 0 or 1
363 die "Couldn't void partial auth";
365 $self->is_success(0);
372 if (!$self->is_success() && !$self->error_message() ) {
374 #additional logging information, possibly too sensitive for an error
375 $self->error_message(
376 "(HTTPS response: ".$server_response.") ".
378 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
379 "(Raw HTTPS content: ".$page.")"
382 my $response_code = $self->response_code() || '';
383 if ($response_code) {
384 $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
386 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
393 # read $self->server_response and decipher any errors
398 if ($self->server_response =~ /^200/) {
399 my $response = XMLin($page);
400 warn "Response:\n".Dumper($response)."\n" if $DEBUG > 2;
401 $self->result_code($response->{Status}); # 0 /1
402 $self->response_code($response->{ResponseCode}); # see documentation for translation
403 $self->avs_code($response->{AvsResponse}); # Y / N
405 #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress
406 $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/
407 ? $response->{CvvResponse}
411 $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
412 $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
413 $self->reference_number($response->{ReferenceNumber});
415 $self->is_success($self->result_code() eq '0' ? 1 : 0);
416 if ($self->is_success()) {
417 $self->authorization($response->{ReferenceNumber});
418 } else { # fill in error_message if there is is an error
419 $self->error_message( 'Error '.$response->{ResponseCode}.': '.
420 ( $response->{AdditionalResponseData}
421 || $response->{Receipt}
422 || $response->{ResultCode}
428 die 'Error communicating with vSecureProcessing server (server sent response: '. $self->server_response. ')';
439 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
443 use Business::OnlinePayment;
444 my %processor_info = (
445 platform => 'vsecure_platform',
446 appid => 'vsecure_appid',
447 tid => '54', #optional, defaults to 01
450 new Business::OnlinePayment( "vSecureProcessing", %processor_info);
452 login => 'vsecure@user.id',
453 password => '12345678901234567890', #vsecure gid
456 action => 'Normal Authorization',
457 description => 'Business::OnlinePayment test',
459 customer_id => 'tfb',
460 name => 'Tofu Beast',
461 address => '123 Anystreet',
465 card_number => '4007000000027',
466 expiration => '09/02',
467 cvv2 => '1234', #optional
471 if($tx->is_success()) {
472 print "Card processed successfully: ".$tx->authorization."\n";
474 print "Card was rejected: ".$tx->error_message."\n";
479 For detailed information see L<Business::OnlinePayment>.
481 =head1 METHODS AND FUNCTIONS
483 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
487 Returns the response error code.
491 Returns the response error description text.
493 =head2 server_response
495 Returns the complete response from the server.
497 =head1 Handling of content(%content) data:
501 The following actions are valid
507 =head1 Setting vSecureProcessing parameters from content(%content)
509 The following rules are applied to map data to vSecureProcessing parameters
510 from content(%content):
512 # param => $content{<key>}
513 AccountNumber => 'card_number',
515 ExpirationMonth => \( $month ), # MM from MM/YY of 'expiration'
516 ExpirationYear => \( $year ), # YY from MM/YY of 'expiration'
519 CardHolderFirstName => 'first_name',
520 CardHolderLastName => 'last_name',
522 AvsStreet => 'address',
526 IndustryType => 'IndustryInfo',
532 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
533 Version: 140901 (September 1, 2014).
535 See http://www.vsecureprocessing.com/ for more information.
539 Original author: Alex Brelsfoard
541 Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>
545 Copyright (c) 2015 Freeside Internet Services, Inc.
549 This program is free software; you can redistribute it and/or modify it under
550 the same terms as Perl itself.
554 Need a complete, open-source back-office and customer self-service solution?
555 The Freeside software includes support for credit card and electronic check
556 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
557 trouble ticketing, and customer signup and self-service web interfaces.
559 http://freeside.biz/freeside/
563 perl(1). L<Business::OnlinePayment>.