RT#42265: Add Authorization Only and Reverse Authorization support to B:OP:vSecurePro...
[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.10';
15
16 sub _info {
17   return {
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 )],
23     'token_support'     => 0,
24     'test_transaction'  => 1,
25     'partial_auth'      => 1,
26     'supported_actions' => [
27                              'Normal Authorization',
28                              'Authorization Only',
29                              #'Post Authorization',
30                              'Reverse Authorization',
31                              'Void',
32                              'Credit',
33                            ],
34   };
35 }
36
37 # mapping out all possible endpoints
38 # but this version will only be building out "charge", "void", & "credit"
39 my %payment_actions = (
40     'charge' => {
41         path      => '/vsg2/processpayment',
42         process   => 'ProcessPayment',
43         fields    => [qw/
44           Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf3 AccountNumber
45           ExpirationMonth ExpirationYear Cvv
46           CardHolderFirstName CardHolderLastName AvsZip AvsStreet
47           IndustryType ApplicationId Recurring
48         /],
49     },
50     'void' => {
51         path      => '/vsg2/processvoid',
52         process   => 'ProcessVoid',
53         fields    => [qw(
54           Amount Cf1 Cf2 Cf3 AccountNumber
55           ExpirationMonth ExpirationYear ReferenceNumber
56           TransactionDate IndustryType ApplicationId
57         )],
58     },
59     'refund' => {
60         path      => '/vsg2/processrefund',
61         process   => 'ProcessRefund',
62         fields    => [qw(
63           Amount Cf1 Cf2 Cf3 AccountNumber
64           ExpirationMonth ExpirationYear ApplicationId
65         )],
66     },
67     'authorize' => {
68         path      => '/vsg2/processauth',
69         process   => 'ProcessAuth',
70         fields    => [qw(
71           Amount TypeOfSale Cf1 Cf2 Cf3 AccountNumber
72           ExpirationMonth ExpirationYear Cvv
73           CardHolderFirstName CardHolderLastName AvsZip AvsStreet
74           IndustryType ApplicationId
75         )],
76
77     },
78     'authorize_cancel' => {
79         path      => '/vsg2/processauthcancel',
80         process   => 'ProcessAuthCancel',
81         fields    => [qw(
82           Amount AccountNumber
83           ExpirationMonth ExpirationYear
84           ReferenceNumber ResponseCode TransactionDate
85           IndustryType ApplicationId
86         )],
87     },
88     'capture' => {
89         path      => '/vsg2/processcaptureonly',
90     },
91     'create_token' => {
92         path      => '/vsg2/createtoken',
93     },
94     'delete_token' => {
95         path      => '/vsg2/deletetoken',
96     },
97     'query_token' => {
98         path      => '/vsg2/querytoken',
99     },
100     'update_exp_date' => {
101         path      => '/vsg2/updateexpiration',
102     },
103     'update_token' => {
104         path      => '/vsg2/updatetoken',
105     },
106
107 );
108
109 my %action_mapping = (
110     'normal authorization'  => 'charge',
111     'credit'                => 'refund',
112     'authorization only'    => 'authorize',
113     'post authorization'    => 'capture',
114     'reverse authorization' => 'authorize_cancel'
115     # void => void
116 );
117
118 sub set_defaults {
119     my $self = shift;
120     my %options = @_;
121     
122     # inistialize standard B::OP attributes
123     $self->is_success(0);
124     $self->$_( '' ) for qw/authorization
125                            result_code
126                            error_message
127                            server
128                            path
129                            server_response/;
130                            
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
135     
136     $self->build_subs( qw(
137             platform tid appid
138             action reference_number cvv2_response avs_code
139             risk_score txn_amount txn_date partial_auth_amount
140     ) );
141     
142     $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
143     
144     $self->server('svr1.vsecureprocessing.com');
145     
146     $self->tid($options{'tid'});
147     
148     $self->platform($options{'platform'});
149     
150     $self->appid($options{'appid'});
151     
152     $self->port(443);
153     
154 }
155
156 sub clean_content {
157     my ($self,$content) = @_;
158     my %content = $self->content();
159     
160     {
161         no warnings 'uninitialized';
162         
163         # strip non-digits from card number
164         my $card_number = '';
165         if ( $content{card_number} ) {
166             $content{card_number} =~ s/\D//g;
167         }
168         
169         if ($content{'description'} && length($content{'description'}) >20) {
170             $content{'description'} = substr($content{'description'},0,20);
171         }
172         
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);
180         }
181         
182         if (    !$content{'first_name'}
183              || !$content{'last_name'} && $content{'name'}
184            )
185         {
186             ($content{'first_name'}, $content{'last_name'}) =
187               split(' ', $content{'name'}, 2);
188         }
189         
190         if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
191             $content{'street_number'} = $1;
192         }
193     }
194     warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
195     $self->content(%content);
196 }
197
198 sub process_content {
199     my $self = shift;
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'}
205                  );
206     $self->path($payment_actions{ $self->action }{path})
207       unless length($self->path);
208     $self->appid($content{appid}) if (!$self->appid && $content{appid});
209 }
210
211 sub submit {
212     my $self = shift;
213     
214     # inistialize standard B::OP attributes
215     $self->is_success(0);
216     $self->$_( '' ) for qw/authorization
217                            result_code
218                            error_message
219                            server_response/;
220                            
221     # clean and process the $self->content info
222     $self->process_content();
223     my %content = $self->content;
224     my $action = $self->action();
225
226     if ( $self->test_transaction ) {
227       $self->server('dvrotsos2.kattare.com');
228     }
229     
230     my @acceptable_actions = ('charge', 'refund', 'void', 'authorize', 'authorize_cancel');
231     
232     unless ( grep { $action eq $_ } @acceptable_actions ) {
233         croak "'$action' is not supported at this time.";
234     }
235     
236     # fill in the xml vars
237     my $xml_vars = {
238         auth => {
239             Platform    => $self->platform,
240             UserId      => $content{'login'},
241             GID         => $content{'password'},
242             Tid         => $self->tid || '01',
243         },
244         
245         payment => {
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'} : '',
252             Cf3             => '',
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'} : '',
261 #            IndustryType    =>  { 
262 #                                IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
263 #                                IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
264 #                                },
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'} : '',
274         }
275         # we won't be using level2 nor level3.  So I'm leaving them out for now.
276     };
277   
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 /);
289     }
290     
291     # check the requirements are met.
292     my @missing_fields;
293     foreach my $field (@required_fields) {
294         push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
295     }
296     if (scalar(@missing_fields)) {
297         croak "Missing required fields: ".join(', ', @missing_fields);
298     }
299     
300     my $xml_data;
301     my $writer = new XML::Writer( OUTPUT      => \$xml_data,
302                                   DATA_MODE   => 0,
303                                   DATA_INDENT => 0,
304                                   ENCODING    => 'utf-8',
305                                 );
306     $writer->xmlDecl();
307     $writer->startTag('Request');
308     $writer->startTag('MerchantData');
309     foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
310         $writer->dataElement( $key, $xml_vars->{auth}{$key} );
311     }
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}); 
318         }else {
319             $writer->startTag($key);
320             foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
321               $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} ); 
322             }
323             $writer->endTag($key);
324         }
325     }
326     $writer->endTag($payment_actions{ $self->action }{process});
327     $writer->endTag('Request');
328     $writer->end();
329     
330     warn "XML:\n$xml_data\n" if $DEBUG > 2;
331     
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";
338
339     my $content =
340       "--$boundary\n".
341      "Content-Disposition: form-data; name=\"param\"\n\n".
342      $xml_data."\n".
343      "--$boundary--\n";
344
345     # conform to RFC standards
346     $content =~ s/\n/\r\n/gs;
347
348     my ( $page, $server_response, %headers ) =
349       $self->https_post( $opts, $content );
350   
351     # store the server response.
352     $self->server_response($server_response);
353     # parse the result page.
354     $self->parse_response($page);
355
356     if ( $self->is_success && $self->result_code == 10 ) { #partial auth
357
358       if ( $content{'partial_auth'} ) {
359
360         $self->partial_auth_amount( $self->txn_amount );
361
362       } else {
363  
364         #XXX reverse auth if i was an auth only...
365         my $void = new Business::OnlinePayment(
366                          'vSecureProcessing',
367                          map { $_ -> $self->$_() } qw( platform appid tid )
368                    );
369
370         $void->content(
371           'action'           => 'Void',
372           'amount'           => $self->txn_amount,
373           'test_transaction' => $self->test_transaction,
374           'authorization'    => $self->authorization,
375           map { $_ => $content{$_} } qw( login password card_number expiration )
376         );
377       
378         $void->submit;
379
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";
383         } else {
384           $self->is_success(0);
385         }
386
387       }
388
389     }
390     
391     if (!$self->is_success() && !$self->error_message() ) {
392         if ( $DEBUG ) {
393             #additional logging information, possibly too sensitive for an error
394             $self->error_message(
395               "(HTTPS response: ".$server_response.") ".
396               "(HTTPS headers: ".
397             join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
398               "(Raw HTTPS content: ".$page.")"
399             );
400         } else {
401             my $result_code = $self->result_code() || '';
402             if ($result_code) {
403                 $self->error_message(qq|Error code ${result_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
404             }else {
405                 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
406             }
407         }
408     }
409     
410 }
411
412 # read $self->server_response and decipher any errors
413 sub parse_response {
414     my $self = shift;
415     my $page = shift;
416
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
423
424         #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress
425         $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/
426                                 ? $response->{CvvResponse}
427                                 : ''
428                             );
429
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???
434         
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}
442                                     )
443                                 );
444         }
445         
446     } else {
447         die 'Error communicating with vSecureProcessing server (server sent response: '. $self->server_response. ')';
448     }
449     
450 }
451
452 1;
453 __END__
454
455
456 =head1 NAME
457
458 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
459
460 =head1 SYNOPSIS
461
462   use Business::OnlinePayment;
463   my %processor_info = (
464     platform    => 'vsecure_platform',
465     appid       => 'vsecure_appid',
466     tid         => '54', #optional, defaults to 01
467   );
468   my $tx =
469     new Business::OnlinePayment( "vSecureProcessing", %processor_info);
470   $tx->content(
471       login          => 'vsecure@user.id',
472       password       => '12345678901234567890', #vsecure gid
473
474       type           => 'CC',
475       action         => 'Normal Authorization',
476       description    => 'Business::OnlinePayment test',
477       amount         => '49.95',
478       customer_id    => 'tfb',
479       name           => 'Tofu Beast',
480       address        => '123 Anystreet',
481       city           => 'Anywhere',
482       state          => 'UT',
483       zip            => '84058',
484       card_number    => '4007000000027',
485       expiration     => '09/02',
486       cvv2           => '1234', #optional
487   );
488   $tx->submit();
489
490   if($tx->is_success()) {
491       print "Card processed successfully: ".$tx->authorization."\n";
492   } else {
493       print "Card was rejected: ".$tx->error_message."\n";
494   }
495
496 =head1 DESCRIPTION
497
498 For detailed information see L<Business::OnlinePayment>.
499
500 =head1 METHODS AND FUNCTIONS
501
502 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
503
504 =head2 result_code
505
506 Returns the response error code.
507
508 =head2 error_message
509
510 Returns the response error description text.
511
512 =head2 server_response
513
514 Returns the complete response from the server.
515
516 =head1 Handling of content(%content) data:
517
518 =head2 action
519
520 The following actions are valid
521
522   normal authorization
523   credit
524   void
525
526 =head1 Setting vSecureProcessing parameters from content(%content)
527
528 The following rules are applied to map data to vSecureProcessing parameters
529 from content(%content):
530
531       # param => $content{<key>}
532       AccountNumber       => 'card_number',
533       Cvv                 => 'cvv2',
534       ExpirationMonth     => \( $month ), # MM from MM/YY of 'expiration'
535       ExpirationYear      => \( $year ), # YY from MM/YY of 'expiration'
536       Trk1                => 'track1',
537       Trk2                => 'track2',
538       CardHolderFirstName => 'first_name',
539       CardHolderLastName  => 'last_name',
540       Amount              => 'amount'
541       AvsStreet           => 'address',
542       AvsZip              => 'zip',
543       Cf1                 => 'invoice_number',
544       Cf2                 => 'customer_id',
545       IndustryType        => 'IndustryInfo',
546
547 =head1 NOTE
548
549 =head1 COMPATIBILITY
550
551 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
552 Version: 140901 (September 1, 2014).
553
554 See http://www.vsecureprocessing.com/ for more information.
555
556 =head1 AUTHORS
557
558 Original author: Alex Brelsfoard
559
560 Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>
561
562 =head1 COPYRIGHT
563
564 Copyright (c) 2015 Freeside Internet Services, Inc.
565
566 All rights reserved.
567
568 This program is free software; you can redistribute it and/or modify it under
569 the same terms as Perl itself.
570
571 =head1 ADVERTISEMENT
572
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
577 interfaces.
578
579 http://freeside.biz/freeside/
580
581 =head1 SEE ALSO
582
583 perl(1). L<Business::OnlinePayment>.
584
585 =cut
586