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