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