File Coverage

blib/lib/Business/OnlinePayment/Litle.pm
Criterion Covered Total %
statement 260 761 34.1
branch 84 328 25.6
condition 26 173 15.0
subroutine 34 54 62.9
pod 25 25 100.0
total 429 1341 31.9


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::Litle;
2              
3              
4 4     4   29124 use warnings;
  4         9  
  4         117  
5 4     4   20 use strict;
  4         7  
  4         67  
6              
7 4     4   438 use Business::OnlinePayment;
  4         2547  
  4         78  
8 4     4   1570 use Business::OnlinePayment::HTTPS;
  4         69079  
  4         163  
9 4     4   1817 use Business::OnlinePayment::Litle::ErrorCodes '%ERRORS';
  4         14  
  4         401  
10 4     4   29 use vars qw(@ISA $me $DEBUG);
  4         9  
  4         203  
11 4     4   1687 use MIME::Base64;
  4         1995  
  4         198  
12 4     4   2306 use HTTP::Tiny;
  4         128420  
  4         137  
13 4     4   2142 use XML::Writer;
  4         21593  
  4         109  
14 4     4   2711 use XML::Simple;
  4         25978  
  4         31  
15 4     4   282 use Tie::IxHash;
  4         11  
  4         103  
16 4     4   1825 use Business::CreditCard qw(cardtype);
  4         5704  
  4         240  
17 4     4   1574 use Data::Dumper;
  4         13806  
  4         219  
18 4     4   1740 use IO::String;
  4         7383  
  4         123  
19 4     4   78 use Carp qw(croak);
  4         31  
  4         216  
20 4     4   1408 use Log::Scrubber qw(disable $SCRUBBER scrubber :Carp scrubber_add_scrubber);
  4         10749  
  4         25  
21              
22             @ISA = qw(Business::OnlinePayment::HTTPS);
23             $me = 'Business::OnlinePayment::Litle';
24             $DEBUG = 0;
25             our $VERSION = '0.957'; # VERSION
26              
27             # PODNAME: Business::OnlinePayment::Litle
28              
29             # ABSTRACT: Business::OnlinePayment::Litle - Vantiv (was Litle & Co.) Backend for Business::OnlinePayment
30              
31              
32             sub server_request {
33 11     11 1 43 my ( $self, $val, $tf ) = @_;
34 11 100       127 if ($val) {
35 5         36 $self->{server_request} = scrubber $val;
36 5 50       709 $self->server_request_dangerous($val,1) unless $tf;
37             }
38 11         29 return $self->{server_request};
39             }
40              
41              
42             sub server_request_dangerous {
43 5     5 1 18 my ( $self, $val, $tf ) = @_;
44 5 50       17 if ($val) {
45 5         12 $self->{server_request_dangerous} = $val;
46 5 50       20 $self->server_request($val,1) unless $tf;
47             }
48 5         13 return $self->{server_request_dangerous};
49             }
50              
51              
52             sub server_response {
53 11     11 1 39 my ( $self, $val, $tf ) = @_;
54 11 100       43 if ($val) {
55 5         28 $self->{server_response} = scrubber $val;
56 5 50       589 $self->server_response_dangerous($val,1) unless $tf;
57             }
58 11         28 return $self->{server_response};
59             }
60              
61              
62             sub server_response_dangerous {
63 5     5 1 19 my ( $self, $val, $tf ) = @_;
64 5 50       19 if ($val) {
65 5         17 $self->{server_response_dangerous} = $val;
66 5 50       22 $self->server_response($val,1) unless $tf;
67             }
68 5         34 return $self->{server_response_dangerous};
69             }
70              
71              
72              
73             sub _info {
74             return {
75 0     0   0 info_compat => '0.01',
76             gateway_name => 'Litle',
77             gateway_url => 'http://www.vantiv.com',
78             module_version => $VERSION,
79             supported_types => ['CC'],
80             supported_actions => {
81             CC => [
82             'Normal Authorization',
83             'Post Authorization',
84             'Authorization Only',
85             'Credit',
86             'Void',
87             'Auth Reversal',
88             ],
89             },
90             };
91             }
92              
93              
94             sub set_defaults {
95 8     8 1 6987 my $self = shift;
96 8         29 my %opts = @_;
97              
98 8         48 $self->build_subs(
99             qw( order_number md5 avs_code cvv2_response card_token
100             cavv_response api_version xmlns failure_status batch_api_version chargeback_api_version
101             is_prepaid prepaid_balance get_affluence chargeback_server chargeback_port chargeback_path
102             verify_SSL phoenixTxnId is_duplicate card_token card_token_response card_token_message
103             )
104             );
105              
106 8         3235 $self->test_transaction(0);
107              
108 8 50       27 if ( $opts{debug} ) {
109 0         0 $self->debug( $opts{debug} );
110 0         0 delete $opts{debug};
111             }
112              
113             ## load in the defaults
114 8         19 my %_defaults = ();
115 8         25 foreach my $key ( keys %opts ) {
116 8 50       35 $key =~ /^default_(\w*)$/ or next;
117 8         31 $_defaults{$1} = $opts{$key};
118 8         21 delete $opts{$key};
119             }
120              
121 8         31 $self->{_scrubber} = \&_default_scrubber;
122 8 100       24 if( defined $_defaults{'Scrubber'} ) {
123 2         5 my $code = $_defaults{'Scrubber'};
124 2 100       6 if( ref($code) ne 'CODE' ) {
125 1         33 warn('default_Scrubber is not a code ref');
126             }
127             else {
128 1         2 $self->{_scrubber} = $code;
129             }
130             }
131              
132 8 50       140 $self->api_version('8.1') unless $self->api_version;
133 8 50       332 $self->batch_api_version('8.1') unless $self->batch_api_version;
134 8 50       346 $self->chargeback_api_version('2.2') unless $self->chargeback_api_version;
135 8 50       358 $self->xmlns('http://www.litle.com/schema') unless $self->xmlns;
136             }
137              
138              
139             sub test_transaction {
140 15     15 1 11249 my $self = shift;
141 15         29 my $testMode = shift;
142 15 50 0     48 if (! defined $testMode) { $testMode = $self->{'test_transaction'} || 0; }
  0         0  
143              
144 15 100       86 if (lc($testMode) eq 'sandbox') {
    50          
    50          
    100          
145 6         17 $self->{'test_transaction'} = 'sandbox';
146 6         124 $self->verify_SSL(0);
147              
148 6         150 $self->server('www.testlitle.com');
149 6         174 $self->port('443');
150 6         143 $self->path('/sandbox/communicator/online');
151              
152 6         155 $self->chargeback_server('service-postlive.litle.com'); # no sandbox exists, so fallback to certify
153 6         148 $self->chargeback_port('443');
154 6         133 $self->chargeback_path('/services/communicator/chargebacks/webCommunicator');
155             } elsif (lc($testMode) eq 'localhost') {
156             # this allows the user to create a local web server to do generic testing with
157 0         0 $self->{'test_transaction'} = 'localhost';
158 0         0 $self->verify_SSL(0);
159              
160 0         0 $self->server('localhost');
161 0         0 $self->port('443');
162 0         0 $self->path('/sandbox/communicator/online');
163              
164 0         0 $self->chargeback_server('localhost');
165 0         0 $self->chargeback_port('443');
166 0         0 $self->chargeback_path('/services/communicator/chargebacks/webCommunicator');
167             } elsif (lc($testMode) eq 'prelive') {
168 0         0 $self->{'test_transaction'} = $testMode;
169 0         0 $self->verify_SSL(0);
170              
171 0         0 $self->server('payments.vantivprelive.com');
172 0         0 $self->port('443');
173 0         0 $self->path('/vap/communicator/online');
174              
175 0         0 $self->chargeback_server('services.vantivprelive.com');
176 0         0 $self->chargeback_port('443');
177 0         0 $self->chargeback_path('/services/communicator/chargebacks/webCommunicator');
178             } elsif ($testMode) {
179 1         6 $self->{'test_transaction'} = $testMode;
180 1         18 $self->verify_SSL(0);
181              
182 1         23 $self->server('payments.vantivpostlive.com');
183 1         23 $self->port('443');
184 1         22 $self->path('/vap/communicator/online');
185              
186 1         22 $self->chargeback_server('services.vantivpostlive.com');
187 1         22 $self->chargeback_port('443');
188 1         28 $self->chargeback_path('/services/communicator/chargebacks/webCommunicator');
189             } else {
190 8         35 $self->{'test_transaction'} = 0;
191 8         190 $self->verify_SSL(1);
192              
193 8         223 $self->server('payments.vantivcnp.com');
194 8         200 $self->port('443');
195 8         490 $self->path('/vap/communicator/online');
196              
197 8         179 $self->chargeback_server('services.vantivcnp.com');
198 8         172 $self->chargeback_port('443');
199 8         176 $self->chargeback_path('/services/communicator/chargebacks/webCommunicator');
200             }
201              
202 15         133 return $self->{'test_transaction'};
203             }
204              
205              
206             sub map_fields {
207 6     6 1 16 my ( $self, $content ) = @_;
208              
209 6         16 my $action = lc( $content->{'action'} );
210 6         45 my %actions = (
211             'normal authorization' => 'sale',
212             'authorization only' => 'authorization',
213             'post authorization' => 'capture',
214             'void' => 'void',
215             'credit' => 'credit',
216             'auth reversal' => 'authReversal',
217             'account update' => 'accountUpdate',
218             'tokenize' => 'registerTokenRequest',
219              
220             # AVS ONLY
221             # Capture Given
222             # Force Capture
223             #
224             );
225 6   33     29 $content->{'TransactionType'} = $actions{$action} || $action;
226              
227 6         43 my $type_translate = {
228             'VISA card' => 'VI',
229             'MasterCard' => 'MC',
230             'Discover card' => 'DI',
231             'American Express card' => 'AX',
232             'Diner\'s Club/Carte Blanche' => 'DI',
233             'JCB' => 'DI',
234             'China Union Pay' => 'DI',
235             };
236              
237             $content->{'card_type'} =
238             $type_translate->{ cardtype( $content->{'card_number'} ) }
239 6 100 33     35 || $content->{'type'} if $content->{'card_number'};
240              
241 6 50 33     165 if ( $content->{recurring_billing}
242             && $content->{recurring_billing} eq 'YES' )
243             {
244 0         0 $content->{'orderSource'} = 'recurring';
245             }
246             else {
247 6         16 $content->{'orderSource'} = 'ecommerce';
248             }
249             $content->{'customerType'} =
250 6 50       20 $content->{'orderSource'} eq 'recurring'
251             ? 'Existing'
252             : 'New'; # new/Existing
253              
254 6         15 $content->{'deliverytype'} = 'SVC';
255              
256             # stuff it back into %content
257 6 50 33     43 if ( $content->{'products'} && ref( $content->{'products'} ) eq 'ARRAY' ) {
258 6         12 my $count = 1;
259 6         13 foreach ( @{ $content->{'products'} } ) {
  6         18  
260 12         28 $_->{'itemSequenceNumber'} = $count++;
261             }
262             }
263              
264 6 50 0     26 if( $content->{'velocity_check'} && (
      33        
265             $content->{'velocity_check'} != 0
266             && $content->{'velocity_check'} !~ m/false/i ) ) {
267 0         0 $content->{'velocity_check'} = 'true';
268             } else {
269 6         15 $content->{'velocity_check'} = 'false';
270             }
271              
272 6 50 0     21 if( $content->{'partial_auth'} && (
      33        
273             $content->{'partial_auth'} != 0
274             && $content->{'partial_auth'} !~ m/false/i ) ) {
275 0         0 $content->{'partial_auth'} = 'true';
276             } else {
277 6         13 $content->{'partial_auth'} = 'false';
278             }
279              
280 6         12 $self->content( %{$content} );
  6         49  
281 6         424 return $content;
282             }
283              
284              
285             sub format_misc_field {
286 288     288 1 511 my ($self, $content, $trunc) = @_;
287              
288 288 100       663 if( defined $content->{ $trunc->[0] } ) {
    50          
289 232         636 utf8::upgrade($content->{ $trunc->[0] });
290 232         482 my $len = length( $content->{ $trunc->[0] } );
291 232 50 100     1291 if ( $trunc->[3] && $trunc->[2] && $len != 0 && $len < $trunc->[2] ) {
    50 100        
      66        
      66        
      33        
292             # Zero is a valid length (mostly for cvv2 value)
293 0         0 croak "$trunc->[0] has too few characters";
294             }
295             elsif ( $trunc->[3] && $trunc->[1] && $len > $trunc->[1] ) {
296 0         0 croak "$trunc->[0] has too many characters";
297             }
298 232         657 $content->{ $trunc->[0] } = substr($content->{ $trunc->[0] } , 0, $trunc->[1] );
299             #warn "$trunc->[0] => $len => $content->{ $trunc->[0] }\n" if $DEBUG;
300             }
301             elsif ( $trunc->[4] ) {
302 0         0 croak "$trunc->[0] is required";
303             }
304             }
305              
306              
307             sub format_amount_field {
308 78     78 1 147 my ($self, $data, $field) = @_;
309 78 100       198 if (defined ( $data->{$field} ) ) {
310 54         324 $data->{$field} = sprintf( "%.2f", $data->{$field} );
311 54         207 $data->{$field} =~ s/\.//g;
312             }
313             }
314              
315              
316             sub format_phone_field {
317 6     6 1 15 my ($self, $data, $field) = @_;
318 6 50       21 if (defined ( $data->{$field} ) ) {
319 6         101 my $convertPhone = {
320             'a' => 2, 'b' => 2, 'c' => 2,
321             'd' => 3, 'e' => 3, 'f' => 3,
322             'g' => 4, 'h' => 4, 'i' => 4,
323             'j' => 5, 'k' => 5, 'l' => 5,
324             'm' => 6, 'n' => 6, 'o' => 6,
325             'p' => 7, 'q' => 7, 'r' => 7, 's' => 7,
326             't' => 8, 'u' => 8, 'v' => 8,
327             'w' => 9, 'x' => 9, 'y' => 9, 'z' => 9,
328             };
329 6 50       27 $data->{$field} =~ s/(\D)/$$convertPhone{lc($1)}||''/eg;
  12         118  
330             }
331             }
332              
333              
334             sub map_request {
335 6     6 1 17 my ( $self, $content ) = @_;
336              
337 6         27 $self->map_fields($content);
338              
339 6         15 my $action = $content->{'TransactionType'};
340              
341 6         19 my @required_fields = qw(action type);
342              
343 6         46 $self->required_fields(@required_fields);
344              
345             # for tabbing
346             # set dollar amounts to the required format (eg $5.00 should be 500)
347 6         253 foreach my $field ( 'amount', 'salesTax', 'discountAmount', 'shippingAmount', 'dutyAmount' ) {
348 30         72 $self->format_amount_field($content, $field);
349             }
350              
351             # make sure the date is in MMYY format
352 6         53 $content->{'expiration'} =~ s/^(\d{1,2})\D*\d*?(\d{2})$/$1$2/;
353              
354 6 50       33 if ( ! defined $content->{'description'} ) { $content->{'description'} = ''; } # schema req
  0         0  
355 6         27 $content->{'description'} =~ s/[^\w\s\*\,\-\'\#\&\.]//g;
356              
357             # Litle pre 0.934 used token, however BOP likes card_token
358 6 50 66     36 $content->{'card_token'} = $content->{'token'} if ! defined $content->{'card_token'} && defined $content->{'card_token'};
359              
360             # only numbers are allowed in company_phone
361 6         31 $self->format_phone_field($content, 'company_phone');
362              
363 6   33     47 $content->{'invoice_number_length_15'} ||= $content->{'invoice_number'}; # orderId = 25, invoiceReferenceNumber = 15
364              
365             # put in a list of constraints
366 6         151 my @validate = (
367             # field, maxLen, minLen, errorOnLength, isRequired
368             [ 'name', 100, 0, 0, 0 ],
369             [ 'email', 100, 0, 0, 0 ],
370             [ 'address', 35, 0, 0, 0 ],
371             [ 'city', 35, 0, 0, 0 ],
372             [ 'state', 30, 0, 0, 0 ], # 30 is allowed, but it should be the 2 char code
373             [ 'zip', 20, 0, 0, 0 ],
374             [ 'country', 3, 0, 0, 0 ], # should use iso 3166-1 2 char code
375             [ 'phone', 20, 0, 0, 0 ],
376              
377             [ 'ship_name', 100, 0, 0, 0 ],
378             [ 'ship_email', 100, 0, 0, 0 ],
379             [ 'ship_address',35, 0, 0, 0 ],
380             [ 'ship_city', 35, 0, 0, 0 ],
381             [ 'ship_state', 30, 0, 0, 0 ], # 30 is allowed, but it should be the 2 char code
382             [ 'ship_zip', 20, 0, 0, 0 ],
383             [ 'ship_country', 3, 0, 0, 0 ], # should use iso 3166-1 2 char code
384             [ 'ship_phone', 20, 0, 0, 0 ],
385              
386             #[ 'customerType',13, 0, 0, 0 ],
387              
388             ['company_phone',13, 0, 0, 0 ],
389             [ 'description', 25, 0, 0, 0 ],
390              
391             [ 'po_number', 17, 0, 0, 0 ],
392             [ 'salestax', 8, 0, 1, 0 ],
393             [ 'discount', 8, 0, 1, 0 ],
394             [ 'shipping', 8, 0, 1, 0 ],
395             [ 'duty', 8, 0, 1, 0 ],
396             ['invoice_number',25, 0, 0, 0 ],
397             ['invoice_number_length_15',15,0, 0, 0 ],
398             [ 'orderdate', 10, 0, 0, 0 ], # YYYY-MM-DD
399              
400             [ 'recycle_by', 8, 0, 0, 0 ],
401             [ 'recycle_id', 25, 0, 0, 0 ],
402              
403             [ 'affiliate', 25, 0, 0, 0 ],
404              
405             [ 'card_type', 2, 2, 1, 0 ],
406             [ 'card_number', 25, 13, 1, 0 ],
407             [ 'expiration', 4, 4, 1, 0 ], # MMYY
408             [ 'cvv2', 4, 3, 1, 0 ],
409             # 'card_token' does not have a documented limit
410              
411             [ 'customer_id', 25, 0, 0, 0 ],
412             );
413 6         23 foreach my $trunc ( @validate ) {
414 204         428 $self->format_misc_field($content,$trunc);
415             #warn "$trunc->[0] => ".($content->{ $trunc->[0] }||'')."\n" if $DEBUG;
416             }
417              
418 6         41 tie my %billToAddress, 'Tie::IxHash', $self->_revmap_fields(
419             content => $content,
420             name => 'name',
421             email => 'email',
422             addressLine1 => 'address',
423             city => 'city',
424             state => 'state',
425             zip => 'zip',
426             country => 'country'
427             , #TODO: will require validation to the spec, this field wont' work as is
428             phone => 'phone',
429             );
430              
431 6         598 tie my %shipToAddress, 'Tie::IxHash', $self->_revmap_fields(
432             content => $content,
433             name => 'ship_name',
434             email => 'ship_email',
435             addressLine1 => 'ship_address',
436             city => 'ship_city',
437             state => 'ship_state',
438             zip => 'ship_zip',
439             country => 'ship_country'
440             , #TODO: will require validation to the spec, this field wont' work as is
441             phone => 'ship_phone',
442             );
443              
444 6         514 tie my %customerinfo, 'Tie::IxHash',
445             $self->_revmap_fields(
446             content => $content,
447             customerType => 'customerType',
448             );
449              
450 6         191 tie my %custombilling, 'Tie::IxHash',
451             $self->_revmap_fields(
452             content => $content,
453             phone => 'company_phone',
454             descriptor => 'description',
455             );
456              
457             ## loop through product list and generate lineItemData for each
458             #
459 6         311 my @products = ();
460 6 50 33     27 if( defined $content->{'products'} && scalar( @{ $content->{'products'} } ) < 100 ){
  6         30  
461 6         12 foreach my $prodOrig ( @{ $content->{'products'} } ) {
  6         21  
462             # use a local copy of prod so that we do not have issues if they try to submit more then once.
463 12         106 my %prod = %$prodOrig;
464 12         33 foreach my $field ( 'tax','amount','totalwithtax','discount' ) {
465             # Note: DO NOT format 'cost', it uses the decimal format
466 48         109 $self->format_amount_field(\%prod, $field);
467             }
468              
469 12         82 my @validate = (
470             # field, maxLen, minLen, errorOnLength, isRequired
471             [ 'description', 26, 0, 0, 0 ],
472             [ 'tax', 8, 0, 1, 0 ],
473             [ 'amount', 8, 0, 1, 0 ],
474             [ 'totalwithtax', 8, 0, 1, 0 ],
475             [ 'discount', 8, 0, 1, 0 ],
476             [ 'code', 12, 0, 0, 0 ],
477             [ 'cost', 12, 0, 1, 0 ],
478             );
479 12         37 foreach my $trunc ( @validate ) { $self->format_misc_field(\%prod,$trunc); }
  84         181  
480              
481 12         47 tie my %lineitem, 'Tie::IxHash',
482             $self->_revmap_fields(
483             content => \%prod,
484             itemSequenceNumber => 'itemSequenceNumber',
485             itemDescription => 'description',
486             productCode => 'code',
487             quantity => 'quantity',
488             unitOfMeasure => 'units',
489             taxAmount => 'tax',
490             lineItemTotal => 'amount',
491             lineItemTotalWithTax => 'totalwithtax',
492             itemDiscountAmount => 'discount',
493             commodityCode => 'code',
494             unitCost => 'cost', # This "amount" field uses decimals
495             );
496 12         1709 push @products, \%lineitem;
497             }
498             }
499              
500             #
501             #
502 6         35 tie my %enhanceddata, 'Tie::IxHash', $self->_revmap_fields(
503             content => $content,
504             customerReference => 'po_number',
505             salesTax => 'salestax',
506             discountAmount => 'discount',
507             shippingAmount => 'shipping',
508             dutyAmount => 'duty',
509             invoiceReferenceNumber => 'invoice_number_length_15',
510             orderDate => 'orderdate',
511             lineItemData => \@products,
512             );
513              
514 6         253 tie my %card, 'Tie::IxHash', $self->_revmap_fields(
515             content => $content,
516             type => 'card_type',
517             number => 'card_number',
518             expDate => 'expiration',
519             cardValidationNum => 'cvv2',
520             );
521              
522 6         341 tie my %token, 'Tie::IxHash', $self->_revmap_fields(
523             content => $content,
524             litleToken => 'card_token',
525             expDate => 'expiration',
526             cardValidationNum => 'cvv2',
527             );
528              
529 6         276 tie my %processing, 'Tie::IxHash', $self->_revmap_fields(
530             content => $content,
531             bypassVelocityCheck => 'velocity_check',
532             );
533              
534 6         204 tie my %cardholderauth, 'Tie::IxHash',
535             $self->_revmap_fields(
536             content => $content,
537             authenticationValue => '3ds',
538             authenticationTransactionId => 'visaverified',
539             customerIpAddress => 'ip',
540             authenticatedByMerchant => 'authenticated',
541             );
542              
543 6         183 tie my %merchantdata, 'Tie::IxHash',
544             $self->_revmap_fields(
545             content => $content,
546             affiliate => 'affiliate',
547             merchantGroupingId => 'merchant_grouping_id',
548             );
549              
550 6         187 tie my %recyclingrequest, 'Tie::IxHash',
551             $self->_revmap_fields(
552             content => $content,
553             recycleBy => 'recycle_by',
554             recycleId => 'recycle_id',
555             );
556              
557 6         250 my %req;
558              
559 6 50       28 if ( $action eq 'registerTokenRequest' ) {
    100          
    50          
    0          
    0          
    0          
    0          
    0          
560 0 0 0     0 croak 'missing card_number' if length($content->{'card_number'} || '') == 0;
561 0         0 tie %req, 'Tie::IxHash', $self->_revmap_fields(
562             content => $content,
563             orderId => 'invoice_number',
564             accountNumber => 'card_number',
565             );
566             }
567             elsif ( $action eq 'sale' ) {
568 5 100 100     252 croak 'missing card_token or card_number' if length($content->{'card_number'} || $content->{'card_token'} || '') == 0;
569             tie %req, 'Tie::IxHash', $self->_revmap_fields(
570             content => $content,
571             orderId => 'invoice_number',
572             amount => 'amount',
573             orderSource => 'orderSource',
574             billToAddress => \%billToAddress,
575             card => $content->{'card_number'} ? \%card : {},
576 4 100       29 token => $content->{'card_token'} ? \%token : {},
    100          
577              
578             #cardholderAuthentication => \%cardholderauth,
579             customBilling => \%custombilling,
580             enhancedData => \%enhanceddata,
581             processingInstructions => \%processing,
582             allowPartialAuth => 'partial_auth',
583             merchantData => \%merchantdata,
584             recyclingRequest => \%recyclingrequest,
585             );
586             }
587             elsif ( $action eq 'authorization' ) {
588 1 50 0     8 croak 'missing card_token or card_number' if length($content->{'card_number'} || $content->{'card_token'} || '') == 0;
589             tie %req, 'Tie::IxHash', $self->_revmap_fields(
590             content => $content,
591             orderId => 'invoice_number',
592             amount => 'amount',
593             orderSource => 'orderSource',
594             billToAddress => \%billToAddress,
595             card => $content->{'card_number'} ? \%card : {},
596 1 50       10 token => $content->{'card_token'} ? \%token : {},
    50          
597              
598             #cardholderAuthentication => \%cardholderauth,
599             processingInstructions => \%processing,
600             customBilling => \%custombilling,
601             allowPartialAuth => 'partial_auth',
602             merchantData => \%merchantdata,
603             recyclingRequest => \%recyclingrequest,
604             );
605             }
606             elsif ( $action eq 'capture' ) {
607 0         0 push @required_fields, qw( order_number amount );
608 0         0 tie %req, 'Tie::IxHash',
609             $self->_revmap_fields(
610             content => $content,
611             litleTxnId => 'order_number',
612             amount => 'amount',
613             enhancedData => \%enhanceddata,
614             processingInstructions => \%processing,
615             );
616             }
617             elsif ( $action eq 'credit' ) {
618              
619             # IF there is a litleTxnId, it's a normal linked credit
620 0 0       0 if( $content->{'order_number'} ){
621 0         0 push @required_fields, qw( order_number amount );
622 0         0 tie %req, 'Tie::IxHash', $self->_revmap_fields(
623             content => $content,
624             litleTxnId => 'order_number',
625             amount => 'amount',
626             customBilling => \%custombilling,
627             processingInstructions => \%processing,
628             );
629             }
630             # ELSE it's an unlinked, which requires different data
631             else {
632 0 0 0     0 croak 'missing card_token or card_number' if length($content->{'card_number'} || $content->{'card_token'} || '') == 0;
633 0         0 push @required_fields, qw( invoice_number amount );
634             tie %req, 'Tie::IxHash', $self->_revmap_fields(
635             content => $content,
636             orderId => 'invoice_number',
637             amount => 'amount',
638             orderSource => 'orderSource',
639             billToAddress => \%billToAddress,
640             card => $content->{'card_number'} ? \%card : {},
641 0 0       0 token => $content->{'card_token'} ? \%token : {},
    0          
642             customBilling => \%custombilling,
643             processingInstructions => \%processing,
644             );
645             }
646             }
647             elsif ( $action eq 'void' ) {
648 0         0 push @required_fields, qw( order_number );
649 0         0 tie %req, 'Tie::IxHash',
650             $self->_revmap_fields(
651             content => $content,
652             litleTxnId => 'order_number',
653             processingInstructions => \%processing,
654             );
655             }
656             elsif ( $action eq 'authReversal' ) {
657 0         0 push @required_fields, qw( order_number amount );
658 0         0 tie %req, 'Tie::IxHash',
659             $self->_revmap_fields(
660             content => $content,
661             litleTxnId => 'order_number',
662             amount => 'amount',
663             );
664             }
665             elsif ( $action eq 'accountUpdate' ) {
666 0         0 push @required_fields, qw( card_number expiration );
667 0         0 tie %req, 'Tie::IxHash',
668             $self->_revmap_fields(
669             content => $content,
670             orderId => 'customer_id',
671             card => \%card,
672             );
673             }
674              
675 5         742 $self->required_fields(@required_fields);
676 5         336 return \%req;
677             }
678              
679             sub submit {
680 0     0 1 0 my ($self) = @_;
681              
682 0         0 local $SCRUBBER=1;
683 0         0 $self->_litle_init;
684              
685 0         0 my %content = $self->content();
686              
687 0 0       0 warn 'Pre processing: '.Dumper(\%content) if $DEBUG;
688 0         0 my $req = $self->map_request( \%content );
689 0 0       0 warn 'Post processing: '.Dumper(\%content) if $DEBUG;
690 0         0 my $post_data;
691              
692 0         0 my $writer = new XML::Writer(
693             OUTPUT => \$post_data,
694             DATA_MODE => 1,
695             DATA_INDENT => 2,
696             ENCODING => 'utf-8',
697             );
698              
699             ## set the authentication data
700 0         0 tie my %authentication, 'Tie::IxHash',
701             $self->_revmap_fields(
702             content => \%content,
703             user => 'login',
704             password => 'password',
705             );
706              
707 0 0       0 warn Dumper($req) if $DEBUG;
708             ## Start the XML Document, parent tag
709 0         0 $writer->xmlDecl();
710             $writer->startTag(
711             "litleOnlineRequest",
712             version => $self->api_version,
713             xmlns => $self->xmlns,
714 0         0 merchantId => $content{'merchantid'},
715             );
716              
717 0         0 $self->_xmlwrite( $writer, 'authentication', \%authentication );
718              
719             ## partial capture modifier, odd location, because it modifies the start tag :(
720 0         0 my %extra;
721 0 0       0 if ($content{'TransactionType'} eq 'capture'){
722 0 0       0 $extra{'partial'} = $content{'partial'} ? 'true' : 'false';
723             }
724              
725             $writer->startTag(
726             $content{'TransactionType'},
727             id => $content{'invoice_number'},
728             reportGroup => $content{'report_group'} || 'BOP',
729 0   0     0 customerId => $content{'customer_id'} || 1,
      0        
730             %extra,
731             );
732 0         0 foreach ( keys( %{$req} ) ) {
  0         0  
733 0         0 $self->_xmlwrite( $writer, $_, $req->{$_} );
734             }
735              
736 0         0 $writer->endTag( $content{'TransactionType'} );
737 0         0 $writer->endTag("litleOnlineRequest");
738 0         0 $writer->end();
739             ## END XML Generation
740              
741 0         0 $self->server_request( $post_data );
742 0 0       0 warn $self->server_request if $DEBUG;
743              
744 0 0       0 if ( $] ge '5.008' ) {
745             # http_post expects data in this format
746 0 0       0 utf8::encode($post_data) if utf8::is_utf8($post_data);
747             }
748              
749 0         0 my ( $page, $status_code, %headers ) = $self->https_post( { 'Content-Type' => 'text/xml; charset=utf-8' } , $post_data);
750              
751 0         0 $self->server_response( $page );
752 0 0       0 warn Dumper $self->server_response, $status_code, \%headers if $DEBUG;
753              
754 0         0 my $response = $self->_parse_xml_response( $page, $status_code );
755              
756 0         0 $content{'TransactionType'} =~ s/Request$//; # no clue why some of the types have a Request and some do not
757              
758 0 0 0     0 if ( exists( $response->{'response'} ) && $response->{'response'} == 1 ) {
759             ## parse error type error
760 0         0 warn Dumper 'https://'.$self->server.':'.$self->port.$self->path,$response, $self->server_request;
761 0         0 $self->error_message( $response->{'message'} );
762 0         0 return;
763             } else {
764             $self->error_message(
765             $response->{ $content{'TransactionType'} . 'Response' }
766 0         0 ->{'message'} );
767             }
768 0         0 $self->{_response} = $response;
769              
770 0 0       0 warn Dumper($response) if $DEBUG;
771              
772             ## Set up the data:
773 0         0 my $resp = $response->{ $content{'TransactionType'} . 'Response' };
774 0         0 $self->{_response} = $resp;
775 0   0     0 $self->card_token( $resp->{'litleToken'} || $resp->{'tokenResponse'}->{'litleToken'} || $content{'card_token'} || '' );
776 0   0     0 $self->order_number( $resp->{'litleTxnId'} || '' );
777 0   0     0 $self->result_code( $resp->{'response'} || '' );
778 0 0       0 $resp->{'authCode'} =~ s/\D//g if $resp->{'authCode'};
779 0   0     0 $self->authorization( $resp->{'authCode'} || '' );
780 0   0     0 $self->cvv2_response( $resp->{'fraudResult'}->{'cardValidationResult'}
781             || '' );
782 0   0     0 $self->avs_code( $resp->{'fraudResult'}->{'avsResult'} || '' );
783 0 0 0     0 if( $resp->{enhancedAuthResponse}
      0        
784             && $resp->{enhancedAuthResponse}->{fundingSource}
785             && $resp->{enhancedAuthResponse}->{fundingSource}->{type} eq 'PREPAID' ) {
786              
787 0         0 $self->is_prepaid(1);
788 0         0 $self->prepaid_balance( $resp->{enhancedAuthResponse}->{fundingSource}->{availableBalance} );
789             } else {
790 0         0 $self->is_prepaid(0);
791             }
792              
793             #$self->is_dupe( $resp->{'duplicate'} ? 1 : 0 );
794 0 0 0     0 if( defined $resp->{'duplicate'} && $resp->{'duplicate'} eq 'true' ) {
795 0         0 $self->is_duplicate(1);
796             }
797             else {
798 0         0 $self->is_duplicate(0);
799             }
800              
801 0 0       0 if( defined $resp->{tokenResponse} ) {
802 0         0 $self->card_token($resp->{tokenResponse}->{litleToken});
803 0         0 $self->card_token_response($resp->{tokenResponse}->{tokenResponseCode});
804 0         0 $self->card_token_message($resp->{tokenResponse}->{tokenMessage});
805             }
806              
807 0 0 0     0 if( $resp->{enhancedAuthResponse}
808             && $resp->{enhancedAuthResponse}->{affluence}
809             ){
810 0         0 $self->get_affluence( $resp->{enhancedAuthResponse}->{affluence} );
811             }
812 0 0       0 $self->is_success( $self->result_code() eq '000' ? 1 : 0 );
813 0 0 0     0 if(
      0        
814             $self->result_code() eq '010' # Partial approval, if they chose that option
815             || ($self->result_code() eq '802' && $self->card_token) # Card is already a token
816             ) {
817 0         0 $self->is_success(1);
818             }
819              
820             ##Failure Status for 3.0 users
821 0 0       0 if ( !$self->is_success ) {
822             my $f_status =
823             $ERRORS{ $self->result_code }->{'failure'}
824 0 0       0 ? $ERRORS{ $self->result_code }->{'failure'}
825             : 'decline';
826 0         0 $self->failure_status($f_status);
827             }
828              
829 0 0       0 unless ( $self->is_success() ) {
830 0 0       0 unless ( $self->error_message() ) {
831             $self->error_message( "(HTTPS response: $status_code) "
832             . "(HTTPS headers: "
833 0         0 . join( ", ", map { "$_ => " . $headers{$_} } keys %headers )
  0         0  
834             . ") "
835             . "(Raw HTTPS content: ".$self->server_response().")" );
836             }
837             }
838              
839             }
840              
841              
842             sub chargeback_retrieve_support_doc {
843 0     0 1 0 my ( $self ) = @_;
844 0         0 $self->_litle_support_doc('RETRIEVE');
845 0 0       0 if ($self->is_success) { $self->{'fileContent'} = $self->{'server_response_dangerous'}; } else { $self->{'fileContent'} = undef; }
  0         0  
  0         0  
846             }
847              
848              
849             sub chargeback_delete_support_doc {
850 0     0 1 0 my ( $self ) = @_;
851 0         0 $self->_litle_support_doc('DELETE' );
852             }
853              
854              
855             sub chargeback_upload_support_doc {
856 0     0 1 0 my ( $self ) = @_;
857 0         0 $self->_litle_support_doc('UPLOAD' );
858             }
859              
860              
861             sub chargeback_replace_support_doc {
862 0     0 1 0 my ( $self ) = @_;
863 0         0 $self->_litle_support_doc('REPLACE' );
864             }
865              
866             sub _litle_support_doc {
867 0     0   0 my ( $self, $action ) = @_;
868              
869 0         0 local $SCRUBBER=1;
870 0         0 $self->_litle_init;
871              
872 0         0 my %content = $self->content();
873              
874 0         0 my $requiredargs = ['case_id','filename','merchantid'];
875 0 0       0 if ($action =~ /(?:UPLOAD|REPLACE)/) { push @$requiredargs, 'filecontent', 'mimetype'; }
  0         0  
876 0         0 foreach my $key (@$requiredargs) {
877 0 0       0 croak "Missing arg $key" unless $content{$key};
878             }
879              
880 0         0 my $actionRESTful = {
881             'DELETE' => 'DELETE',
882             'RETRIEVE' => 'GET',
883             'UPLOAD' => 'POST',
884             'REPLACE' => 'PUT',
885             };
886 0 0       0 die "UNDEFINED ACTION: $action" unless defined $actionRESTful->{$action};
887              
888             {
889 4     4   15759 use bytes;
  4         12  
  4         27  
  0         0  
890 0 0       0 if ( defined $content{'filecontent'} ) {
891 0 0       0 if ( length($content{'filecontent'}) > 2097152 ) { # file limit of 2M
892 0         0 my $msg = 'Filesize Exceeds Limit Of 2MB';
893 0         0 $self->result_code( 012 ); ## no critic
894 0         0 $self->error_message( $msg );
895 0         0 croak $msg;
896             }
897 0         0 my $allowedTypes = {
898             'application/pdf' => 1,
899             'image/gif' => 1,
900             'image/jpeg' => 1,
901             'image/png' => 1,
902             'image/tiff' => 1,
903             };
904 0 0 0     0 if ( ! defined $allowedTypes->{$content{'mimetype'}||''} ) {
905 0         0 croak "File must be one of PDF/GIF/JPG/PNG/TIFF".$content{'mimetype'};
906             }
907             }
908             }
909              
910 0         0 my $caseidURI = $content{'case_id'};
911 0         0 my $filenameURI = $content{'filename'};
912 0         0 my $merchantidURI = $content{'merchantid'};
913 0         0 foreach ( $caseidURI, $filenameURI, $merchantidURI ) {
914 0         0 s/([^a-z0-9\.\-])/sprintf('%%%X',ord($1))/ige;
  0         0  
915             }
916              
917 0         0 my $url = 'https://'.$self->chargeback_server.':'.$self->chargeback_port.'//services/chargebacks/documents/'.$merchantidURI.'/'.$caseidURI.'/'.$filenameURI;
918             my $response = HTTP::Tiny->new( verify_SSL=>$self->verify_SSL )->request($actionRESTful->{$action}, $url, {
919             headers => {
920             'Authorization' => 'Basic ' . MIME::Base64::encode("$content{'login'}:$content{'password'}",''),
921             'Content-Type' => $content{'mimetype'} || 'text/plain',
922             },
923 0   0     0 content => $content{'filecontent'},
924             } );
925              
926 0         0 $self->server_request( $content{'mimetype'} );
927 0         0 $self->server_response( $response->{'content'} );
928              
929 0 0 0     0 if ( $action eq 'RETRIEVE' && $response->{'status'} =~ /^200/ && substr($response->{'content'},0,500) !~ /
      0        
930             # the RETRIEVE action returns the actual page as the file, rather then returning XML
931 0         0 $self->is_success(1);
932             } else {
933 0         0 my $xml_response = $self->_parse_xml_response( $response->{'content'}, $response->{'status'} );
934              
935 0 0 0     0 if (defined $xml_response && defined $xml_response->{'ChargebackCase'}{'Document'}{'ResponseCode'}) {
936 0 0       0 $self->is_success( $xml_response->{'ChargebackCase'}{'Document'}{'ResponseCode'} eq '000' ? 1 : 0 );
937 0         0 $self->result_code( $xml_response->{'ChargebackCase'}{'Document'}{'ResponseCode'} );
938 0         0 $self->error_message( $xml_response->{'ChargebackCase'}{'Document'}{'ResponseMessage'} );
939             } else {
940 0         0 croak "UNRECOGNIZED RESULT: ".$self->server_response;
941             }
942             }
943             }
944              
945              
946             sub chargeback_list_support_docs {
947 0     0 1 0 my ( $self ) = @_;
948              
949 0         0 local $SCRUBBER=1;
950 0         0 $self->_litle_init;
951              
952 0         0 my %content = $self->content();
953              
954 0 0       0 croak "Missing arg case_id" unless $content{'case_id'};
955 0 0       0 croak "Missing arg merchantid" unless $content{'merchantid'};
956 0         0 my $caseidURI = $content{'case_id'};
957 0         0 my $merchantidURI = $content{'merchantid'};
958 0         0 foreach ( $caseidURI, $merchantidURI ) {
959 0         0 s/([^a-z0-9\.\-])/sprintf('%%%X',ord($1))/ige;
  0         0  
960             }
961              
962 0         0 my $url = 'https://'.$self->chargeback_server.':'.$self->chargeback_port.'//services/chargebacks/documents/'.$merchantidURI.'/'.$caseidURI.'/';
963 0         0 my $response = HTTP::Tiny->new( verify_SSL=>$self->verify_SSL )->request('GET', $url, {
964             headers => { Authorization => 'Basic ' . MIME::Base64::encode("$content{'login'}:$content{'password'}",'') },
965             } );
966              
967 0         0 $self->server_request( $url );
968 0         0 $self->server_response( $response->{'content'} );
969              
970 0         0 my $xml_response = $self->_parse_xml_response( $response->{'content'}, $response->{'status'} );
971              
972 0 0 0     0 if (defined $xml_response && $xml_response->{'ChargebackCase'}{'ResponseCode'}) {
    0 0        
973 0         0 $self->result_code( $xml_response->{'ChargebackCase'}{'ResponseCode'} );
974 0         0 $self->error_message( $xml_response->{'ChargebackCase'}{'ResponseMessage'} );
975             } elsif (defined $xml_response && $xml_response->{'ChargebackCase'}{'DocumentEntry'}) {
976 0         0 $self->is_success(1);
977 0         0 $self->result_code( '000' );
978              
979 0         0 my $ref = $xml_response->{'ChargebackCase'}{'DocumentEntry'};
980 0 0 0     0 if (defined $ref->{'id'} && ref $ref->{'id'} eq '') {
981             # XMLin does not parse the result properly for a single document. This fixes the single document format to match the multi-doc format
982 0         0 $ref = { $ref->{'id'} => $ref };
983             }
984 0         0 return $ref;
985             } else {
986 0         0 croak "UNRECOGNIZED RESULT: ".$self->server_response;
987             }
988 0         0 return {};
989             }
990              
991             sub _parse_xml_response {
992 5     5   33 my ( $self, $page, $status_code ) = @_;
993 5         18 my $response = {};
994 5 50       40 if ( $status_code =~ /^200/ ) {
995 5 50       23 if ( ! eval { $response = XMLin($page); } ) {
  5         43  
996 0         0 die "XML PARSING FAILURE: $@";
997             }
998             }
999             else {
1000 0         0 $status_code =~ s/[\r\n\s]+$//; # remove newline so you can see the error in a linux console
1001 0 0       0 if ( $status_code =~ /^(?:900|599)/ ) { $status_code .= ' - verify Litle has whitelisted your IP'; }
  0         0  
1002 0         0 die "CONNECTION FAILURE: $status_code";
1003             }
1004 5         152331 return $response;
1005             }
1006              
1007             sub _parse_batch_response {
1008 0     0   0 my ( $self, $args ) = @_;
1009 0         0 my @results;
1010 0         0 my $resp = $self->{'batch_response'};
1011 0         0 $self->order_number( $resp->{'litleBatchId'} );
1012              
1013             #$self->invoice_number( $resp->{'id'} );
1014             my @result_types =
1015 0         0 grep { $_ =~ m/Response$/ }
1016 0         0 keys %{$resp}; ## get a list of result types in this batch
  0         0  
1017             return {
1018 0         0 'account_update' => $self->_get_update_response,
1019             ## do the other response types now
1020             };
1021             }
1022              
1023              
1024             sub add_item {
1025 0     0 1 0 my $self = shift;
1026             ## do we want to render it now, or later?
1027 0         0 push @{ $self->{'batch_entries'} }, shift;
  0         0  
1028             }
1029              
1030              
1031             sub create_batch {
1032 0     0 1 0 my ( $self, %opts ) = @_;
1033              
1034 0         0 local $SCRUBBER=1;
1035 0         0 $self->_litle_init(\%opts);
1036              
1037 0 0 0     0 if ( ! defined $self->{'batch_entries'} || scalar( @{ $self->{'batch_entries'} } ) < 1 ) {
  0         0  
1038 0         0 $self->error_message('Cannot create an empty batch');
1039 0         0 return;
1040             }
1041              
1042 0         0 my $post_data;
1043              
1044 0         0 my $writer = new XML::Writer(
1045             OUTPUT => \$post_data,
1046             DATA_MODE => 1,
1047             DATA_INDENT => 2,
1048             ENCODING => 'utf-8',
1049             );
1050             ## set the authentication data
1051 0         0 tie my %authentication, 'Tie::IxHash',
1052             $self->_revmap_fields(
1053             content => \%opts,
1054             user => 'login',
1055             password => 'password',
1056             );
1057              
1058             ## Start the XML Document, parent tag
1059 0         0 $writer->xmlDecl();
1060             $writer->startTag(
1061             "litleRequest",
1062             version => $self->batch_api_version,
1063             xmlns => $self->xmlns,
1064 0   0     0 id => $opts{'batch_id'} || time,
1065             numBatchRequests => 1, #hardcoded for now, not doing multiple merchants
1066             );
1067              
1068             ## authentication
1069 0         0 $self->_xmlwrite( $writer, 'authentication', \%authentication );
1070             ## batch Request tag
1071             $writer->startTag(
1072             'batchRequest',
1073             id => $opts{'batch_id'} || time,
1074 0         0 numAccountUpdates => scalar( @{ $self->{'batch_entries'} } ),
1075 0   0     0 merchantId => $opts{'merchantid'},
1076             );
1077 0         0 foreach my $entry ( @{ $self->{'batch_entries'} } ) {
  0         0  
1078 0         0 $self->_litle_scrubber_add_card($entry->{'card_number'});
1079 0         0 my $req = $self->map_request( $entry );
1080             $writer->startTag(
1081             $entry->{'TransactionType'},
1082             id => $entry->{'invoice_number'},
1083             reportGroup => $entry->{'report_group'} || 'BOP',
1084 0   0     0 customerId => $entry->{'customer_id'} || 1,
      0        
1085             );
1086 0         0 foreach ( keys( %{$req} ) ) {
  0         0  
1087 0         0 $self->_xmlwrite( $writer, $_, $req->{$_} );
1088             }
1089 0         0 $writer->endTag( $entry->{'TransactionType'} );
1090             ## need to also handle the action tag here, and custid info
1091             }
1092 0         0 $writer->endTag("batchRequest");
1093 0         0 $writer->endTag("litleRequest");
1094 0         0 $writer->end();
1095             ## END XML Generation
1096              
1097 0         0 $self->server_request( $post_data );
1098 0 0       0 warn $self->server_request if $DEBUG;
1099              
1100             #----- Send it
1101 0 0 0     0 if ( $opts{'method'} && $opts{'method'} eq 'sftp' ) { #FTP
    0 0        
1102 0         0 my $sftp = $self->_sftp_connect(\%opts,'inbound');
1103              
1104             ## save the file out, can't put directly from var, and is multibyte, so issues from filehandle
1105 0   0     0 my $filename = $opts{'batch_id'} || $opts{'login'} . "_" . time;
1106 0         0 my $io = IO::String->new($post_data);
1107 0         0 tie *IO, 'IO::String';
1108              
1109 0 0       0 $sftp->put( $io, "$filename.prg" )
1110             or $self->_die("Cannot PUT $filename", $sftp->error);
1111 0 0       0 $sftp->rename( "$filename.prg",
1112             "$filename.asc" ) #once complete, you rename it, for pickup
1113             or $self->die("Cannot RENAME file", $sftp->error);
1114 0         0 $self->is_success(1);
1115 0         0 $self->server_response( $sftp->message );
1116             }
1117             elsif ( $opts{'method'} && $opts{'method'} eq 'https' ) { #https post
1118 0         0 $self->port('15000');
1119 0         0 $self->path('/');
1120 0         0 my ( $page, $status_code, %headers ) =
1121             $self->https_post($post_data);
1122 0         0 $self->server_response( $page );
1123              
1124 0 0       0 warn Dumper [ $page, $status_code, \%headers ] if $DEBUG;
1125              
1126 0         0 my $response = {};
1127 0 0       0 if ( $status_code =~ /^200/ ) {
1128 0 0 0     0 if ( ! eval { $response = XMLin($page); } ) {
  0 0       0  
1129 0         0 $self->_die("XML PARSING FAILURE: $@");
1130             }
1131             elsif ( exists( $response->{'response'} )
1132             && $response->{'response'} == 1 )
1133             {
1134             ## parse error type error
1135 0         0 warn Dumper( $response, $self->server_request );
1136 0         0 $self->error_message( $response->{'message'} );
1137 0         0 return;
1138             }
1139             else {
1140             $self->error_message(
1141 0         0 $response->{'batchResponse'}->{'message'} );
1142             }
1143             }
1144             else {
1145 0         0 $self->_die("CONNECTION FAILURE: $status_code");
1146             }
1147 0         0 $self->{_response} = $response;
1148              
1149             ##parse out the batch info as our general status
1150 0         0 my $resp = $response->{'batchResponse'};
1151 0         0 $self->order_number( $resp->{'litleSessionId'} );
1152 0         0 $self->result_code( $response->{'response'} );
1153 0 0       0 $self->is_success( $response->{'response'} eq '0' ? 1 : 0 );
1154              
1155 0 0       0 warn Dumper($response) if $DEBUG;
1156 0 0       0 unless ( $self->is_success() ) {
1157 0 0       0 unless ( $self->error_message() ) {
1158             $self->error_message(
1159             "(HTTPS response: $status_code) "
1160             . "(HTTPS headers: "
1161             . join( ", ",
1162 0         0 map { "$_ => " . $headers{$_} } keys %headers )
  0         0  
1163             . ") "
1164             . "(Raw HTTPS content: $page)"
1165             );
1166             }
1167             }
1168 0 0       0 if ( $self->is_success() ) {
1169 0         0 $self->{'batch_response'} = $resp;
1170             }
1171             }
1172              
1173             }
1174              
1175              
1176             sub send_rfr {
1177 0     0 1 0 my ( $self, $args ) = @_;
1178              
1179 0         0 local $SCRUBBER=1;
1180 0         0 $self->_litle_init($args);
1181              
1182 0         0 my $post_data;
1183 0         0 my $writer = new XML::Writer(
1184             OUTPUT => \$post_data,
1185             DATA_MODE => 1,
1186             DATA_INDENT => 2,
1187             ENCODING => 'utf-8',
1188             );
1189             ## set the authentication data
1190 0         0 tie my %authentication, 'Tie::IxHash',
1191             $self->_revmap_fields(
1192             content => $args,
1193             user => 'login',
1194             password => 'password',
1195             );
1196              
1197             ## Start the XML Document, parent tag
1198 0         0 $writer->xmlDecl();
1199 0         0 $writer->startTag(
1200             "litleRequest",
1201             version => $self->batch_api_version,
1202             xmlns => $self->xmlns,
1203             numBatchRequests => 0,
1204             );
1205              
1206             ## authentication
1207 0         0 $self->_xmlwrite( $writer, 'authentication', \%authentication );
1208             ## batch Request tag
1209 0         0 $writer->startTag('RFRRequest');
1210 0         0 $writer->startTag('accountUpdateFileRequestData');
1211 0         0 $writer->startTag('merchantId');
1212 0         0 $writer->characters( $args->{'merchantid'} );
1213 0         0 $writer->endTag('merchantId');
1214 0         0 $writer->startTag('postDay');
1215 0         0 $writer->characters( $args->{'date'} );
1216 0         0 $writer->endTag('postDay');
1217 0         0 $writer->endTag('accountUpdateFileRequestData');
1218 0         0 $writer->endTag("RFRRequest");
1219 0         0 $writer->endTag("litleRequest");
1220 0         0 $writer->end();
1221             ## END XML Generation
1222             #
1223 0         0 $self->port('15000');
1224 0         0 $self->path('/');
1225 0         0 my ( $page, $status_code, %headers ) = $self->https_post($post_data);
1226              
1227 0         0 $self->server_request( $post_data );
1228 0         0 $self->server_response( $page );
1229 0 0       0 warn $self->server_request if $DEBUG;
1230              
1231 0 0       0 warn Dumper [ $page, $status_code, \%headers ] if $DEBUG;
1232              
1233 0         0 my $response = {};
1234 0 0       0 if ( $status_code =~ /^200/ ) {
1235 0 0 0     0 if ( ! eval { $response = XMLin($page); } ) {
  0 0       0  
1236 0         0 die "XML PARSING FAILURE: $@";
1237             }
1238             elsif ( exists( $response->{'response'} ) && $response->{'response'} == 1 )
1239             {
1240             ## parse error type error
1241 0         0 warn Dumper( $response, $self->server_request );
1242 0         0 $self->error_message( $response->{'message'} );
1243 0         0 return;
1244             }
1245             else {
1246 0         0 $self->error_message( $response->{'RFRResponse'}->{'message'} );
1247             }
1248             }
1249             else {
1250 0         0 die "CONNECTION FAILURE: $status_code";
1251             }
1252 0         0 $self->{_response} = $response;
1253 0 0       0 if ( $response->{'RFRResponse'} ) {
1254             ## litle returns an 'error' if the file is not done. So it's not ready yet.
1255 0         0 $self->result_code( $response->{'RFRResponse'}->{'response'} );
1256 0         0 return;
1257             }
1258             else {
1259              
1260             #if processed, it returns as a batch, so, success, and let get the details
1261 0         0 my $resp = $response->{'batchResponse'};
1262 0 0       0 $self->is_success( $resp->{'response'} eq '000' ? 1 : 0 );
1263 0         0 $self->{'batch_response'} = $resp;
1264 0         0 $self->_parse_batch_response;
1265             }
1266             }
1267              
1268             sub _sftp_connect {
1269 0     0   0 my ($self,$args,$dir) = @_;
1270 0 0       0 $self->_die("Missing ftp_username") if ! $args->{'ftp_username'};
1271 0 0       0 $self->_die("Missing ftp_password") if ! $args->{'ftp_password'};
1272 0         0 require Net::SFTP::Foreign;
1273             my $sftp = Net::SFTP::Foreign->new(
1274             $self->server(),
1275             timeout => $args->{'ftp_timeout'} || 90,
1276             stderr_discard => 1,
1277             user => $args->{'ftp_username'},
1278 0   0     0 password => $args->{'ftp_password'},
1279             );
1280 0 0       0 $sftp->error and $self->_die("SSH connection failed: " . $sftp->error);
1281              
1282 0 0       0 if ($dir) {
1283 0 0       0 $sftp->setcwd($dir)
1284             or $self->_die("Cannot change working directory ", $sftp->error);
1285             }
1286              
1287 0         0 return $sftp;
1288             }
1289              
1290             sub _die {
1291 0     0   0 my $self = shift;
1292 0         0 my $msg = join '', @_;
1293 0         0 $self->is_success(0);
1294 0         0 $self->error_message( $msg );
1295 0         0 die $msg."\n";
1296             }
1297              
1298              
1299             sub retrieve_batch_list {
1300 0     0 1 0 my ($self, %opts ) = @_;
1301              
1302 0         0 local $SCRUBBER=1;
1303 0         0 $self->_litle_init(\%opts);
1304              
1305 0         0 my $sftp = $self->_sftp_connect(\%opts,'outbound');
1306              
1307 0 0       0 my $ls = $sftp->ls( wanted => qr/\.asc$/ )
1308             or $self->_die("Cannot get directory listing ", $sftp->error);
1309              
1310 0         0 my @filenames = map {$_->{'filename'}} @{ $ls };
  0         0  
  0         0  
1311 0         0 $self->is_success(1);
1312 0         0 return \@filenames;
1313             }
1314              
1315              
1316             sub retrieve_batch_delete {
1317 0     0 1 0 my ( $self, %opts ) = @_;
1318              
1319 0         0 local $SCRUBBER=1;
1320 0         0 $self->_litle_init(\%opts);
1321              
1322 0 0       0 $self->_die("Missing batch_id") if !$opts{'batch_id'};
1323              
1324 0         0 my $sftp = $self->_sftp_connect(\%opts,'outbound');
1325              
1326 0         0 my $filename = $opts{'batch_id'};
1327 0 0       0 $sftp->remove( $filename )
1328             or $self->_die("Cannot delete $filename: ", $sftp->error);
1329              
1330 0         0 $self->is_success(1);
1331             }
1332              
1333              
1334             sub retrieve_batch {
1335 0     0 1 0 my ( $self, %opts ) = @_;
1336              
1337 0         0 local $SCRUBBER=1;
1338 0         0 $self->_litle_init(\%opts);
1339              
1340 0 0       0 $self->_die("Missing batch_id") if !$opts{'batch_id'};
1341              
1342 0         0 my $post_data;
1343 0 0       0 if ( $opts{'batch_return'} ) {
1344             ## passed in data structure
1345 0         0 $post_data = $opts{'batch_return'};
1346 0         0 $self->server_request('Data was provided using batch_return option');
1347             }
1348             else {
1349             ## go download a batch
1350 0         0 my $sftp = $self->_sftp_connect(\%opts,'outbound');
1351              
1352 0         0 my $filename = $opts{'batch_id'};
1353 0         0 $self->server_request('SFTP requesting file: '.$filename,1);
1354 0 0       0 $post_data = $sftp->get_content( $filename )
1355             or $self->_die("Cannot GET $filename", $sftp->error);
1356             }
1357 0         0 $self->server_response_dangerous($post_data,1);
1358 0         0 $self->server_response('Litle scrubber not initialized yet, see server_response_dangerous for a copy of the server response. Please note it may contain data that is not appropriate to store.',1);
1359              
1360 0         0 my $response = {};
1361 0 0 0     0 if ( ! eval { $response = XMLin($post_data,
  0 0       0  
1362             ForceArray => [ 'accountUpdateResponse' ],
1363             KeyAttr => '-id',
1364             ); } ) {
1365 0         0 $self->_die("XML PARSING FAILURE: $@");
1366             }
1367             elsif ( exists( $response->{'response'} ) && $response->{'response'} == 1 ) {
1368             ## parse error type error
1369 0         0 warn Dumper( $response, $self->{'_post_data'} );
1370 0   0     0 $self->_die($response->{'message'} || 'No reason given');
1371             }
1372             else {
1373             ## update the status
1374 0         0 $self->error_message( $response->{'batchResponse'}->{'message'} );
1375             }
1376              
1377 0         0 $self->{_response} = $response;
1378 0         0 my $resp = $response->{'batchResponse'};
1379 0         0 $self->order_number( $resp->{'litleSessionId'} );
1380 0         0 $self->result_code( $response->{'response'} );
1381 0 0       0 $self->is_success( $response->{'response'} eq '0' ? 1 : 0 );
1382 0 0       0 if ( $self->is_success() ) {
1383 0         0 $self->{'batch_response'} = $resp;
1384 0         0 return $self->_parse_batch_response;
1385             }
1386             }
1387              
1388             sub _get_update_response {
1389 0     0   0 my $self = shift;
1390 0         0 require Business::OnlinePayment::Litle::UpdaterResponse;
1391 0         0 my @response;
1392 0         0 foreach
1393 0         0 my $item ( @{ $self->{'batch_response'}->{'accountUpdateResponse'} } )
1394             {
1395 0         0 push @response,
1396             Business::OnlinePayment::Litle::UpdaterResponse->new( $item );
1397             }
1398 0         0 return \@response;
1399             }
1400              
1401             sub _revmap_fields {
1402 88     88   160 my $self = shift;
1403 88         335 tie my (%map), 'Tie::IxHash', @_;
1404 88         7591 my %content;
1405 88 50 33     298 if ( $map{'content'} && ref( $map{'content'} ) eq 'HASH' ) {
1406 88         1445 %content = %{ delete( $map{'content'} ) };
  88         308  
1407             }
1408             else {
1409 0         0 warn "WARNING: This content has not been pre-processed with map_fields";
1410 0         0 %content = $self->content();
1411             }
1412              
1413             map {
1414 88         3590 my $value;
  459         2990  
1415 459 100       1212 if ( ref( $map{$_} ) eq 'HASH' ) {
    100          
    50          
    100          
1416 39 100       243 $value = $map{$_} if ( keys %{ $map{$_} } );
  39         125  
1417             }
1418             elsif ( ref( $map{$_} ) eq 'ARRAY' ) {
1419 6         71 $value = $map{$_};
1420             }
1421             elsif ( ref( $map{$_} ) ) {
1422 0         0 $value = ${ $map{$_} };
  0         0  
1423             }
1424             elsif ( exists( $content{ $map{$_} } ) ) {
1425 330         7031 $value = $content{ $map{$_} };
1426             }
1427              
1428 459 100       5079 if ( defined($value) ) {
1429 370         1227 ( $_ => $value );
1430             }
1431             else {
1432 89         346 ();
1433             }
1434             } ( keys %map );
1435             }
1436              
1437             sub _xmlwrite {
1438 257     257   724 my ( $self, $writer, $item, $value ) = @_;
1439 257 100       1777 if ( ref($value) eq 'HASH' ) {
    100          
1440 47 50       146 my $attr = $value->{'attr'} ? $value->{'attr'} : {};
1441 47         326 $writer->startTag( $item, %{$attr} );
  47         163  
1442 47         2595 foreach ( keys(%$value) ) {
1443 190 50       5004 next if $_ eq 'attr';
1444 190         1255 $self->_xmlwrite( $writer, $_, $value->{$_} );
1445             }
1446 47         1300 $writer->endTag($item);
1447             }
1448             elsif ( ref($value) eq 'ARRAY' ) {
1449 4         12 foreach ( @{$value} ) {
  4         13  
1450 8         148 $self->_xmlwrite( $writer, $item, $_ );
1451             }
1452             }
1453             else {
1454 206         602 $writer->startTag($item);
1455 206         11619 $writer->characters($value);
1456 206         4866 $writer->endTag($item);
1457             }
1458             }
1459              
1460             sub _default_scrubber {
1461 9     9   870 my $cc = shift;
1462 9         48 my $del = substr($cc,0,6).('X'x(length($cc)-10)).substr($cc,-4,4); # show first 6 and last 4
1463 9         49 return $del;
1464             }
1465              
1466             sub _litle_scrubber_add_card {
1467 7     7   136 my ( $self, $cc ) = @_;
1468 7 100       45 return if ! $cc;
1469 5         15 my $scrubber = $self->{_scrubber};
1470 5         12 scrubber_add_scrubber({$cc=>&{$scrubber}($cc)});
  5         19  
1471             }
1472              
1473             sub _litle_init {
1474 6     6   20 my ( $self, $opts ) = @_;
1475              
1476             # initialize/reset the reporting methods
1477 6         128 $self->is_success(0);
1478 6         81 $self->server_request('');
1479 6         29 $self->server_response('');
1480 6         123 $self->error_message('');
1481              
1482             # some calls are passed via the content method, others are direct arguments... this way we cover both
1483 6         61 my %content = $self->content();
1484 6         178 foreach my $ptr (\%content,$opts) {
1485 12 100       81 next if ! $ptr;
1486             scrubber_init({
1487             quotemeta($ptr->{'password'}||'')=>'DELETED',
1488             quotemeta($ptr->{'ftp_password'}||'')=>'DELETED',
1489 6 50 50     96 ($ptr->{'cvv2'} ? '(?<=[^\d])'.quotemeta($ptr->{'cvv2'}).'(?=[^\d])' : '')=>'DELETED',
      50        
1490             });
1491 6         1378 $self->_litle_scrubber_add_card($ptr->{'card_number'});
1492             }
1493             }
1494              
1495              
1496             sub chargeback_activity_request {
1497 0     0 1   my ( $self ) = @_;
1498              
1499 0           local $SCRUBBER=1;
1500 0           $self->_litle_init;
1501              
1502 0           my $post_data;
1503 0           my %content = $self->content();
1504              
1505             ## activity_date
1506             ## Type = Date; Format = YYYY-MM-DD
1507 0 0 0       if ( ! $content{'activity_date'} || $content{'activity_date'} !~ m/^\d{4}-(\d{2})-(\d{2})$/ || $1 > 12 || $2 > 31) {
      0        
      0        
1508 0   0       $self->_die("Invalid Date Pattern, YYYY-MM-DD required:" . ( $content{'activity_date'} || 'undef'));
1509             }
1510             #
1511             ## financials only [true,false]
1512             # The financialOnly element is an optional child of the litleChargebackActivitiesRequest element.
1513             # You use this flag in combination with the activityDate element to specify a request for chargeback financial activities that occurred on the specified date.
1514             # A value of true returns only activities that had financial impact on the specified date.
1515             # A value of false returns all activities on the specified date.
1516             #Type = Boolean; Valid Values = true or false
1517 0           my $financials;
1518 0 0         if ( defined( $content{'financial_only'} ) ) {
1519 0 0         $financials = $content{'financial_only'} ? 'true' : 'false';
1520             }
1521             else {
1522 0           $financials = 'false';
1523             }
1524              
1525 0           my $writer = new XML::Writer(
1526             OUTPUT => \$post_data,
1527             DATA_MODE => 1,
1528             DATA_INDENT => 2,
1529             ENCODING => 'utf-8',
1530             );
1531             ## set the authentication data
1532 0           tie my %authentication, 'Tie::IxHash',
1533             $self->_revmap_fields(
1534             content => \%content,
1535             user => 'login',
1536             password => 'password',
1537             );
1538              
1539             ## Start the XML Document, parent tag
1540 0           $writer->xmlDecl();
1541 0           $writer->startTag(
1542             "litleChargebackActivitiesRequest",
1543             version => $self->chargeback_api_version,
1544             xmlns => $self->xmlns,
1545             );
1546              
1547             ## authentication
1548 0           $self->_xmlwrite( $writer, 'authentication', \%authentication );
1549             ## batch Request tag
1550 0           $writer->startTag('activityDate');
1551 0           $writer->characters( $content{'activity_date'} );
1552 0           $writer->endTag('activityDate');
1553 0           $writer->startTag('financialOnly');
1554 0           $writer->characters($financials);
1555 0           $writer->endTag('financialOnly');
1556 0           $writer->endTag("litleChargebackActivitiesRequest");
1557 0           $writer->end();
1558             ## END XML Generation
1559              
1560 0           $self->{'_post_data'} = $post_data;
1561 0 0         warn $self->{'_post_data'} if $DEBUG;
1562             #my ( $page, $status_code, %headers ) = $self->https_post( { 'Content-Type' => 'text/xml; charset=utf-8' } , $post_data);
1563 0           my $url = 'https://'.$self->chargeback_server.':'.$self->chargeback_port.'/'.$self->chargeback_path;
1564 0           my $tiny_response = HTTP::Tiny->new( verify_SSL=>$self->verify_SSL )->request('POST', $url, {
1565             headers => { 'Content-Type' => 'text/xml; charset=utf-8', },
1566             content => $post_data,
1567             } );
1568              
1569 0           my $page = $tiny_response->{'content'};
1570 0           $self->server_request( $post_data );
1571 0           $self->server_response( $page );
1572 0           my $status_code = $tiny_response->{'status'};
1573 0           my %headers = %{$tiny_response->{'headers'}};
  0            
1574              
1575 0 0         warn Dumper $page, $status_code, \%headers if $DEBUG;
1576              
1577 0           my $response = {};
1578 0 0         if ( $status_code =~ /^200/ ) {
1579             ## Failed to parse
1580 0 0 0       if ( !eval { $response = XMLin($page,
  0 0          
1581             ForceArray => [ 'caseActivity' ],
1582             ); } ) {
1583 0           $self->_die("XML PARSING FAILURE: $@, $page");
1584             } ## well-formed failure message
1585             elsif ( exists( $response->{'response'} )
1586             && $response->{'response'} == 1 )
1587             {
1588             ## parse error type error
1589 0           warn Dumper( $response, $self->{'_post_data'} );
1590 0           $self->error_message( $response->{'message'} );
1591 0           return;
1592             } ## success message
1593             else {
1594             $self->error_message(
1595 0           $response->{'litleChargebackActivitiesResponse'}->{'message'} );
1596             }
1597             }
1598             else {
1599 0           $status_code =~ s/[\r\n\s]+$//
1600             ; # remove newline so you can see the error in a linux console
1601 0 0         if ( $status_code =~ /^(?:900|599)/ ) {
1602 0           $status_code .= ' - verify Litle has whitelisted your IP';
1603             }
1604 0           $self->_die("CONNECTION FAILURE: $status_code");
1605             }
1606 0           $self->{_response} = $response;
1607              
1608 0           my @response_list;
1609 0           require Business::OnlinePayment::Litle::ChargebackActivityResponse;
1610 0           foreach my $case ( @{ $response->{caseActivity} } ) {
  0            
1611 0           push @response_list,
1612             Business::OnlinePayment::Litle::ChargebackActivityResponse->new($case);
1613             }
1614              
1615 0 0         warn Dumper($response) if $DEBUG;
1616 0           $self->is_success(1);
1617 0           return \@response_list;
1618             }
1619              
1620              
1621             sub chargeback_update_request {
1622 0     0 1   my ( $self ) = @_;
1623              
1624 0           local $SCRUBBER=1;
1625 0           $self->_litle_init;
1626              
1627 0           my $post_data;
1628 0           my %content = $self->content();
1629              
1630 0           foreach my $key (qw(case_id merchant_activity_id activity )) {
1631             ## case_id
1632             ## merchant_activity_id
1633             ## activity
1634 0 0         croak "Missing arg $key" unless $content{$key};
1635             }
1636              
1637 0           my $writer = new XML::Writer(
1638             OUTPUT => \$post_data,
1639             DATA_MODE => 1,
1640             DATA_INDENT => 2,
1641             ENCODING => 'utf-8',
1642             );
1643             ## set the authentication data
1644 0           tie my %authentication, 'Tie::IxHash',
1645             $self->_revmap_fields(
1646             content => \%content,
1647             user => 'login',
1648             password => 'password',
1649             );
1650              
1651             ## Start the XML Document, parent tag
1652 0           $writer->xmlDecl();
1653 0           $writer->startTag(
1654             "litleChargebackUpdateRequest",
1655             version => $self->chargeback_api_version,
1656             xmlns => $self->xmlns,
1657             );
1658              
1659             ## authentication
1660 0           $self->_xmlwrite( $writer, 'authentication', \%authentication );
1661 0           $writer->startTag('caseUpdate');
1662 0           $writer->startTag('caseId');
1663 0           $writer->characters( $content{'case_id'} );
1664 0           $writer->endTag('caseId');
1665              
1666 0           $writer->startTag('merchantActivityId');
1667 0           $writer->characters( $content{'merchant_activity_id'} );
1668 0           $writer->endTag('merchantActivityId');
1669              
1670 0           $writer->startTag('activity');
1671 0           $writer->characters( $content{'activity'} );
1672 0           $writer->endTag('activity');
1673              
1674 0           $writer->endTag('caseUpdate');
1675 0           $writer->endTag("litleChargebackUpdateRequest");
1676 0           $writer->end();
1677             ## END XML Generation
1678              
1679 0           $self->{'_post_data'} = $post_data;
1680 0 0         warn $self->{'_post_data'} if $DEBUG;
1681             #my ( $page, $status_code, %headers ) = $self->https_post($post_data);
1682 0           my $url = 'https://'.$self->chargeback_server.':'.$self->chargeback_port.'/'.$self->chargeback_path;
1683 0           my $tiny_response = HTTP::Tiny->new( verify_SSL=>$self->verify_SSL )->request('POST', $url, {
1684             headers => { 'Content-Type' => 'text/xml; charset=utf-8', },
1685             content => $post_data,
1686             } );
1687              
1688 0           my $page = $tiny_response->{'content'};
1689 0           $self->server_response( $page );
1690 0           my $status_code = $tiny_response->{'status'};
1691 0           my %headers = %{$tiny_response->{'headers'}};
  0            
1692              
1693 0 0         warn Dumper $page, $status_code, \%headers if $DEBUG;
1694              
1695 0           my $response = {};
1696 0 0         if ( $status_code =~ /^200/ ) {
1697             ## Failed to parse
1698 0 0         if ( !eval { $response = XMLin($page); } ) {
  0            
1699 0           die "XML PARSING FAILURE: $@, $page";
1700             } ## well-formed failure message
1701 0           $self->{_response} = $response;
1702 0 0         if ( exists( $response->{'response'} ) ) {
1703             ## parse error type error
1704 0           warn Dumper( $response, $self->{'_post_data'} );
1705 0           $self->result_code( $response->{'response'} ); # 0 - success, 1 invalid xml
1706 0           $self->error_message( $response->{'message'} );
1707 0           $self->phoenixTxnId( $response->{'caseUpdateResponse'}{'phoenixTxnId'} );
1708 0           $self->is_success(1);
1709 0           return $response->{'caseUpdateResponse'}{'phoenixTxnId'};
1710             }
1711             else {
1712 0           die "UNKNOWN XML RESULT: $page";
1713             }
1714             }
1715             else {
1716 0           $status_code =~ s/[\r\n\s]+$//
1717             ; # remove newline so you can see the error in a linux console
1718 0 0         if ( $status_code =~ /^(?:900|599)/ ) {
1719 0           $status_code .= ' - verify Litle has whitelisted your IP';
1720             }
1721 0           die "CONNECTION FAILURE: $status_code";
1722             }
1723             }
1724              
1725              
1726              
1727             1; # End of Business::OnlinePayment::Litle
1728              
1729             __END__