File Coverage

blib/lib/Business/OnlinePayment/CardConnect.pm
Criterion Covered Total %
statement 133 196 67.8
branch 28 62 45.1
condition 5 21 23.8
subroutine 24 29 82.7
pod 7 7 100.0
total 197 315 62.5


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::CardConnect;
2 2     2   54065 use warnings;
  2         7  
  2         76  
3 2     2   13 use strict;
  2         5  
  2         52  
4              
5 2     2   303 use Business::OnlinePayment;
  2         2490  
  2         57  
6 2     2   628 use Business::OnlinePayment::HTTPS;
  2         33376  
  2         70  
7 2     2   17 use vars qw(@ISA $me $DEBUG);
  2         3  
  2         107  
8 2     2   10 use URI::Escape;
  2         5  
  2         89  
9 2     2   927 use HTTP::Tiny;
  2         67446  
  2         88  
10 2     2   916 use JSON qw(to_json from_json);
  2         15117  
  2         14  
11 2     2   980 use Business::CreditCard qw(cardtype);
  2         2597  
  2         124  
12 2     2   754 use Data::Dumper;
  2         10718  
  2         162  
13 2     2   21 use Carp qw(croak);
  2         6  
  2         104  
14 2     2   782 use Log::Scrubber qw(disable $SCRUBBER scrubber :Carp scrubber_add_scrubber);
  2         9577  
  2         31  
15              
16             @ISA = qw(Business::OnlinePayment::HTTPS);
17             $me = 'Business::OnlinePayment::CardConnect';
18             $DEBUG = 0;
19             our $VERSION = '0.004'; # VERSION
20              
21             # PODNAME: Business::OnlinePayment::CardConnect
22              
23             # ABSTRACT: Business::OnlinePayment::CardConnect - CardConnect Backend for Business::OnlinePayment
24              
25             =head1 SYNOPSIS
26              
27             This is a plugin for the Business::OnlinePayment interface. Please refer to that documentation for general usage, and here for CardConnect specific usage.
28              
29             In order to use this module, you will need to have an account set up with CardConnect L
30              
31             use Business::OnlinePayment;
32             my $tx = Business::OnlinePayment->new("CardConnect");
33              
34             $tx->content(
35             type => 'CC',
36             login => 'testdrive',
37             password => '123qwe',
38             action => 'Normal Authorization',
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             );
52             $tx->submit();
53              
54             if($tx->is_success()) {
55             print "Card processed successfully: ".$tx->authorization."\n";
56             } else {
57             print "Card was rejected: ".$tx->error_message."\n";
58             }
59              
60             =head1 METHODS AND FUNCTIONS
61              
62             See L for the complete list. The following methods either override the methods in L or provide additional functions.
63              
64             =head2 result_code
65              
66             Returns the response error code.
67              
68             =head2 error_message
69              
70             Returns the response error description text.
71              
72             =head2 server_request
73              
74             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.
75              
76             =cut
77              
78             sub server_request {
79 2     2 1 5 my ( $self, $val, $tf ) = @_;
80 2 100       6 if ($val) {
81 1         4 $self->{server_request} = scrubber $val;
82 1 50       102 $self->server_request_dangerous($val,1) unless $tf;
83             }
84 2         4 return $self->{server_request};
85             }
86              
87             =head2 server_request_dangerous
88              
89             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.
90              
91             =cut
92              
93             sub server_request_dangerous {
94 1     1 1 3 my ( $self, $val, $tf ) = @_;
95 1 50       4 if ($val) {
96 1         3 $self->{server_request_dangerous} = $val;
97 1 50       3 $self->server_request($val,1) unless $tf;
98             }
99 1         2 return $self->{server_request_dangerous};
100             }
101              
102             =head2 server_response
103              
104             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.
105              
106             =cut
107              
108             sub server_response {
109 2     2 1 436 my ( $self, $val, $tf ) = @_;
110 2 100       9 if ($val) {
111 1         9 $self->{server_response} = scrubber $val;
112 1 50       1513 $self->server_response_dangerous($val,1) unless $tf;
113             }
114 2         7 return $self->{server_response};
115             }
116              
117             =head2 server_response_dangerous
118              
119             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.
120              
121             =cut
122              
123             sub server_response_dangerous {
124 1     1 1 7 my ( $self, $val, $tf ) = @_;
125 1 50       5 if ($val) {
126 1         3 $self->{server_response_dangerous} = $val;
127 1 50       6 $self->server_response($val,1) unless $tf;
128             }
129 1         5 return $self->{server_response_dangerous};
130             }
131              
132             =head1 Handling of content(%content) data:
133              
134             =head2 action
135              
136             The following actions are valid
137              
138             normal authorization
139             authorization only
140             post authorization
141             credit
142             void
143             auth reversal
144              
145             =head1 TESTING
146              
147             In order to run the provided test suite, you will first need to apply and get your account setup with CyberSource. Then you can use the test account information they give you to run the test suite. The scripts will look for three environment variables to connect: BOP_USERNAME, BOP_PASSWORD, BOP_MERCHANTID
148              
149             =head1 FUNCTIONS
150              
151             =head2 _info
152              
153             Return the introspection hash for BOP 3.x
154              
155             =cut
156              
157             =head2 _info
158              
159             Return the introspection hash for BOP 3.x
160              
161             =cut
162              
163             sub _info {
164             return {
165 0     0   0 info_compat => '0.01',
166             gateway_name => 'CyberSource - SOAP Toolkit API',
167             gateway_url => 'http://www.cybersource.com',
168             module_version => $Business::OnlinePayment::CardConnect::VERSION,
169             supported_types => ['CC','ECHECK'],
170             supported_actions => {
171             CC => [
172             'Normal Authorization',
173             'Post Authorization',
174             'Authorization Only',
175             'Credit',
176             'Void',
177             'Auth Reversal',
178             ],
179             },
180             };
181             }
182              
183             =head2 set_defaults
184              
185             Used by BOP to set default values during "new"
186              
187             =cut
188              
189             sub set_defaults {
190 1     1 1 43 my $self = shift;
191 1         2 my %opts = @_;
192              
193 1         7 $self->build_subs(
194             qw( order_number md5 avs_code cvv2_response card_token cavv_response failure_status verify_SSL )
195             );
196              
197 1         232 $self->build_subs( # built only for backwards compatibily with old cybersource moose version
198             qw( response_code response_headers response_page login password require_avs )
199             );
200              
201 1         206 $self->test_transaction(0);
202 1         11 $self->{_scrubber} = \&_default_scrubber;
203             }
204              
205             =head2 test_transaction
206              
207             Get/set the server used for processing transactions. Possible values are Live, Certification, and Sandbox
208             Default: Live
209              
210             #Live
211             $self->test_transaction(0);
212              
213             #Test
214             $self->test_transaction(1); # currently not different from live
215              
216             #Read current value
217             $val = $self->test_transaction();
218              
219             =cut
220              
221             sub test_transaction {
222 2     2 1 681 my $self = shift;
223 2         5 my $testMode = shift;
224 2 50 0     8 if (! defined $testMode) { $testMode = $self->{'test_transaction'} || 0; }
  0         0  
225              
226 2         51 $self->require_avs(0);
227 2         78 $self->verify_SSL(0);
228 2         43 $self->port('6443');
229 2         43 $self->path('/cardconnect/rest/auth');
230 2         43 $self->server('fts.cardconnect.com');
231 2         42 $self->SUPER::test_transaction($testMode);
232             }
233              
234             =head2 submit
235              
236             Submit your transaction to cybersource
237              
238             =cut
239              
240             sub submit {
241 0     0 1 0 my ($self) = @_;
242 0         0 local $SCRUBBER=1;
243 0         0 $self->_cardconnect_init;
244 0         0 my %content = $self->content();
245              
246 0         0 my $action_map = {
247             'Normal Authorization' => 'auth', # this method auto detects when capture is needed
248             'Authorization Only' => 'auth',
249             'Post Authorization' => 'capture',
250             'Void' => 'void',
251             'Auth Reversal' => 'void',
252             'Credit' => 'refund',
253             };
254 0   0     0 my $action = $action_map->{$content{'action'}} || die "Unsupported action: ".$content{'action'};
255 0 0 0     0 die 'Amount must contain a decimal' if defined $content{'amount'} && $content{'amount'} !~ /\./;
256              
257 0         0 my $method = '_cardconnect_'.$action;
258 0         0 return $self->$method();
259             }
260              
261             sub _cardconnect_void {
262 0     0   0 my ($self) = @_;
263 0         0 my %content = $self->content();
264              
265             my $post_data = {
266             retref => $content{'order_number'},
267 0         0 merchid => $content{'merchantid'},
268             };
269              
270 0         0 my $page = $self->_do_put_request( 'void', $post_data );
271 0         0 my $response = $page->{'content_json'};
272              
273 0 0       0 $self->is_success($response->{'respstat'} eq 'A' ? $response : undef);
274 0         0 $self->result_code($response->{'respstat'});
275 0         0 $self->order_number($response->{'retref'});
276 0         0 $self->error_message($response->{'resptext'});
277              
278 0         0 return $response;
279             }
280              
281             sub _cardconnect_refund {
282 0     0   0 my ($self) = @_;
283 0         0 my %content = $self->content();
284              
285             my $post_data = {
286             retref => $content{'order_number'},
287 0         0 merchid => $content{'merchantid'},
288             };
289 0 0       0 $post_data->{'amount'} = $content{'amount'} if defined $content{'amount'};
290              
291 0         0 my $page = $self->_do_put_request( 'capture', $post_data );
292 0         0 my $response = $page->{'content_json'};
293              
294 0 0       0 $self->is_success($response->{'respstat'} eq 'A' ? $response : undef);
295 0         0 $self->result_code($response->{'respstat'});
296 0         0 $self->order_number($response->{'retref'});
297 0         0 $self->error_message($response->{'resptext'});
298 0         0 $self->order_number($response->{'retref'});
299 0         0 $self->error_message($response->{'resptext'});
300 0         0 $self->card_token($response->{'token'});
301              
302 0         0 return $response;
303             }
304              
305             sub _cardconnect_capture {
306 0     0   0 my ($self) = @_;
307 0         0 my %content = $self->content();
308              
309             my $post_data = {
310             retref => $content{'order_number'},
311 0         0 merchid => $content{'merchantid'},
312             };
313 0 0       0 $post_data->{'amount'} = $content{'amount'} if defined $content{'amount'};
314 0         0 $self->_cardconnect_add_level2($post_data);
315              
316 0         0 my $page = $self->_do_put_request( 'capture', $post_data );
317 0         0 my $response = $page->{'content_json'};
318              
319 0 0       0 $self->is_success($response->{'respstat'} eq 'A' ? $response : undef);
320 0         0 $self->result_code($response->{'respstat'});
321 0         0 $self->order_number($response->{'retref'});
322 0         0 $self->error_message($response->{'resptext'});
323 0         0 $self->order_number($response->{'retref'});
324 0         0 $self->error_message($response->{'resptext'});
325 0         0 $self->card_token($response->{'token'});
326              
327 0         0 return $response;
328             }
329              
330             sub _cardconnect_add_level2 {
331 1     1   3 my ($self,$post_data) = @_;
332 1         2 my %content = $self->content();
333 1 50       14 $post_data->{'ponumber'} = $content{'po_number'} if defined $content{'po_number'};
334 1 50       6 $post_data->{'shiptozip'} = $content{'ship_zip'} if defined $content{'ship_zip'};
335 1 50       2 $post_data->{'taxamnt'} = $content{'tax'} if defined $content{'tax'};
336 1 50 33     5 if ( defined $content{'products'} && scalar( @{ $content{'products'} } ) < 100 ) {
  0         0  
337 0         0 my @products;
338 0         0 my $lineno = 0;
339 0         0 foreach my $productOrig ( @{ $content{'products'} } ) {
  0         0  
340 0         0 $lineno++;
341             my $item = {
342             "discamnt" => $productOrig->{'discount'},
343             "unitcost" => $productOrig->{'cost'},
344             "lineno" => $lineno,
345             "description" => $productOrig->{'description'},
346             "taxamnt" => $productOrig->{'tax'},
347             "quantity" => $productOrig->{'quantity'},
348 0         0 "netamnt" => $productOrig->{'amount'},
349             #"upc" => "UPC-1",
350             #"material" => "MATERIAL-1"
351             };
352 0         0 push @products, $item;
353             }
354             }
355             }
356              
357             sub _cardconnect_auth {
358 1     1   2 my ($self) = @_;
359 1         3 my %content = $self->content();
360              
361 1         18 my $post_data = {};
362 1 50 33     5 if ($content{'routing_code'} && $content{'account_number'}) {
    50          
363             $post_data = {
364             accttype => 'ECHK',
365             account => $content{'account_number'},
366             bankaba => $content{'routing_code'},
367             merchid => $content{'merchantid'},
368             name => $content{'first_name'}.' '.$content{'last_name'},
369             amount => $content{'amount'},
370 0   0     0 currency => $content{'currency'} || "USD",
371             }
372             } elsif ($content{'card_number'}) {
373 1         4 $content{'expiration'} =~ s/\///; # CardConnect doesn't want the / between MM and YY
374             $post_data = {
375             merchid => $content{'merchantid'},
376             orderid => $content{'invoice_number'},
377              
378             account => $content{'card_number'},
379             expiry => $content{'expiration'},
380             cvv2 => $content{'cvv2'},
381              
382             amount => $content{'amount'},
383             currency => $content{'currency'} || "USD",
384             name => $content{'first_name'}.' '.$content{'last_name'},
385             address => $content{'address'},
386             city => $content{'city'},
387             region => $content{'state'},
388             country => $content{'country'},
389             postal => $content{'zip'},
390             email => $content{'email'},
391             ecomind => "E",
392             track => undef,
393             tokenize => "Y",
394             userfields => [
395 1   50     16 { description => $content{'description'} },
396             ],
397             };
398 1         3 $self->_cardconnect_add_level2($post_data);
399             } else {
400 0         0 die 'Unsupported payment method';
401             }
402 1 50       3 $post_data->{'capture'} = "Y" if $content{'action'} eq 'Normal Authorization';
403              
404 1         3 my $page = $self->_do_put_request( 'auth', $post_data );
405 1         7 my $response = $page->{'content_json'};
406              
407 1 50       43 $self->is_success($response->{'respstat'} eq 'A' ? $response : undef);
408 1         55 $self->result_code($response->{'respstat'});
409 1         55 $self->authorization($response->{'authcode'});
410 1         56 $self->order_number($response->{'retref'});
411 1         50 $self->error_message($response->{'resptext'});
412 1         53 $self->card_token($response->{'token'});
413 1         57 $self->avs_code($response->{'avsresp'});
414 1         52 $self->cvv2_response($response->{'cvvresp'});
415              
416 1         49 return $response;
417             }
418              
419             sub _do_put_request {
420 1     1   2 my ($self, $action, $post_data) = @_;
421 1         3 my %content = $self->content(); # needed for basic auth
422 1         19 my $options = {
423             headers => {
424             'Content-Type' => 'application/json',
425             },
426             content => to_json $post_data,
427             };
428 1         98 $self->login($content{'login'});
429 1         24 $self->password($content{'password'});
430 1         9 my $url= 'https://'.uri_escape($content{'login'}).':'.uri_escape($content{'password'}).'@fts.cardconnect.com:6443/cardconnect/rest/'.$action;
431 1         39 $self->server_request( $url."\n\n".$options->{'content'} );
432 1 50       2 warn $self->server_request if $DEBUG;
433 1         747 my $page = HTTP::Tiny->new->request('PUT', $url, $options);
434 1         651745 $self->server_response( $page );
435 1 50       5 warn Dumper $self->server_response if $DEBUG;
436 1 50       9 if ($page->{'status'} eq '200') {
    50          
437 0         0 $page->{'content_json'} = eval { from_json $page->{'content'}; }
  0         0  
438             } elsif ($page->{'status'} eq '401') {
439 1         10 $page->{'content_json'} = {
440             respstat => 'U',
441             resptext => 'This request requires authentication.',
442             };
443             } else {
444 0         0 $page->{'content_json'} = {
445             respstat => 'U',
446             resptext => 'Unknown response from payment gateway.',
447             };
448             }
449 1         5 my $e = $@;
450 1 50       6 die "Could not process JSON: ".$e if ($e);
451 1         66 $self->response_code($page->{'status'});
452 1         55 $self->response_headers($page->{'headers'});
453 1         52 $self->response_page($page->{'content'});
454 1         30 return $page;
455             }
456              
457             sub _default_scrubber {
458 1     1   4 my $cc = shift;
459 1         6 my $del = substr($cc,0,6).('X'x(length($cc)-10)).substr($cc,-4,4); # show first 6 and last 4
460 1         8 return $del;
461             }
462              
463             sub _cardconnect_scrubber_add_card {
464 1     1   2 my ( $self, $cc ) = @_;
465 1 50       4 return if ! $cc;
466 1         2 my $scrubber = $self->{_scrubber};
467 1         42 scrubber_add_scrubber({quotemeta($cc)=>&{$scrubber}($cc)});
  1         6  
468             }
469              
470             sub _cardconnect_init {
471 1     1   3 my ( $self, $opts ) = @_;
472              
473             # initialize/reset the reporting methods
474 1         22 $self->is_success(0);
475 1         10 $self->server_request('');
476 1         7 $self->server_response('');
477 1         19 $self->error_message('');
478              
479             # some calls are passed via the content method, others are direct arguments... this way we cover both
480 1         8 my %content = $self->content();
481 1         20 foreach my $ptr (\%content,$opts) {
482 2 100       19 next if ! $ptr;
483             scrubber_init({
484             quotemeta($ptr->{'password'}||'')=>'DELETED',
485             quotemeta($ptr->{'ftp_password'}||'')=>'DELETED',
486 1 50 50     22 ($ptr->{'cvv2'} ? '(?<=[^\d])'.quotemeta($ptr->{'cvv2'}).'(?=[^\d])' : '')=>'DELETED',
      50        
487             });
488 1         202 $self->_cardconnect_scrubber_add_card($ptr->{'card_number'});
489             }
490             }
491              
492             1;