File Coverage

blib/lib/Business/OnlinePayment/Vindicia/Select.pm
Criterion Covered Total %
statement 196 240 81.6
branch 40 82 48.7
condition 16 49 32.6
subroutine 29 32 90.6
pod 10 10 100.0
total 291 413 70.4


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