Stable working code.
[Business-OnlinePayment-vSecureProcessing.git] / vSecureProcessing.pm
1 package Business::OnlinePayment::vSecureProcessing;
2
3 use strict;
4 use Carp;
5 use Template;
6 use XML::Simple;
7 use Data::Dumper;
8
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11 #use Net::SSLeay qw(post_http post_https make_headers make_form);
12 use vars qw($VERSION $DEBUG @ISA $me);
13
14 @ISA = qw(Business::OnlinePayment::HTTPS);
15 $DEBUG = 0;
16 $VERSION = '0.01';
17 $me = 'Business::OnlinePayment::vSecureProcessing';
18
19
20 # $server: http://dvrotsos2.kattare.com
21
22 # mapping out all possible endpoints
23 # but this version will only be building out "charge", "void", & "credit"
24 my %payment_actions = (
25     'charge' => {
26         path      => '/vsg2/processpayment',
27     },
28     'void' => {
29         path      => '/vsg2/processvoid',
30     },
31     'refund' => {
32         path      => '/vsg2/processrefund',
33     },
34     'authorize' => {
35         path      => '/vsg2/processauth',
36     },
37     'authorize_cancel' => {
38         path      => '/vsg2/processauthcancel',
39     },
40     'capture' => {
41         path      => '/vsg2/processcaptureonly',
42     },
43     'create_token' => {
44         path      => '/vsg2/createtoken',
45     },
46     'delete_token' => {
47         path      => '/vsg2/deletetoken',
48     },
49     'query_token' => {
50         path      => '/vsg2/querytoken',
51     },
52     'update_exp_date' => {
53         path      => '/vsg2/updateexpiration',
54     },
55     'update_token' => {
56         path      => '/vsg2/updatetoken',
57     },
58
59 );
60
61 my %action_mapping = (
62     'normal authorization'  => 'charge',
63     'credit'                => 'refund',
64     'authorization only'    => 'authorize',
65     'post authorization'    => 'capture',
66     'reverse authorization' => 'authorize_cancel'
67     # void => void
68 );
69
70 sub set_defaults {
71     my $self = shift;
72     my %options = @_;
73     
74     # inistialize standard B::OP attributes
75     $self->is_success(0);
76     $self->$_( '' ) for qw/authorization
77                            result_code
78                            error_message
79                            server
80                            port
81                            path
82                            server_response/;
83                            
84     # B::OP creates the following accessors:
85     #     server, port, path, test_transaction, transaction_type,
86     #     server_response, is_success, authorization,
87     #     result_code, error_message,
88     
89     $self->build_subs(qw/
90             env platform userid gid tid appid action cvv_response
91             avs_response risk_score txn_amount txn_date
92     /);
93     
94     $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
95     
96     
97     
98     $self->server($options{'server'});
99     
100     $self->gid($options{'gid'});
101     
102     $self->tid($options{'tid'});
103     
104     $self->platform($options{'platform'});
105     
106     $self->appid($options{'appid'});
107     
108     $self->env((defined($options{'env'})) ? $options{'env'} : 'live'); # 'live'/'test'
109     
110     $self->port(443);
111 }
112
113
114
115 sub clean_content {
116     my ($self,$content) = @_;
117     my %content = $self->content();
118     
119     {
120         no warnings 'uninitialized';
121         
122         # strip non-digits from card number
123         my $card_number = '';
124         if ( $content{card_number} ) {
125             $content{card_number} =~ s/\D//g;
126         }
127         
128         # separate month and year values for expiry_date
129         if ( $content{expiration} ) {
130             ($content{exp_month}, $content{exp_year}) = split /\//, $content{expiration};
131             $content{exp_month} = sprintf "%02d", $content{exp_month};
132             $content{exp_year}  = substr($content{exp_year},0,2) if ($content{exp_year} > 99);
133         }
134         
135         if (!$content{'first_name'} || !$content{'last_name'} && $content{'name'}) {
136             ($content{'first_name'}, $content{'last_name'}) = split(' ', $content{'name'}, 2);
137         }
138         
139         if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
140             $content{'street_number'} = $1;
141         }
142     }
143     warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
144     $self->content(%content);
145 }
146
147 sub process_content {
148     my $self = shift;
149     $self->clean_content();
150     my %content = $self->content();
151     $self->action(($action_mapping{lc $content{'action'}}) ? $action_mapping{lc $content{'action'}} : lc $content{'action'});
152     $self->path($payment_actions{ $self->action }{path})
153       unless length($self->path);
154     $self->appid($content{appid}) if (!$self->appid && $content{appid});
155 }
156
157 sub submit {
158     my $self = shift;
159     
160     # inistialize standard B::OP attributes
161     $self->is_success(0);
162     $self->$_( '' ) for qw/authorization
163                            result_code
164                            error_message
165                            server_response/;
166                            
167     # clean and process the $self->content info
168     $self->process_content();
169     my %content = $self->content;
170     my $action = $self->action();
171     
172     my @acceptable_actions = ('charge', 'refund', 'void');
173     
174     unless ( grep { $action eq $_ } @acceptable_actions ) {
175         croak "'$action' is not supported at this time.";
176     }
177     
178     # fill out the template vars
179     my $template_vars = {
180         
181         auth => {
182             platform    => $self->platform,
183             userid      => $self->userid,
184             gid         => $self->gid,
185             tid         => $self->tid
186         },
187         
188         payment => {
189             amount          => $content{'amount'},
190             track1          => ($content{'track1'}) ? $content{'track1'} : '',
191             track2          => ($content{'track2'}) ? $content{'track2'} : '',
192             type            => ($content{'description'}) ? $content{'description'} : '',
193             cf1             => ($content{'UDField1'}) ? $content{'UDField1'} : '',
194             cf2             => ($content{'UDField2'}) ? $content{'UDField2'} : '',
195             cf3             => '',
196             account_number  => ($content{'card_number'}) ? $content{'card_number'} : '',
197             exp_month       => $content{'exp_month'},
198             exp_year        => $content{'exp_year'},
199             cvv             => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
200             first_name      => ($content{'first_name'}) ? $content{'first_name'} : '',
201             last_name       => ($content{'last_name'}) ? $content{'last_name'} : '',
202             postal_code     => ($content{'zip'}) ? $content{'zip'} : '',
203             street_address  => ($content{'street_number'}) ? $content{'street_number'} : '',
204             industry_type   => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
205             invoice_num     => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
206             appid           => $self->appid(),
207             recurring       => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
208             response_code   => ($content{'response_code'}) ? $content{'response_code'} : '',
209             reference_number=> ($content{'ref_num'}) ? $content{'ref_num'} : '',
210             token           => ($content{'token'}) ? $content{'token'} : '',
211             receipt         => ($content{'receipt'}) ? $content{'receipt'} : '',
212             transaction_date=> ($content{'txn_date'}) ? $content{'txn_date'} : '',
213             merchant_data   => ($content{'merchant_data'}) ? $content{'merchant_data'} : '',
214         },
215         
216         # we won't be using level2 nor level3.  So I'm leaving them blank for now.
217         level2 => {
218             card_type             => '',
219             purchase_code         => '',
220             country_code          => '',
221             ship_tp_postal_code   => '',
222             ship_from_postal_code => '',
223             sales_tax             => '',
224             product_description1  => '',
225             product_description2  => '',
226             product_description3  => '',
227             product_description4  => ''
228         },
229         
230         level3 => {
231             purchase_order_num    => '',
232             order_date            => '',
233             duty_amount           => '',
234             alt_tax_amount        => '',
235             discount_amount       => '',
236             freight_amount        => '',
237             tax_exempt            => '',
238             line_item_count       => '',
239             purchase_items        => $self->_parse_line_items()
240         }
241     };
242     
243   
244     # create the list of required fields based on the action
245     my @required_fields = qw/ amount /;
246     if ($action eq 'charge') {
247         push(@required_fields, $_) foreach (qw/ account_number cvv exp_month exp_year /);
248     }elsif ($action eq 'void') {
249         push(@required_fields, $_) foreach (qw/ reference_number transaction_date /);
250     }elsif ($action eq 'refund') {
251         push(@required_fields, $_) foreach (qw/ amount account_number exp_month exp_year /);
252     }
253     
254     # check the requirements are met.
255     my @missing_fields;
256     foreach my $field (@required_fields) {
257         push(@missing_fields, $field) if (!$template_vars->{payment}{$field});
258     }
259     if (scalar(@missing_fields)) {
260         croak "Missing required fields: ".join(', ', @missing_fields);
261     }
262     
263     # read in the appropriate xml template
264     my $xml_template = _get_xml_template( $action );
265     # create a template object.
266     my $tt = Template->new();
267     # populate the XML template.
268     my $xml_data;
269     $tt->process( \$xml_template, $template_vars, \$xml_data ) || croak $tt->error();
270     
271     warn "XML:\n$xml_data\n" if $DEBUG > 2;
272     
273     my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
274     # opts for B:OP:HTTPS::https_post
275     my $opts = { headers => {}};
276     $opts->{'Content-Type'} =
277     $opts->{headers}->{'Content-Type'} =
278         "multipart/form-data, boundary=$boundary";
279
280     my $content =
281       "--$boundary\n".
282      "Content-Disposition: form-data; name=\"param\"\n\n".
283      $xml_data."\n".
284      "--$boundary--\n";
285
286     # conform to RFC standards
287     $content =~ s/\n/\r\n/gs;
288
289     my ( $page, $server_response, %headers ) = $self->https_post( $opts, $content );
290   
291     # store the server response.
292     $self->server_response($server_response);
293     # parse the result page.
294     $self->parse_response($page);
295     
296     if (!$self->is_success() && !$self->error_message() ) {
297         if ( $DEBUG ) {
298             #additional logging information, possibly too sensitive for an error msg
299             # (vSecureProcessing seems to have a failure mode where they return the full
300             #  original request including card number)
301             $self->error_message(
302               "(HTTPS response: ".$server_response.") ".
303               "(HTTPS headers: ".
304             join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
305               "(Raw HTTPS content: ".$page.")"
306             );
307         } else {
308             my $response_code = $self->response_code() || '';
309             if ($response_code) {
310                 $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
311             }else {
312                 $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
313             }
314         }
315     }
316     
317 }
318
319 # read $self->server_response and decipher any errors
320 sub parse_response {
321     my $self = shift;
322     my $page = shift;
323
324     if ($self->server_response =~ /^200/) {
325         my $response = XMLin($page);
326         warn "RESPONSE: \n".Dumper($response)."\n";
327         $self->result_code($response->{Status});
328         $self->avs_response($response->{AvsResponse});
329         $self->cvv_response($response->{CvvResponse});
330         $self->txn_date($response->{TransactionDate});
331         $self->txn_amount($response->{TransactionAmount} / 100);
332         $self->cvv_response($response->{CvvResponse});
333         $self->is_success($self->result_code() eq '0' ? 1 : 0);
334         if ($self->is_success()) {
335             $self->authorization($response->{AuthIdentificationResponse});
336         }
337         # fill in error_message if there is is an error
338         if ( !$self->is_success && exists($response->{ResultCode})) {
339             $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{ResultCode});
340         }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
341             $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
342         }
343         
344     }else {
345         $self->is_success(0);
346         $self->error_message('Error communicating with vSecureProcessing server');
347         return;
348     }
349     
350     
351 }
352
353 sub _get_xml_template {
354     my $action = shift;
355     
356     my $xml_template = q|<Request >
357     <MerchantData> 
358         <Platform>[% auth.platform %]</Platform>
359         <UserId>[% auth.userid %]</UserId> 
360         <GID>[% auth.gid %]</GID>
361         <Tid>[% auth.tid %]</Tid>
362     </MerchantData>
363     |;
364     
365     if ($action eq 'charge') {
366         $xml_template .= _get_xml_template_charge();
367     }elsif($action eq 'void') {
368         $xml_template .= _get_xml_template_void();
369     }elsif($action eq 'authorize') {
370         $xml_template .= _get_xml_template_auth();
371     }elsif($action eq 'authorize_cancel') {
372         $xml_template .= _get_xml_template_auth_cancel();
373     }elsif($action eq 'refund') {
374         $xml_template .= _get_xml_template_refund();
375     }elsif($action eq 'capture') {
376         $xml_template .= _get_xml_template_capture();
377     }elsif($action eq 'create_token') {
378         $xml_template .= _get_xml_template_create_token();
379     }elsif($action eq 'delete_token') {
380         $xml_template .= _get_xml_template_delete_token();
381     }elsif($action eq 'query_token') {
382         $xml_template .= _get_xml_template_query_token();
383     }elsif($action eq 'update_exp_date') {
384         $xml_template .= _get_xml_template_update_exp_date();
385     }elsif($action eq 'update_token') {
386         $xml_template .= _get_xml_template_update_token();
387     }
388     
389     $xml_template .= "</Request>";
390     $xml_template =~ s/[\n\t\s]*//g;
391     
392     return $xml_template;
393 }
394
395 sub _get_xml_template_charge {
396     my $xml_template = q|<ProcessPayment>
397         <Amount>[% payment.amount %]</Amount>
398         <Trk1>[% payment.track1 %]</Trk1>
399         <Trk2>[% payment.track2 %]</Trk2>
400         <TypeOfSale>[% payment.type %]</TypeOfSale>
401         <Cf1>[% payment.cf1 %]</Cf1>
402         <Cf2>[% payment.cf2 %]</Cf2>
403         <Cf3>[% payment.cf3 %]</Cf3>
404         <AccountNumber>[% payment.account_number %]</AccountNumber>
405         <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
406         <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
407         <Cvv>[% payment.cvv %]</Cvv>
408         <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
409         <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
410         <AvsZip>[% payment.postal_code %]</AvsZip>
411         <AvsStreet>[% payment.street_address %]</AvsStreet>
412         <IndustryType>
413             <IndType >[% payment.industry_type %]</IndType >
414             <IndInvoice>[% payment.invoice_num %]</IndInvoice>
415         </IndustryType>
416         <ApplicationId>[% payment.appid %]</ApplicationId>
417         <Recurring>[% payment.recurring %]</Recurring>
418     </ProcessPayment>|;
419     
420     # other options (that we are not using right now):    
421 #     <Level2PurchaseInfo>
422 #         <Level2CardType>[% level2.card_type %]</Level2CardType >
423 #         <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
424 #         <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
425 #         <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
426 #         <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
427 #         <SalesTax>[% level2.sales_tax %]</SalesTax>
428 #         <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
429 #         <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
430 #         <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
431 #         <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
432 #     </Level2PurchaseInfo>
433 #     <Level3PurchaseInfo>
434 #         <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
435 #         <OrderDate>[% level3.order_date %]</OrderDate>
436 #         <DutyAmount>[% level3.duty_amount %]</DutyAmount>
437 #         <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
438 #         <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
439 #         <FreightAmount>[% level3.freight_amount %]</FreightAmount>
440 #         <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
441 #         <LineItemCount>[% level3.line_item_count %]</LineItemCount>
442 #         <PurchaseItems>
443 #             [% level3.purchase_items %]
444 #         </PurchaseItems>
445 #     </Level3PurchaseInfo>
446
447     return $xml_template;
448 }
449
450 sub _parse_line_items {
451     my $self = shift;
452     my %content = $self->content();
453     
454     return '' if (!$content{'items'});
455     
456     my @line_items;
457     my $template = q|            <LineItem>
458                 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
459                 <ItemCode>[% code %]</ItemCode>
460                 <ItemDescription>[% desc %]</ItemDescription>
461                 <ItemQuantity>[% qty %]</ItemQuantity>
462                 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
463                 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
464                 <ItemAmount>[% amount %]</ItemAmount>
465                 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
466                 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
467                 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
468             </LineItem>|;
469     
470     
471     my @items = $content{'items'};
472     foreach my $item (@items) {
473         # fille in the slots from $template with details in $item
474         # push to @line_items
475     }
476     
477     return join("\n", @line_items);
478 }
479
480 sub _get_xml_template_void {
481     my $xml_template = q|<ProcessVoid>
482         <Amount>[% payment.amount %]</Amount>
483         <AccountNumber>[% payment.account_number %]</AccountNumber>
484         <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
485         <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
486         <ReferenceNumber>[% payment.reference_number %]</ReferenceNumber>
487         <TransactionDate/>
488         <IndustryType1>[% payment.industry_type %]</IndustryType1>
489         <ApplicationId>[% payment.appid %]</ApplicationId>
490     </ProcessVoid>|;
491
492     return $xml_template;
493 }
494
495 sub _get_xml_template_refund {
496     my $xml_template = q|<ProcessRefund>
497         <Amount>[% payment.amount %]</Amount>
498         <AccountNumber>[% payment.account_number %]</AccountNumber>
499         <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
500         <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
501         <ApplicationId>[% payment.appid %]</ApplicationId>
502     </ProcessRefund>|;
503
504     return $xml_template;
505 }
506
507 sub _get_xml_template_auth {
508     my $xml_template = '';
509
510     return $xml_template;
511 }
512
513 sub _get_xml_template_auth_cancel {
514     my $xml_template = '';
515
516     return $xml_template;
517 }
518
519 sub _get_xml_template_capture {
520     my $xml_template = '';
521
522     return $xml_template;
523 }
524
525 sub _get_xml_template_create_token {
526     my $xml_template = '';
527
528     return $xml_template;
529 }
530
531 sub _get_xml_template_delete_token {
532     my $xml_template = '';
533
534     return $xml_template;
535 }
536
537 sub _get_xml_template_query_token {
538     my $xml_template = '';
539
540     return $xml_template;
541 }
542
543 sub _get_xml_template_update_exp_date {
544     my $xml_template = '';
545
546     return $xml_template;
547 }
548
549 sub _get_xml_template_update_token {
550     my $xml_template = '';
551
552     return $xml_template;
553 }
554
555
556 1;
557 __END__
558
559
560 =head1 NAME
561
562 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
563
564 =head1 SYNOPSIS
565
566   use Business::OnlinePayment;
567   my %processor_info = (
568     platform    => '####',
569     gid         => 12345678901234567890,
570     tid         => 01,
571     user_id     => '####',
572     url         => 'www.####.com'
573   );
574   my $tx =
575     new Business::OnlinePayment( "vSecureProcessing", %processor_info);
576   $tx->content(
577       appid          => '######',
578       type           => 'VISA',
579       action         => 'Normal Authorization',
580       description    => 'Business::OnlinePayment test',
581       amount         => '49.95',
582       customer_id    => 'tfb',
583       name           => 'Tofu Beast',
584       address        => '123 Anystreet',
585       city           => 'Anywhere',
586       state          => 'UT',
587       zip            => '84058',
588       card_number    => '4007000000027',
589       expiration     => '09/02',
590       cvv2           => '1234', #optional
591   );
592   $tx->submit();
593
594   if($tx->is_success()) {
595       print "Card processed successfully: ".$tx->authorization."\n";
596   } else {
597       print "Card was rejected: ".$tx->error_message."\n";
598   }
599
600 =head1 DESCRIPTION
601
602 For detailed information see L<Business::OnlinePayment>.
603
604 =head1 METHODS AND FUNCTIONS
605
606 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
607
608 =head2 result_code
609
610 Returns the response error code.
611
612 =head2 error_message
613
614 Returns the response error description text.
615
616 =head2 server_response
617
618 Returns the complete response from the server.
619
620 =head1 Handling of content(%content) data:
621
622 =head2 action
623
624 The following actions are valid
625
626   normal authorization
627   credit
628   void
629
630 =head1 Setting vSecureProcessing parameters from content(%content)
631
632 The following rules are applied to map data to vSecureProcessing parameters
633 from content(%content):
634
635       # param => $content{<key>}
636       AccountNumber       => 'card_number',
637       Cvv                 => 'cvv2',
638       ExpirationMonth     => \( $month ), # MM from MM/YY of 'expiration'
639       ExpirationYear      => \( $year ), # YY from MM/YY of 'expiration'
640       Trk1                => 'track1',
641       Trk2                => 'track2',
642       CardHolderFirstName => 'first_name',
643       CardHolderLastName  => 'last_name',
644       Amount              => 'amount'
645       AvsStreet           => 'address',
646       AvsZip              => 'zip',
647       Cf1                 => 'UDField1',
648       Cf2                 => 'UDField2',
649       IndustryType        => 'IndustryInfo',
650
651 =head1 NOTE
652
653 =head1 COMPATIBILITY
654
655 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
656
657 See http://www.vsecureprocessing.com/ for more information.
658
659 =head1 AUTHORS
660
661 Original author: Alex Brelsfoard
662
663 Current maintainer: Alex Brelsfoard
664
665 =head1 COPYRIGHT
666
667 Copyright (c) 2015 Freeside Internet Services, Inc.
668
669 All rights reserved.
670
671 This program is free software; you can redistribute it and/or modify it under
672 the same terms as Perl itself.
673
674 =head1 ADVERTISEMENT
675
676 Need a complete, open-source back-office and customer self-service solution?
677 The Freeside software includes support for credit card and electronic check
678 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
679 trouble ticketing, and customer signup and self-service web interfaces.
680
681 http://freeside.biz/freeside/
682
683 =head1 SEE ALSO
684
685 perl(1). L<Business::OnlinePayment>.
686
687 =cut
688
689