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