cvv2_response should never be passed back as a hashref
[Business-OnlinePayment-vSecureProcessing.git] / lib / Business / OnlinePayment / vSecureProcessing.pm
1 package Business::OnlinePayment::vSecureProcessing;
2
3 use strict;
4 use vars qw($VERSION $DEBUG @ISA);
5 use Carp;
6 use XML::Writer;
7 use XML::Simple;
8 use Data::Dumper;
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11
12 @ISA = qw(Business::OnlinePayment::HTTPS);
13 $DEBUG = 0;
14 $VERSION = '0.05';
15
16 # mapping out all possible endpoints
17 # but this version will only be building out "charge", "void", & "credit"
18 my %payment_actions = (
19     'charge' => {
20         path      => '/vsg2/processpayment',
21         process   => 'ProcessPayment',
22         fields    => [qw/
23           Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf AccountNumber
24           ExpirationMonth ExpirationYear Cvv
25           CardHolderFirstName CardHolderLastName AvsZip AvsStreet
26           IndustryType ApplicationId Recurring
27         /],
28     },
29     'void' => {
30         path      => '/vsg2/processvoid',
31         process   => 'ProcessVoid',
32         fields    => [qw(
33           Amount AccountNumber ExpirationMonth ExpirationYear ReferenceNumber
34           TransactionDate IndustryType ApplicationId
35         )],
36     },
37     'refund' => {
38         path      => '/vsg2/processrefund',
39         process   => 'ProcessRefund',
40         fields    => [qw(
41           Amount AccountNumber ExpirationMonth ExpirationYear ApplicationId
42         )],
43     },
44     'authorize' => {
45         path      => '/vsg2/processauth',
46     },
47     'authorize_cancel' => {
48         path      => '/vsg2/processauthcancel',
49     },
50     'capture' => {
51         path      => '/vsg2/processcaptureonly',
52     },
53     'create_token' => {
54         path      => '/vsg2/createtoken',
55     },
56     'delete_token' => {
57         path      => '/vsg2/deletetoken',
58     },
59     'query_token' => {
60         path      => '/vsg2/querytoken',
61     },
62     'update_exp_date' => {
63         path      => '/vsg2/updateexpiration',
64     },
65     'update_token' => {
66         path      => '/vsg2/updatetoken',
67     },
68
69 );
70
71 my %action_mapping = (
72     'normal authorization'  => 'charge',
73     'credit'                => 'refund',
74     'authorization only'    => 'authorize',
75     'post authorization'    => 'capture',
76     'reverse authorization' => 'authorize_cancel'
77     # void => void
78 );
79
80 sub set_defaults {
81     my $self = shift;
82     my %options = @_;
83     
84     # inistialize standard B::OP attributes
85     $self->is_success(0);
86     $self->$_( '' ) for qw/authorization
87                            result_code
88                            error_message
89                            server
90                            path
91                            server_response/;
92                            
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,
97     
98     $self->build_subs(qw/
99             platform tid appid
100             action reference_number cvv2_response avs_code response_code
101             risk_score txn_amount txn_date
102     /);
103     
104     $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
105     
106     $self->server('svr1.vsecureprocessing.com');
107     
108     $self->tid($options{'tid'});
109     
110     $self->platform($options{'platform'});
111     
112     $self->appid($options{'appid'});
113     
114     $self->port(443);
115     
116 }
117
118 sub clean_content {
119     my ($self,$content) = @_;
120     my %content = $self->content();
121     
122     {
123         no warnings 'uninitialized';
124         
125         # strip non-digits from card number
126         my $card_number = '';
127         if ( $content{card_number} ) {
128             $content{card_number} =~ s/\D//g;
129         }
130         
131         if ($content{'description'} && length($content{'description'}) >20) {
132             $content{'description'} = substr($content{'description'},0,20);
133         }
134         
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);
142         }
143         
144         if (    !$content{'first_name'}
145              || !$content{'last_name'} && $content{'name'}
146            )
147         {
148             ($content{'first_name'}, $content{'last_name'}) =
149               split(' ', $content{'name'}, 2);
150         }
151         
152         if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
153             $content{'street_number'} = $1;
154         }
155     }
156     warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
157     $self->content(%content);
158 }
159
160 sub process_content {
161     my $self = shift;
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'}
167                  );
168     $self->path($payment_actions{ $self->action }{path})
169       unless length($self->path);
170     $self->appid($content{appid}) if (!$self->appid && $content{appid});
171 }
172
173 sub submit {
174     my $self = shift;
175     
176     # inistialize standard B::OP attributes
177     $self->is_success(0);
178     $self->$_( '' ) for qw/authorization
179                            result_code
180                            error_message
181                            server_response/;
182                            
183     # clean and process the $self->content info
184     $self->process_content();
185     my %content = $self->content;
186     my $action = $self->action();
187
188     if ( $self->test_transaction ) {
189       $self->server('dvrotsos2.kattare.com');
190     }
191     
192     my @acceptable_actions = ('charge', 'refund', 'void');
193     
194     unless ( grep { $action eq $_ } @acceptable_actions ) {
195         croak "'$action' is not supported at this time.";
196     }
197     
198     # fill in the xml vars
199     my $xml_vars = {
200         auth => {
201             Platform    => $self->platform,
202             UserId      => $content{'login'},
203             GID         => $content{'password'},
204             Tid         => $self->tid || '01',
205         },
206         
207         payment => {
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'} : '',
214             Cf3             => '',
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'} : '',
223 #            IndustryType    =>  { 
224 #                                IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
225 #                                IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
226 #                                },
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'} : ''
233         }
234         # we won't be using level2 nor level3.  So I'm leaving them out for now.
235     };
236   
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 /);
248     }
249     
250     # check the requirements are met.
251     my @missing_fields;
252     foreach my $field (@required_fields) {
253         push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
254     }
255     if (scalar(@missing_fields)) {
256         croak "Missing required fields: ".join(', ', @missing_fields);
257     }
258     
259     my $process_action = $action;
260     $process_action =~ s/\b([a-z])/\u$1/g;
261     $process_action = 'Process'.$process_action;
262     my $xml_data;
263     my $writer = new XML::Writer( OUTPUT      => \$xml_data,
264                                   DATA_MODE   => 0,
265                                   DATA_INDENT => 0,
266                                   ENCODING    => 'utf-8',
267                                 );
268     $writer->xmlDecl();
269     $writer->startTag('Request');
270     $writer->startTag('MerchantData');
271     foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
272         $writer->dataElement( $key, $xml_vars->{auth}{$key} );
273     }
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}); 
280         }else {
281             $writer->startTag($key);
282             foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
283               $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} ); 
284             }
285             $writer->endTag($key);
286         }
287     }
288     $writer->endTag($payment_actions{ $self->action }{process});
289     $writer->endTag('Request');
290     $writer->end();
291     
292     warn "XML:\n$xml_data\n" if $DEBUG > 2;
293     
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";
300
301     my $content =
302       "--$boundary\n".
303      "Content-Disposition: form-data; name=\"param\"\n\n".
304      $xml_data."\n".
305      "--$boundary--\n";
306
307     # conform to RFC standards
308     $content =~ s/\n/\r\n/gs;
309
310     my ( $page, $server_response, %headers ) =
311       $self->https_post( $opts, $content );
312   
313     # store the server response.
314     $self->server_response($server_response);
315     # parse the result page.
316     $self->parse_response($page);
317     
318     if (!$self->is_success() && !$self->error_message() ) {
319         if ( $DEBUG ) {
320             #additional logging information, possibly too sensitive for an error
321             $self->error_message(
322               "(HTTPS response: ".$server_response.") ".
323               "(HTTPS headers: ".
324             join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
325               "(Raw HTTPS content: ".$page.")"
326             );
327         } else {
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)|);
331             }else {
332                 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
333             }
334         }
335     }
336     
337 }
338
339 # read $self->server_response and decipher any errors
340 sub parse_response {
341     my $self = shift;
342     my $page = shift;
343
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
350
351         #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress
352         $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/
353                                 ? $response->{CvvResponse}
354                                 : ''
355                             );
356
357         $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
358         $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
359         $self->reference_number($response->{ReferenceNumber});
360         
361         $self->is_success($self->result_code() eq '0' ? 1 : 0);
362         if ($self->is_success()) {
363             $self->authorization($response->{ReferenceNumber});
364         }
365         # fill in error_message if there is is an error
366         if ( !$self->is_success && exists($response->{AdditionalResponseData})) {
367             $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{AdditionalResponseData});
368         }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
369             $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
370         }
371         
372     } else {
373         die 'Error communicating with vSecureProcessing server';
374         return;
375     }
376     
377 }
378
379 1;
380 __END__
381
382
383 =head1 NAME
384
385 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
386
387 =head1 SYNOPSIS
388
389   use Business::OnlinePayment;
390   my %processor_info = (
391     platform    => 'vsecure_platform',
392     appid       => 'vsecure_appid',
393     tid         => '54', #optional, defaults to 01
394   );
395   my $tx =
396     new Business::OnlinePayment( "vSecureProcessing", %processor_info);
397   $tx->content(
398       login          => 'vsecure@user.id',
399       password       => '12345678901234567890', #vsecure gid
400
401       type           => 'CC',
402       action         => 'Normal Authorization',
403       description    => 'Business::OnlinePayment test',
404       amount         => '49.95',
405       customer_id    => 'tfb',
406       name           => 'Tofu Beast',
407       address        => '123 Anystreet',
408       city           => 'Anywhere',
409       state          => 'UT',
410       zip            => '84058',
411       card_number    => '4007000000027',
412       expiration     => '09/02',
413       cvv2           => '1234', #optional
414   );
415   $tx->submit();
416
417   if($tx->is_success()) {
418       print "Card processed successfully: ".$tx->authorization."\n";
419   } else {
420       print "Card was rejected: ".$tx->error_message."\n";
421   }
422
423 =head1 DESCRIPTION
424
425 For detailed information see L<Business::OnlinePayment>.
426
427 =head1 METHODS AND FUNCTIONS
428
429 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
430
431 =head2 result_code
432
433 Returns the response error code.
434
435 =head2 error_message
436
437 Returns the response error description text.
438
439 =head2 server_response
440
441 Returns the complete response from the server.
442
443 =head1 Handling of content(%content) data:
444
445 =head2 action
446
447 The following actions are valid
448
449   normal authorization
450   credit
451   void
452
453 =head1 Setting vSecureProcessing parameters from content(%content)
454
455 The following rules are applied to map data to vSecureProcessing parameters
456 from content(%content):
457
458       # param => $content{<key>}
459       AccountNumber       => 'card_number',
460       Cvv                 => 'cvv2',
461       ExpirationMonth     => \( $month ), # MM from MM/YY of 'expiration'
462       ExpirationYear      => \( $year ), # YY from MM/YY of 'expiration'
463       Trk1                => 'track1',
464       Trk2                => 'track2',
465       CardHolderFirstName => 'first_name',
466       CardHolderLastName  => 'last_name',
467       Amount              => 'amount'
468       AvsStreet           => 'address',
469       AvsZip              => 'zip',
470       Cf1                 => 'UDField1',
471       Cf2                 => 'UDField2',
472       IndustryType        => 'IndustryInfo',
473
474 =head1 NOTE
475
476 =head1 COMPATIBILITY
477
478 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
479 Version: 140901 (September 1, 2014).
480
481 See http://www.vsecureprocessing.com/ for more information.
482
483 =head1 AUTHORS
484
485 Original author: Alex Brelsfoard
486
487 Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>
488
489 =head1 COPYRIGHT
490
491 Copyright (c) 2015 Freeside Internet Services, Inc.
492
493 All rights reserved.
494
495 This program is free software; you can redistribute it and/or modify it under
496 the same terms as Perl itself.
497
498 =head1 ADVERTISEMENT
499
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.
504
505 http://freeside.biz/freeside/
506
507 =head1 SEE ALSO
508
509 perl(1). L<Business::OnlinePayment>.
510
511 =cut
512