File Coverage

blib/lib/Business/OnlinePayment/Vindicia/Select.pm
Criterion Covered Total %
statement 192 233 82.4
branch 40 82 48.7
condition 16 49 32.6
subroutine 28 31 90.3
pod 10 10 100.0
total 286 405 70.6


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::Vindicia::Select;
2 1     1   6121 use strict;
  1         2  
  1         27  
3 1     1   4 use warnings;
  1         1  
  1         24  
4              
5 1     1   4 use Business::OnlinePayment;
  1         1  
  1         28  
6 1     1   3 use vars qw(@ISA $me $DEBUG $VERSION);
  1         2  
  1         59  
7 1     1   640 use HTTP::Tiny;
  1         36879  
  1         39  
8 1     1   728 use XML::Writer;
  1         5321  
  1         34  
9 1     1   924 use XML::Simple;
  1         6756  
  1         9  
10 1     1   762 use Business::CreditCard qw(cardtype);
  1         1504  
  1         69  
11 1     1   530 use Data::Dumper;
  1         4837  
  1         80  
12 1     1   620 use Log::Scrubber qw(disable $SCRUBBER scrubber :Carp scrubber_add_scrubber);
  1         5710  
  1         6  
13              
14             @ISA = qw(Business::OnlinePayment);
15             $me = 'Business::OnlinePayment::Vindicia::Select';
16             $DEBUG = 0;
17             $VERSION = '0.002';
18              
19             =head1 NAME
20              
21             Business::OnlinePayment::Vindicia::Select - Backend for Business::OnlinePayment
22              
23             =head1 SYNOPSIS
24              
25             This is a plugin for the Business::OnlinePayment interface. Please refer to that docuementation for general usage.
26              
27             use Business::OnlinePayment;
28             my $tx = Business::OnlinePayment->new(
29             "Vindicia::Select",
30             default_Origin => 'NEW', # or RECURRING
31             );
32              
33             $tx->content(
34             type => 'CC',
35             login => 'testdrive',
36             password => '123qwe',
37             action => 'billTransactions',
38             description => 'FOO*Business::OnlinePayment test',
39             amount => '49.95',
40             customer_id => 'tfb',
41             name => 'Tofu Beast',
42             address => '123 Anystreet',
43             city => 'Anywhere',
44             state => 'UT',
45             zip => '84058',
46             card_number => '4007000000027',
47             expiration => '09/02',
48             cvv2 => '1234', #optional
49             invoice_number => '54123',
50             vindicia_nvp => {
51             custom => 'data',
52             goes => 'here',
53             },
54             );
55             $tx->submit();
56              
57             =head1 METHODS AND FUNCTIONS
58              
59             See L for the complete list. The following methods either override the methods in L or provide additional functions.
60              
61             =head2 result_code
62              
63             Returns the response error code. Will be empty if no code is returned, or if multiple codes can exist.
64              
65             =head2 error_message
66              
67             Returns the response error description text. Will be empty if no code error is returned, or if multiple errors can exist.
68              
69             =head2 server_request
70              
71             Returns the complete request that was sent to the server. The request has been stripped of card_num, cvv2, and password. So it should be safe to log.
72              
73             =cut
74              
75             sub server_request {
76 12     12 1 23 my ( $self, $val, $tf ) = @_;
77 12 100       26 if ($val) {
78 4         19 $self->{server_request} = scrubber $val;
79 4 50       335 $self->server_request_dangerous($val,1) unless $tf;
80             }
81 12         16 return $self->{server_request};
82             }
83              
84             =head2 server_request_dangerous
85              
86             Returns the complete request that was sent to the server. This could contain data that is NOT SAFE to log. It should only be used in a test environment, or in a PCI compliant manner.
87              
88             =cut
89              
90             sub server_request_dangerous {
91 4     4 1 8 my ( $self, $val, $tf ) = @_;
92 4 50       10 if ($val) {
93 4         10 $self->{server_request_dangerous} = $val;
94 4 50       13 $self->server_request($val,1) unless $tf;
95             }
96 4         5 return $self->{server_request_dangerous};
97             }
98              
99             =head2 server_response
100              
101             Returns the complete response from the server. The response has been stripped of card_num, cvv2, and password. So it should be safe to log.
102              
103             =cut
104              
105             sub server_response {
106 12     12 1 571 my ( $self, $val, $tf ) = @_;
107 12 100       24 if ($val) {
108 4         11 $self->{server_response} = scrubber $val;
109 4 50       209 $self->server_response_dangerous($val,1) unless $tf;
110             }
111 12         15 return $self->{server_response};
112             }
113              
114             =head2 server_response_dangerous
115              
116             Returns the complete response from the server. This could contain data that is NOT SAFE to log. It should only be used in a test environment, or in a PCI compliant manner.
117              
118             =cut
119              
120             sub server_response_dangerous {
121 4     4 1 42 my ( $self, $val, $tf ) = @_;
122 4 50       9 if ($val) {
123 4         8 $self->{server_response_dangerous} = $val;
124 4 50       8 $self->server_response($val,1) unless $tf;
125             }
126 4         7 return $self->{server_response_dangerous};
127             }
128              
129             =head1 Handling of content(%content) data:
130              
131             =head2 action
132              
133             The following actions are valid
134              
135             normal authorization
136             authorization only
137             post authorization
138             credit
139             void
140             auth reversal
141              
142             =head1 TESTING
143              
144             The test suite runs using mocked data results.
145             All tests are run using MOCKED return values.
146             If you wish to run REAL tests then add these ENV variables.
147              
148             export PERL_BUSINESS_VINDICIA_USERNAME=your_test_user
149             export PERL_BUSINESS_VINDICIA_PASSWORD=your_test_password
150              
151             If you would like to create your own tests, or mock your own responses you can do the following
152              
153             use Business::OnlinePayment;
154             my $tx = Business::OnlinePayment->new(
155             "Vindicia::Select",
156             default_Origin => 'NEW', # or RECURRING
157             );
158             push @{$client->{'mocked'}}, {
159             action => 'billTransactions', # must match the action you call, or the script will die
160             login => 'mocked', # must match the login credentials used, or the script will die
161             resp => 'ok_duplicate', # or you can return a HASH of the actual data you want to mock
162             };
163              
164             =head1 FUNCTIONS
165              
166             =head2 _info
167              
168             Return the introspection hash for BOP 3.x
169              
170             =cut
171              
172             sub _info {
173             return {
174 4     4   54 info_compat => '0.01',
175             gateway_name => 'Vindicia Select - SOAP API',
176             gateway_url => 'http://www.vindicia.com',
177             module_version => $VERSION,
178             supported_types => ['CC','ECHECK'],
179             supported_actions => {
180             CC => [
181              
182             #non-standard bop actions
183             'billTransactions',
184             'fetchBillingResults',
185             'fetchByMerchantTransactionId',
186             'refundTransactions',
187             ],
188             },
189             };
190             }
191              
192             =head2 set_defaults
193              
194             Used by BOP to set default values during "new"
195              
196             =cut
197              
198             sub set_defaults {
199 1     1 1 39 my $self = shift;
200 1         2 my %opts = @_;
201              
202 1         6 $self->build_subs(
203             qw( order_number md5 avs_code cvv2_response card_token cavv_response failure_status verify_SSL )
204             );
205              
206 1         246 $self->test_transaction(0);
207 1         4 $self->{_scrubber} = \&_default_scrubber;
208             }
209              
210             =head2 test_transaction
211              
212             Get/set the server used for processing transactions. Possible values are Live, Certification, and Sandbox
213             Default: Live
214              
215             #Live
216             $self->test_transaction(0);
217              
218             #Test
219             $self->test_transaction(1);
220              
221             #Read current value
222             $val = $self->test_transaction();
223              
224             =cut
225              
226             sub test_transaction {
227 2     2 1 717 my $self = shift;
228 2         2 my $testMode = shift;
229 2 50 0     6 if (! defined $testMode) { $testMode = $self->{'test_transaction'} || 0; }
  0         0  
230              
231 2         48 $self->require_avs(0);
232 2         45 $self->verify_SSL(0);
233 2         37 $self->port('443');
234 2         37 $self->path('v1.1/soap.pl'); #https://soap.prodtest.sj.vindicia.com/v1.1/soap.pl
235 2 100 33     29 if (lc($testMode) eq 'sandbox' || lc($testMode) eq 'test' || $testMode eq '1') {
      66        
236 1         16 $self->server('soap.prodtest.sj.vindicia.com');
237 1         5 $self->{'test_transaction'} = 1;
238             } else {
239 1         19 $self->server('soap.vindicia.com');
240 1         6 $self->{'test_transaction'} = 0;
241             }
242 2         3 return $self->{'test_transaction'};
243             }
244              
245             =head2 submit
246              
247             Do a bop-ish action on vindicia
248              
249             =cut
250              
251             sub submit {
252             my ($self) = @_;
253              
254             local $SCRUBBER=1;
255             $self->_tx_init;
256              
257             my %content = $self->content();
258             my $action = $content{'action'};
259             die 'unsupported action' unless grep { $action eq $_ } @{$self->_info()->{'supported_actions'}->{'CC'}};
260             $self->$action();
261             }
262              
263             =head2 billTransactions
264              
265             Send a batch of transactions to vindica for collection
266              
267             is_success means the call was successful, it does NOT mean all of your transactions were accepted
268             In order to verify your transaction you need to look at result->{'response'} for an ARRAY of potential
269             errors, if no errors exist the result will not have a response array
270              
271             =cut
272              
273             sub billTransactions {
274 1     1 1 7 my ($self) = @_;
275 1         2 local $SCRUBBER=1;
276 1         121 $self->_tx_init;
277 1         5 my %content = $self->content();
278              
279 1         15 my $transactions = [];
280 1 50       2 if ($content{'card_number'}) {
281             # make it so you can submit a single transaction using the normal
282             # BOP content system, this is OPTIONAL
283 1         4 $self->_add_trans($transactions,\%content);
284             }
285 1         1 foreach my $trans (@{$content{'transactions'}}) {
  1         1  
286             # Additional transactions may be submitted using a transactions array
287             # It should follow the same rules that the normal %content hash does
288             # for transactional data
289 4         5 $self->_add_trans($transactions,$trans);
290             }
291              
292 1         3 my $ret = $self->_call_soap('billTransactions', 'transactions', $transactions);
293 1 50 33     52 $self->is_success($ret->{'return'}->{'returnString'} && $ret->{'return'}->{'returnString'} eq 'OK' ? 1 : 0);
294 1         25 $self->order_number($ret->{'return'}->{'soapId'});
295              
296             # make everyone's life easier my making sure this is always an array
297 1 50 33     11 $ret->{'response'} = [$ret->{'response'}] if exists $ret->{'response'} && ref $ret->{'response'} ne 'ARRAY';
298              
299 1         18 $ret;
300             }
301              
302             sub _add_trans {
303 5     5   6 my ($self,$transactions,$content) = @_;
304             my $trans = {
305             subscriptionId => $content->{'subscription_number'},
306             paymentMethodId => $content->{'card_token'},
307             merchantTransactionId => $content->{'invoice_number'},
308             customerId => $content->{'customer_number'},
309             divisionNumber => $content->{'division_number'},
310             authCode => $content->{'authorization'},
311             paymentMethodIsTokenized => 0,
312             status => 'Failed', #we shouldn't be here unless we already failed
313             timestamp => $content->{'timestamp'},
314             amount => $content->{'amount'},
315             currency => $content->{'currency'} || 'USD',
316 5   50     32 creditCardAccount => $content->{'card_number'},
317             };
318 5 50 33     19 if ($content->{'vindicia_nvp'} && ref $content->{'vindicia_nvp'} eq 'HASH') {
319             # A common vindica_nvp would be "vin:Divison"
320 5         21 push @{$trans->{'nameValues'}}, {
321             name => $_,
322             value => $content->{'vindicia_nvp'}->{$_},
323 5 50       3 } foreach grep { !ref $content->{'vindicia_nvp'}->{$_} or die "Invalid vindicia_nvp format" } keys %{$content->{'vindicia_nvp'}};
  5         15  
  5         7  
324             }
325 5         7 push @$transactions, $trans;
326             };
327              
328             =head2 fetchBillingResults
329              
330             Lookup changes in a time period
331              
332             $tx->content(
333             login => 'testdrive',
334             password => '123qwe',
335             action => 'fetchBillingResults',
336             start_timestamp => '2012-09-11T21:34:32.265Z',
337             end_timestamp => '2012-09-11T22:34:32.265Z',
338             page => '0', # optional, defaults to zero
339             page_size => '100', # optional, defaults to 100
340             );
341             my $response = $tx->submit();
342              
343             =cut
344              
345             sub fetchBillingResults {
346 1     1 1 2 my ($self) = @_;
347 1         3 local $SCRUBBER=1;
348 1         115 $self->_tx_init;
349 1         4 my %content = $self->content();
350             my $ret = $self->_call_soap('fetchBillingResults',
351             'timestamp', $content{'start_timestamp'},
352             'endTimestamp', $content{'end_timestamp'},
353             'page', ($content{'page'}//0),
354 1   50     16 'pageSize', ($content{'page_size'}//100),
      50        
355             );
356 1 50 33     34 $self->is_success($ret->{'return'}->{'returnString'} && $ret->{'return'}->{'returnString'} eq 'OK' ? 1 : 0);
357 1         22 $self->order_number($ret->{'return'}->{'soapId'});
358              
359             # make everyone's life easier my making sure this is always an array
360 1 50 33     35 $ret->{'transactions'} = [$ret->{'transactions'}] if exists $ret->{'transactions'} && ref $ret->{'transactions'} ne 'ARRAY';
361              
362 1         5 $ret;
363             }
364              
365             =head2 fetchByMerchantTransactionId
366              
367             Lookup a specific transaction in vindicia
368              
369             $tx->content(
370             login => 'testdrive',
371             password => '123qwe',
372             action => 'fetchByMerchantTransactionId',
373             invoice_number => 'abc123',
374             );
375             my $response = $tx->submit();
376              
377             =cut
378              
379             sub fetchByMerchantTransactionId {
380 1     1 1 3 my ($self) = @_;
381 1         5 local $SCRUBBER=1;
382 1         179 $self->_tx_init;
383 1         7 my %content = $self->content();
384 1         24 my $ret = $self->_call_soap('fetchByMerchantTransactionId', 'merchantTransactionId', $content{'invoice_number'});
385 1 50       46 $self->is_success($ret->{'transaction'} ? 1 : 0);
386 1         35 $self->order_number($ret->{'return'}->{'soapId'});
387 1         16 $ret;
388             }
389              
390             =head2 refundTransactions
391              
392             Cancel or refund (sadly you can't choose one) a transaction.
393              
394             $tx->content(
395             login => 'testdrive',
396             password => '123qwe',
397             action => 'refundTransactions',
398             invoice_number => 'abc123',
399             );
400             my $response = $tx->submit();
401              
402             =cut
403              
404             sub refundTransactions {
405 1     1 1 2 my ($self) = @_;
406 1         3 local $SCRUBBER=1;
407 1         124 $self->_tx_init;
408 1         5 my %content = $self->content();
409              
410 1         15 my @refunds;
411 1 50       5 push @refunds, $content{'invoice_number'} if exists $content{'invoice_number'};
412             # TODO, do we even care to send more than one?
413              
414 1         3 my $ret = $self->_call_soap('refundTransactions', 'refunds', \@refunds);
415 1 50 33     33 $self->is_success($ret->{'return'}->{'returnString'} && $ret->{'return'}->{'returnString'} eq 'OK' ? 1 : 0);
416 1         20 $self->order_number($ret->{'return'}->{'soapId'});
417              
418             # make everyone's life easier my making sure this is always an array
419 1 50 33     8 $ret->{'response'} = [$ret->{'response'}] if exists $ret->{'response'} && ref $ret->{'response'} ne 'ARRAY';
420              
421             # Important note: Vindica does NOT return an error message if the invoice_number is not found.... it simply ignores that invoice
422              
423 1         6 $ret;
424             }
425              
426             sub _call_soap {
427 4     4   8 my $self = shift;
428 4         6 my $action = shift;
429 4         8 my @pairs = @_;
430 4         10 my %content = $self->content();
431              
432 4         51 my $post_data;
433 4         28 my $writer = new XML::Writer(
434             OUTPUT => \$post_data,
435             DATA_MODE => 1,
436             DATA_INDENT => 2,
437             ENCODING => 'UTF-8',
438             );
439              
440 4         602 $writer->xmlDecl();
441 4         136 $writer->startTag('SOAP-ENV:Envelope',
442             "xmlns:ns0" => "http://soap.vindicia.com/v1_1/Select",
443             "xmlns:ns1" => "http://schemas.xmlsoap.org/soap/envelope/",
444             "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
445             "xmlns:SOAP-ENV" => "http://schemas.xmlsoap.org/soap/envelope/",
446             );
447 4         416 $writer->startTag('ns1:Body');
448 4         117 $writer->startTag("ns0:$action");
449 4         101 $writer->startTag('auth');
450 4         142 $writer->dataElement('version', '1.1' );
451 4         252 $writer->dataElement('login', $content{'login'} );
452 4         194 $writer->dataElement('password', $content{'password'});
453 4         197 $writer->dataElement('userAgent', "$me $VERSION" );
454 4         217 $writer->endTag('auth');
455 4         76 while (scalar @pairs) {
456 7         41 my $item = shift @pairs;
457 7         9 my $value = shift @pairs;
458 7         15 $self->_xmlwrite( $writer, $item, $value );
459             }
460 4         73 $writer->endTag("ns0:$action");
461 4         75 $writer->endTag('ns1:Body');
462 4         71 $writer->endTag('SOAP-ENV:Envelope');
463 4         68 $writer->end();
464              
465 4         57 $self->server_request( $post_data );
466              
467 4 50 50     15 if (ref $self->{'mocked'} eq 'ARRAY' && scalar @{$self->{'mocked'}}) {
  4         12  
468 4         5 my $mock = shift @{$self->{'mocked'}};
  4         7  
469 4 50       11 die "Unexpected mock action" unless $mock->{'action'} eq $action;
470 4 50       10 die "Unexpected mock login" unless $mock->{'login'} eq $content{'login'};
471 4 50       18 my $resp = ((ref $mock->{'resp'}) ? $mock->{'resp'} : $self->_common_mock($action,$mock->{'resp'}));
472 4         25 $self->server_response( "MOCKED\n\n".Dumper $resp );
473 4         412 return $resp;
474             }
475              
476 0         0 my $url = 'https://'.$self->server.'/'.$self->path;
477 0         0 my $verify_ssl = 1;
478 0         0 my $response = HTTP::Tiny->new( verify_SSL=>$verify_ssl )->request('POST', $url, {
479             headers => {
480             'Content-Type' => 'text/xml;charset=UTF-8',
481             'SOAPAction' => "http://soap.vindicia.com/v1_1/Select#$action",
482             },
483             content => $post_data,
484             } );
485 0         0 $self->server_response( $response->{'content'} );
486 0   0     0 my $resp = eval { XMLin($response->{'content'})->{'soap:Body'}->{$action.'Response'} } || {};
487 0         0 $resp = $self->_resp_simplify($resp);
488             #use Data::Dumper; warn Dumper $post_data,$response->{'content'},$resp;
489 0         0 $resp;
490             }
491              
492             sub _resp_simplify {
493 0     0   0 my ($self,$resp) = @_;
494 0         0 delete $resp->{'xmlns'};
495 0         0 foreach my $t (keys %$resp) {
496 0 0       0 if (ref $resp->{$t} eq 'ARRAY') {
    0          
497 0         0 $resp->{$t} = $self->_resp_simplify_array($resp->{$t});
498             } elsif (ref $resp->{$t} eq 'HASH') {
499 0         0 $resp->{$t} = $self->_resp_simplify_hash($resp->{$t});
500             }
501             }
502 0         0 $resp;
503             }
504              
505             sub _resp_simplify_array {
506 0     0   0 my ($self,$resp) = @_;
507 0         0 foreach my $value (@$resp) {
508 0         0 $self->_resp_simplify_hash($value);
509             }
510 0         0 $resp;
511             }
512              
513             sub _resp_simplify_hash {
514 0     0   0 my ($self,$resp) = @_;
515 0         0 delete $resp->{'xsi:type'};
516 0         0 delete $resp->{'xmlns'};
517 0         0 foreach my $t (keys %{$resp}) {
  0         0  
518 0 0 0     0 if ($t eq 'nameValues') {
    0 0        
519 0         0 my $arr = $resp->{$t};
520 0 0       0 $arr = [$arr] unless ref $arr eq 'ARRAY';
521 0         0 my $hash = {};
522 0         0 foreach my $t2 (@$arr) {
523 0         0 my $n = $t2->{'name'}->{'content'};
524 0         0 my $v = $t2->{'value'}->{'content'};
525 0 0       0 if (!exists $hash->{$n}) {
    0          
526 0         0 $hash->{$n} = $v;
527             } elsif (ref $hash->{$n}) {
528 0         0 push @{$hash->{$n}}, $v;
  0         0  
529             } else {
530 0         0 $hash->{$n} = [$hash->{$n},$v];
531             }
532             }
533 0         0 $resp->{$t} = $hash;
534             } elsif (ref $resp->{$t} eq 'HASH' && (exists $resp->{$t}->{'content'} || exists $resp->{$t}->{'xmlns'})) {
535 0         0 $resp->{$t} = $resp->{$t}->{'content'};
536             }
537             }
538 0         0 $resp;
539             }
540              
541             sub _default_scrubber {
542 6     6   7 my $cc = shift;
543 6         7 my $del = 'DELETED';
544 6 50       13 if (length($cc) > 11) {
    0          
545 6         23 $del = substr($cc,0,6).('X'x(length($cc)-10)).substr($cc,-4,4); # show first 6 and last 4
546             } elsif (length($cc) > 5) {
547 0         0 $del = substr($cc,0,2).('X'x(length($cc)-4)).substr($cc,-2,2); # show first 2 and last 2
548             } else {
549 0         0 $del = ('X'x(length($cc)-2)).substr($cc,-2,2); # show last 2
550             }
551 6         21 return $del;
552             }
553              
554             sub _scrubber_add_card {
555 16     16   29 my ( $self, $cc ) = @_;
556 16 100       35 return if ! $cc;
557 6         7 my $scrubber = $self->{_scrubber};
558 6         10 scrubber_add_scrubber({quotemeta($cc)=>&{$scrubber}($cc)});
  6         9  
559             }
560              
561             sub _tx_init {
562 8     8   12 my ( $self, $opts ) = @_;
563              
564             # initialize/reset the reporting methods
565 8         205 $self->is_success(0);
566 8         53 $self->server_request('');
567 8         25 $self->server_response('');
568 8         149 $self->error_message('');
569              
570             # some calls are passed via the content method, others are direct arguments... this way we cover both
571 8         52 my %content = $self->content();
572 8         155 foreach my $ptr (\%content,$opts) {
573 16 100       32 next if ! $ptr;
574             scrubber_init({
575             quotemeta($ptr->{'password'}||'')=>'DELETED',
576 8 50 50     55 ($ptr->{'cvv2'} ? '(?<=[^\d])'.quotemeta($ptr->{'cvv2'}).'(?=[^\d])' : '')=>'DELETED',
577             });
578 8         1348 $self->_scrubber_add_card($ptr->{'card_number'});
579 8         62 $self->_scrubber_add_card($ptr->{'account_number'});
580             }
581             }
582              
583             sub _xmlwrite {
584 93     93   84 my ( $self, $writer, $item, $value ) = @_;
585 93 100       135 if ( ref($value) eq 'HASH' ) {
    100          
586 10 50       13 my $attr = $value->{'attr'} ? $value->{'attr'} : {};
587 10         8 $writer->startTag( $item, %{$attr} );
  10         20  
588 10         238 foreach ( keys(%$value) ) {
589 75 50       775 next if $_ eq 'attr';
590 75         89 $self->_xmlwrite( $writer, $_, $value->{$_} );
591             }
592 10         123 $writer->endTag($item);
593             } elsif ( ref($value) eq 'ARRAY' ) {
594 7         4 foreach ( @{$value} ) {
  7         12  
595 11         74 $self->_xmlwrite( $writer, $item, $_ );
596             }
597             } else {
598 76         92 $writer->startTag($item);
599 76         1708 $writer->characters($value);
600 76         800 $writer->endTag($item);
601             }
602             }
603              
604             our $common_mock = {
605             billTransactions => {
606             ok => {
607             'return' => {
608             'returnString' => 'OK',
609             'returnCode' => '200',
610             'soapId' => 'aaaaaa4817abcba350f9bded7024a44d9e03b42b',
611             },
612             },
613             ok_duplicate => {
614             'return' => {
615             'returnString' => 'OK',
616             'returnCode' => '200',
617             'soapId' => 'aaaaaa4817abcba350f9bded7024a44d9e03b42b',
618             },
619             'response' => [
620             {
621             'code' => '400',
622             'merchantTransactionId' => 'TEST-1477512979.48453-3',
623             'description' => 'Billing has already been attempted for Transaction ID TEST-1477512979.48453-3'
624             },
625             ],
626             },
627             },
628             fetchBillingResults => {
629             ok => {
630             'return' => {
631             'returnString' => 'OK',
632             'returnCode' => '200',
633             'soapId' => 'aaaaaa4817abcba350f9bded7024a44d9e03b42b',
634             },
635             },
636             },
637             refundTransactions => {
638             ok => {
639             'return' => {
640             'returnString' => 'OK',
641             'returnCode' => '200',
642             'soapId' => 'aaaaaa4817abcba350f9bded7024a44d9e03b42b',
643             },
644             },
645             },
646             fetchByMerchantTransactionId => {
647             ok => {
648             'return' => {
649             'returnCode' => '200',
650             'returnString' => 'OK',
651             'soapId' => 'aaaaaa727eca030a16ddad54e4eaf8088a5fa322'
652             },
653             'transaction' => {
654             'currency' => 'USD',
655             'authCode' => '123456',
656             'selectTransactionId' => 'TEST-1477513825.95777',
657             'amount' => '9000',
658             'subscriptionId' => 'TEST-1477513825.95775',
659             'paymentMethodIsTokenized' => '0',
660             'creditCardAccountHash' => 'aaaaaa96f35af3876fc509665b3dc23a0930aab1',
661             'nameValues' => {
662             'vin:BillingCycle' => '0',
663             'vin:RetryNumber' => '0'
664             },
665             'paymentMethodId' => '1',
666             'divisionNumber' => '1',
667             'subscriptionStartDate' => '2016-10-26T13:30:26-07:00',
668             'creditCardAccount' => '411111XXXXXX1111',
669             'status' => 'Failed',
670             'VID' => 'aaaaaa5286ba2a8199a651d9f7afbee9a015fbb2',
671             'customerId' => '123',
672             'timestamp' => '2012-09-11T15:34:32-07:00',
673             'merchantTransactionId' => 'TEST-1477513825.95777'
674             }
675             },
676             },
677             };
678             sub _common_mock {
679 4     4   7 my ($self,$action,$label) = @_;
680 4   50     16 return $common_mock->{$action}->{$label} || die 'Mock label not found, label: '.$label."\n";
681             }
682              
683             1;