Getting code ready for CPAN and debian relase.
[Business-OnlinePayment-vSecureProcessing.git] / lib / Business / OnlinePayment / 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 reference_number cvv_response
91             avs_response risk_score txn_amount txn_date response_code
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         $self->result_code($response->{Status}); # 0 /1
327         $self->response_code($response->{ResponseCode}); # see documentation for translation
328         $self->avs_response($response->{AvsResponse}); # Y / N
329         $self->cvv_response($response->{CvvResponse}); # P / F
330         $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
331         $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
332         $self->reference_number($response->{ReferenceNumber});
333         
334         $self->is_success($self->result_code() eq '0' ? 1 : 0);
335         if ($self->is_success()) {
336             $self->authorization($response->{AuthIdentificationResponse});
337         }
338         # fill in error_message if there is is an error
339         if ( !$self->is_success && exists($response->{AdditionalResponseData})) {
340             $self->error_message('Error '.$response->{ResponseCode}.': '.$response->{AdditionalResponseData});
341         }elsif ( !$self->is_success && exists($response->{Receipt}) ) {
342             $self->error_message('Error '.$response->{ResponseCode}.': '.(exists($response->{Receipt})) ? $response->{Receipt} : '');
343         }
344         
345     }else {
346         $self->is_success(0);
347         $self->error_message('Error communicating with vSecureProcessing server');
348         return;
349     }
350     
351     
352 }
353
354 sub _get_xml_template {
355     my $action = shift;
356     
357     my $xml_template = q|<Request >
358     <MerchantData> 
359         <Platform>[% auth.platform %]</Platform>
360         <UserId>[% auth.userid %]</UserId> 
361         <GID>[% auth.gid %]</GID>
362         <Tid>[% auth.tid %]</Tid>
363     </MerchantData>
364     |;
365     
366     if ($action eq 'charge') {
367         $xml_template .= _get_xml_template_charge();
368     }elsif($action eq 'void') {
369         $xml_template .= _get_xml_template_void();
370     }elsif($action eq 'authorize') {
371         $xml_template .= _get_xml_template_auth();
372     }elsif($action eq 'authorize_cancel') {
373         $xml_template .= _get_xml_template_auth_cancel();
374     }elsif($action eq 'refund') {
375         $xml_template .= _get_xml_template_refund();
376     }elsif($action eq 'capture') {
377         $xml_template .= _get_xml_template_capture();
378     }elsif($action eq 'create_token') {
379         $xml_template .= _get_xml_template_create_token();
380     }elsif($action eq 'delete_token') {
381         $xml_template .= _get_xml_template_delete_token();
382     }elsif($action eq 'query_token') {
383         $xml_template .= _get_xml_template_query_token();
384     }elsif($action eq 'update_exp_date') {
385         $xml_template .= _get_xml_template_update_exp_date();
386     }elsif($action eq 'update_token') {
387         $xml_template .= _get_xml_template_update_token();
388     }
389     
390     $xml_template .= "</Request>";
391     $xml_template =~ s/[\n\t\s]*//g;
392     
393     return $xml_template;
394 }
395
396 sub _get_xml_template_charge {
397     my $xml_template = q|<ProcessPayment>
398         <Amount>[% payment.amount %]</Amount>
399         <Trk1>[% payment.track1 %]</Trk1>
400         <Trk2>[% payment.track2 %]</Trk2>
401         <TypeOfSale>[% payment.type %]</TypeOfSale>
402         <Cf1>[% payment.cf1 %]</Cf1>
403         <Cf2>[% payment.cf2 %]</Cf2>
404         <Cf3>[% payment.cf3 %]</Cf3>
405         <AccountNumber>[% payment.account_number %]</AccountNumber>
406         <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
407         <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
408         <Cvv>[% payment.cvv %]</Cvv>
409         <CardHolderFirstName>[% payment.first_name %]</CardHolderFirstName>
410         <CardHolderLastName>[% payment.last_name %]</CardHolderLastName>
411         <AvsZip>[% payment.postal_code %]</AvsZip>
412         <AvsStreet>[% payment.street_address %]</AvsStreet>
413         <IndustryType>
414             <IndType >[% payment.industry_type %]</IndType >
415             <IndInvoice>[% payment.invoice_num %]</IndInvoice>
416         </IndustryType>
417         <ApplicationId>[% payment.appid %]</ApplicationId>
418         <Recurring>[% payment.recurring %]</Recurring>
419     </ProcessPayment>|;
420     
421     # other options (that we are not using right now):    
422 #     <Level2PurchaseInfo>
423 #         <Level2CardType>[% level2.card_type %]</Level2CardType >
424 #         <PurchaseCode>[% level2.purchase_code %]</PurchaseCode>
425 #         <ShipToCountryCode>[% level2.country_code %]</ShipToCountryCode>
426 #         <ShipToPostalCode>[% level2.ship_tp_postal_code %]</ShipToPostalCode>
427 #         <ShipFromPostalCode>[% level2.ship_from_postal_code %]</ShipFromPostalCode>
428 #         <SalesTax>[% level2.sales_tax %]</SalesTax>
429 #         <ProductDescription1>[% level2.product_description1 %]</ProductDescription1>
430 #         <ProductDescription2>[% level2.product_description2 %]</ProductDescription2>
431 #         <ProductDescription3>[% level2.product_description3 %]</ProductDescription3>
432 #         <ProductDescription4>[% level2.product_description4 %]</ProductDescription4>
433 #     </Level2PurchaseInfo>
434 #     <Level3PurchaseInfo>
435 #         <PurchaseOrderNumber>[% level3.purchase_order_num %]</PurchaseOrderNumber>
436 #         <OrderDate>[% level3.order_date %]</OrderDate>
437 #         <DutyAmount>[% level3.duty_amount %]</DutyAmount>
438 #         <AlternateTaxAmount>[% level3.alt_tax_amount %]</AlternateTaxAmount>
439 #         <DiscountAmount>[% level3.discount_amount %]</DiscountAmount>
440 #         <FreightAmount>[% level3.freight_amount %]</FreightAmount>
441 #         <TaxExemptFlag>[% level3.tax_exempt %]</TaxExemptFlag>
442 #         <LineItemCount>[% level3.line_item_count %]</LineItemCount>
443 #         <PurchaseItems>
444 #             [% level3.purchase_items %]
445 #         </PurchaseItems>
446 #     </Level3PurchaseInfo>
447
448     return $xml_template;
449 }
450
451 sub _parse_line_items {
452     my $self = shift;
453     my %content = $self->content();
454     
455     return '' if (!$content{'items'});
456     
457     my @line_items;
458     my $template = q|            <LineItem>
459                 <ItemSequenceNumber>[% seq_num %]</ItemSequenceNumber>
460                 <ItemCode>[% code %]</ItemCode>
461                 <ItemDescription>[% desc %]</ItemDescription>
462                 <ItemQuantity>[% qty %]</ItemQuantity>
463                 <ItemUnitOfMeasure>[% unit %]</ItemUnitOfMeasure>
464                 <ItemUnitCost>[% unit_cost %]</ItemUnitCost>
465                 <ItemAmount>[% amount %]</ItemAmount>
466                 <ItemDiscountAmount>[% discount_amount %]</ItemDiscountAmount>
467                 <ItemTaxAmount>[% tax_amount %]</ItemTaxAmount>
468                 <ItemTaxRate>[% tax_rate %]</ItemTaxRate>
469             </LineItem>|;
470     
471     
472     my @items = $content{'items'};
473     foreach my $item (@items) {
474         # fille in the slots from $template with details in $item
475         # push to @line_items
476     }
477     
478     return join("\n", @line_items);
479 }
480
481 sub _get_xml_template_void {
482     my $xml_template = q|<ProcessVoid>
483         <Amount>[% payment.amount %]</Amount>
484         <AccountNumber>[% payment.account_number %]</AccountNumber>
485         <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
486         <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
487         <ReferenceNumber>[% payment.reference_number %]</ReferenceNumber>
488         <TransactionDate/>
489         <IndustryType1>[% payment.industry_type %]</IndustryType1>
490         <ApplicationId>[% payment.appid %]</ApplicationId>
491     </ProcessVoid>|;
492
493     return $xml_template;
494 }
495
496 sub _get_xml_template_refund {
497     my $xml_template = q|<ProcessRefund>
498         <Amount>[% payment.amount %]</Amount>
499         <AccountNumber>[% payment.account_number %]</AccountNumber>
500         <ExpirationMonth>[% payment.exp_month %]</ExpirationMonth>
501         <ExpirationYear>[% payment.exp_year %]</ExpirationYear>
502         <ApplicationId>[% payment.appid %]</ApplicationId>
503     </ProcessRefund>|;
504
505     return $xml_template;
506 }
507
508 sub _get_xml_template_auth {
509     my $xml_template = '';
510
511     return $xml_template;
512 }
513
514 sub _get_xml_template_auth_cancel {
515     my $xml_template = '';
516
517     return $xml_template;
518 }
519
520 sub _get_xml_template_capture {
521     my $xml_template = '';
522
523     return $xml_template;
524 }
525
526 sub _get_xml_template_create_token {
527     my $xml_template = '';
528
529     return $xml_template;
530 }
531
532 sub _get_xml_template_delete_token {
533     my $xml_template = '';
534
535     return $xml_template;
536 }
537
538 sub _get_xml_template_query_token {
539     my $xml_template = '';
540
541     return $xml_template;
542 }
543
544 sub _get_xml_template_update_exp_date {
545     my $xml_template = '';
546
547     return $xml_template;
548 }
549
550 sub _get_xml_template_update_token {
551     my $xml_template = '';
552
553     return $xml_template;
554 }
555
556
557 1;
558 __END__
559
560
561 =head1 NAME
562
563 Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment
564
565 =head1 SYNOPSIS
566
567   use Business::OnlinePayment;
568   my %processor_info = (
569     platform    => '####',
570     gid         => 12345678901234567890,
571     tid         => 01,
572     user_id     => '####',
573     url         => 'www.####.com'
574   );
575   my $tx =
576     new Business::OnlinePayment( "vSecureProcessing", %processor_info);
577   $tx->content(
578       appid          => '######',
579       type           => 'VISA',
580       action         => 'Normal Authorization',
581       description    => 'Business::OnlinePayment test',
582       amount         => '49.95',
583       customer_id    => 'tfb',
584       name           => 'Tofu Beast',
585       address        => '123 Anystreet',
586       city           => 'Anywhere',
587       state          => 'UT',
588       zip            => '84058',
589       card_number    => '4007000000027',
590       expiration     => '09/02',
591       cvv2           => '1234', #optional
592   );
593   $tx->submit();
594
595   if($tx->is_success()) {
596       print "Card processed successfully: ".$tx->authorization."\n";
597   } else {
598       print "Card was rejected: ".$tx->error_message."\n";
599   }
600
601 =head1 DESCRIPTION
602
603 For detailed information see L<Business::OnlinePayment>.
604
605 =head1 METHODS AND FUNCTIONS
606
607 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
608
609 =head2 result_code
610
611 Returns the response error code.
612
613 =head2 error_message
614
615 Returns the response error description text.
616
617 =head2 server_response
618
619 Returns the complete response from the server.
620
621 =head1 Handling of content(%content) data:
622
623 =head2 action
624
625 The following actions are valid
626
627   normal authorization
628   credit
629   void
630
631 =head1 Setting vSecureProcessing parameters from content(%content)
632
633 The following rules are applied to map data to vSecureProcessing parameters
634 from content(%content):
635
636       # param => $content{<key>}
637       AccountNumber       => 'card_number',
638       Cvv                 => 'cvv2',
639       ExpirationMonth     => \( $month ), # MM from MM/YY of 'expiration'
640       ExpirationYear      => \( $year ), # YY from MM/YY of 'expiration'
641       Trk1                => 'track1',
642       Trk2                => 'track2',
643       CardHolderFirstName => 'first_name',
644       CardHolderLastName  => 'last_name',
645       Amount              => 'amount'
646       AvsStreet           => 'address',
647       AvsZip              => 'zip',
648       Cf1                 => 'UDField1',
649       Cf2                 => 'UDField2',
650       IndustryType        => 'IndustryInfo',
651
652 =head1 NOTE
653
654 =head1 COMPATIBILITY
655
656 Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document Version: 140901 (September 1, 2014).
657
658 See http://www.vsecureprocessing.com/ for more information.
659
660 =head1 AUTHORS
661
662 Original author: Alex Brelsfoard
663
664 Current maintainer: Alex Brelsfoard
665
666 =head1 COPYRIGHT
667
668 Copyright (c) 2015 Freeside Internet Services, Inc.
669
670 All rights reserved.
671
672 This program is free software; you can redistribute it and/or modify it under
673 the same terms as Perl itself.
674
675 =head1 ADVERTISEMENT
676
677 Need a complete, open-source back-office and customer self-service solution?
678 The Freeside software includes support for credit card and electronic check
679 processing with vSecureProcessing and over 50 other gateways, invoicing, integrated
680 trouble ticketing, and customer signup and self-service web interfaces.
681
682 http://freeside.biz/freeside/
683
684 =head1 SEE ALSO
685
686 perl(1). L<Business::OnlinePayment>.
687
688 =cut
689
690