0.03, cvv2 not required for all transactions (+add test, update MANIFEST), RT#32782
[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.03';
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 cvv_response avs_response 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_response($response->{AvsResponse}); # Y / N
350         $self->cvv_response($response->{CvvResponse}); # P / F
351         $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
352         $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
353         $self->reference_number($response->{ReferenceNumber});
354         
355         $self->is_success($self->result_code() eq '0' ? 1 : 0);
356         if ($self->is_success()) {
357             $self->authorization($response->{ReferenceNumber});
358         }
359         # fill in error_message if there is is an error
360         if ( !$self->is_success && exists($response->{AdditionalResponseData})) {
361             $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{AdditionalResponseData});
362         }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
363             $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
364         }
365         
366     } else {
367         die 'Error communicating with vSecureProcessing server';
368         return;
369     }
370     
371 }
372
373 1;
374 __END__
375
376
377 =head1 NAME
378
379 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
380
381 =head1 SYNOPSIS
382
383   use Business::OnlinePayment;
384   my %processor_info = (
385     platform    => 'vsecure_platform',
386     appid       => 'vsecure_appid',
387     tid         => '54', #optional, defaults to 01
388   );
389   my $tx =
390     new Business::OnlinePayment( "vSecureProcessing", %processor_info);
391   $tx->content(
392       login          => 'vsecure@user.id',
393       password       => '12345678901234567890', #vsecure gid
394
395       type           => 'CC',
396       action         => 'Normal Authorization',
397       description    => 'Business::OnlinePayment test',
398       amount         => '49.95',
399       customer_id    => 'tfb',
400       name           => 'Tofu Beast',
401       address        => '123 Anystreet',
402       city           => 'Anywhere',
403       state          => 'UT',
404       zip            => '84058',
405       card_number    => '4007000000027',
406       expiration     => '09/02',
407       cvv2           => '1234', #optional
408   );
409   $tx->submit();
410
411   if($tx->is_success()) {
412       print "Card processed successfully: ".$tx->authorization."\n";
413   } else {
414       print "Card was rejected: ".$tx->error_message."\n";
415   }
416
417 =head1 DESCRIPTION
418
419 For detailed information see L<Business::OnlinePayment>.
420
421 =head1 METHODS AND FUNCTIONS
422
423 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
424
425 =head2 result_code
426
427 Returns the response error code.
428
429 =head2 error_message
430
431 Returns the response error description text.
432
433 =head2 server_response
434
435 Returns the complete response from the server.
436
437 =head1 Handling of content(%content) data:
438
439 =head2 action
440
441 The following actions are valid
442
443   normal authorization
444   credit
445   void
446
447 =head1 Setting vSecureProcessing parameters from content(%content)
448
449 The following rules are applied to map data to vSecureProcessing parameters
450 from content(%content):
451
452       # param => $content{<key>}
453       AccountNumber       => 'card_number',
454       Cvv                 => 'cvv2',
455       ExpirationMonth     => \( $month ), # MM from MM/YY of 'expiration'
456       ExpirationYear      => \( $year ), # YY from MM/YY of 'expiration'
457       Trk1                => 'track1',
458       Trk2                => 'track2',
459       CardHolderFirstName => 'first_name',
460       CardHolderLastName  => 'last_name',
461       Amount              => 'amount'
462       AvsStreet           => 'address',
463       AvsZip              => 'zip',
464       Cf1                 => 'UDField1',
465       Cf2                 => 'UDField2',
466       IndustryType        => 'IndustryInfo',
467
468 =head1 NOTE
469
470 =head1 COMPATIBILITY
471
472 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
473 Version: 140901 (September 1, 2014).
474
475 See http://www.vsecureprocessing.com/ for more information.
476
477 =head1 AUTHORS
478
479 Original author: Alex Brelsfoard
480
481 Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>
482
483 =head1 COPYRIGHT
484
485 Copyright (c) 2015 Freeside Internet Services, Inc.
486
487 All rights reserved.
488
489 This program is free software; you can redistribute it and/or modify it under
490 the same terms as Perl itself.
491
492 =head1 ADVERTISEMENT
493
494 Need a complete, open-source back-office and customer self-service solution?
495 The Freeside software includes support for credit card and electronic check
496 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
497 trouble ticketing, and customer signup and self-service web interfaces.
498
499 http://freeside.biz/freeside/
500
501 =head1 SEE ALSO
502
503 perl(1). L<Business::OnlinePayment>.
504
505 =cut
506