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