File Coverage

blib/lib/Business/OnlinePayment/IPPay.pm
Criterion Covered Total %
statement 91 195 46.6
branch 30 114 26.3
condition 12 63 19.0
subroutine 16 18 88.8
pod 1 6 16.6
total 150 396 37.8


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::IPPay;
2              
3 4     4   21674 use strict;
  4         10  
  4         121  
4 4     4   20 use Carp;
  4         8  
  4         210  
5 4     4   2137 use Tie::IxHash;
  4         18148  
  4         121  
6 4     4   3393 use XML::Simple;
  4         36835  
  4         27  
7 4     4   2868 use XML::Writer;
  4         58196  
  4         132  
8 4     4   2079 use Locale::Country;
  4         172891  
  4         344  
9 4     4   645 use Business::OnlinePayment;
  4         3585  
  4         102  
10 4     4   2129 use Business::OnlinePayment::HTTPS;
  4         73574  
  4         177  
11 4     4   32 use vars qw($VERSION $DEBUG @ISA $me);
  4         10  
  4         10589  
12              
13             @ISA = qw(Business::OnlinePayment::HTTPS);
14             $VERSION = '0.11';
15             $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
16              
17             $DEBUG = 0;
18             $me = 'Business::OnlinePayment::IPPay';
19              
20             sub _info {
21             {
22 1     1   155 'info_compat' => '0.01',
23             'module_version' => $VERSION,
24             'supported_types' => [ qw( CC ECHECK ) ],
25             'supported_actions' => { 'CC' => [
26             'Normal Authorization',
27             'Authorization Only',
28             'Post Authorization',
29             'Void',
30             'Credit',
31             'Reverse Authorization',
32             ],
33             'ECHECK' => [
34             'Normal Authorization',
35             'Void',
36             'Credit',
37             ],
38             },
39             'CC_void_requires_card' => 1,
40             'ECHECK_void_requires_account' => 1,
41             };
42             }
43              
44             sub set_defaults {
45 11     11 0 36111 my $self = shift;
46 11         88 my %opts = @_;
47              
48             # standard B::OP methods/data
49 11 50       281 $self->server('gtwy.ippay.com') unless $self->server;
50 11 50       707 $self->port('443') unless $self->port;
51 11 50       587 $self->path('/ippay') unless $self->path;
52              
53 11         410 $self->build_subs(qw( order_number avs_code cvv2_response
54             response_page response_code response_headers
55             ));
56              
57 11 50       553 $DEBUG = exists($opts{debug}) ? $opts{debug} : 0;
58              
59             # module specific data
60 11         36 my %_defaults = ();
61 11         40 foreach my $key (keys %opts) {
62 11 50       75 $key =~ /^default_(\w*)$/ or next;
63 11         44 $_defaults{$1} = $opts{$key};
64 11         34 delete $opts{$key};
65             }
66 11         57 $self->{_defaults} = \%_defaults;
67             }
68              
69             sub map_fields {
70 10     10 0 28 my($self) = @_;
71              
72 10         32 my %content = $self->content();
73              
74             # TYPE MAP
75 10         246 my %types = ( 'visa' => 'CC',
76             'mastercard' => 'CC',
77             'american express' => 'CC',
78             'discover' => 'CC',
79             'check' => 'ECHECK',
80             );
81 10   33     60 $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
82 10         207 $self->transaction_type($content{'type'});
83            
84             # ACTION MAP
85 10         92 my $action = lc($content{'action'});
86 10         82 my %actions =
87             ( 'normal authorization' => 'SALE',
88             'authorization only' => 'AUTHONLY',
89             'post authorization' => 'CAPT',
90             'reverse authorization' => 'REVERSEAUTH',
91             'void' => 'VOID',
92             'credit' => 'CREDIT',
93             );
94 10         48 my %check_actions =
95             ( 'normal authorization' => 'CHECK',
96             'void' => 'VOIDACH',
97             'credit' => 'REVERSAL',
98             );
99              
100 10 100       185 if ($self->transaction_type eq 'CC') {
    50          
101 8   33     96 $content{'TransactionType'} = $actions{$action} || $action;
102             } elsif ($self->transaction_type eq 'ECHECK') {
103              
104 2   33     77 $content{'TransactionType'} = $check_actions{$action} || $action;
105              
106             # ACCOUNT TYPE MAP
107 2         13 my %account_types = ('personal checking' => 'CHECKING',
108             'personal savings' => 'SAVINGS',
109             'business checking' => 'CHECKING',
110             'business savings' => 'SAVINGS',
111             #not technically B:OP valid i guess?
112             'checking' => 'CHECKING',
113             'savings' => 'SAVINGS',
114             );
115             $content{'account_type'} = $account_types{lc($content{'account_type'})}
116 2   33     11 || $content{'account_type'};
117             }
118              
119             $content{Origin} = 'RECURRING'
120 10 50 33     128 if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
121              
122             # stuff it back into %content
123 10         64 $self->content(%content);
124              
125             }
126              
127             sub expdate_month {
128 20     20 0 105 my ($self, $exp) = (shift, shift);
129 20         44 my $month;
130 20 100 66     182 if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
    50 33        
131 8         88 $month = sprintf( "%02d", $1 );
132             }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
133 0         0 $month = sprintf( "%02d", $1 );
134             }
135 20         54 return $month;
136             }
137              
138             sub expdate_year {
139 20     20 0 59 my ($self, $exp) = (shift, shift);
140 20         41 my $year;
141 20 100 66     141 if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
    50 33        
142 8         37 $year = sprintf( "%02d", $1 );
143             }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
144 0         0 $year = sprintf( "%02d", $1 );
145             }
146 20         52 return $year;
147             }
148              
149             sub revmap_fields {
150 58     58 0 110 my $self = shift;
151 58         253 tie my(%map), 'Tie::IxHash', @_;
152 58         7992 my %content = $self->content();
153             map {
154 58         1165 my $value;
  560         3462  
155 560 100       1504 if ( ref( $map{$_} ) eq 'HASH' ) {
    100          
    100          
156 40 100       262 $value = $map{$_} if ( keys %{ $map{$_} } );
  40         101  
157             }elsif( ref( $map{$_} ) ) {
158 108         1271 $value = ${ $map{$_} };
  108         277  
159             }elsif( exists( $content{ $map{$_} } ) ) {
160 135         2317 $value = $content{ $map{$_} };
161             }
162              
163 560 100       7240 if (defined($value)) {
164 217         672 ($_ => $value);
165             }else{
166 343         873 ();
167             }
168             } (keys %map);
169             }
170              
171             sub submit {
172 0     0 1 0 my($self) = @_;
173              
174 0         0 $self->is_success(0);
175 0         0 $self->map_fields();
176              
177 0         0 my @required_fields = qw(action login password type);
178              
179 0         0 my $action = lc($self->{_content}->{action});
180 0         0 my $type = $self->transaction_type();
181 0 0 0     0 if ( $action eq 'normal authorization'
    0 0        
    0 0        
    0 0        
      0        
182             || $action eq 'credit'
183             || $action eq 'authorization only' && $type eq 'CC')
184             {
185 0         0 push @required_fields, qw( amount );
186              
187 0 0       0 push @required_fields, qw( card_number expiration )
188             if ($type eq "CC");
189            
190 0 0       0 push @required_fields,
191             qw( routing_code account_number name ) # account_type
192             if ($type eq "ECHECK");
193            
194             }elsif ( $action eq 'post authorization' && $type eq 'CC') {
195 0         0 push @required_fields, qw( order_number );
196             }elsif ( $action eq 'reverse authorization' && $type eq 'CC') {
197 0         0 push @required_fields, qw( order_number card_number expiration amount );
198             }elsif ( $action eq 'void') {
199 0         0 push @required_fields, qw( order_number amount );
200              
201 0 0       0 push @required_fields, qw( authorization card_number )
202             if ($type eq "CC");
203              
204 0 0       0 push @required_fields,
205             qw( routing_code account_number name ) # account_type
206             if ($type eq "ECHECK");
207              
208             }else{
209             croak "$me can't handle transaction type: ".
210 0         0 $self->{_content}->{action}. " for ".
211             $self->transaction_type();
212             }
213              
214 0         0 my %content = $self->content();
215 0         0 foreach ( keys ( %{($self->{_defaults})} ) ) {
  0         0  
216 0 0       0 $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
217             }
218 0 0       0 if ($self->test_transaction()) {
219 0         0 $content{'login'} = 'TESTTERMINAL';
220 0 0       0 $self->server('testgtwy.ippay.com') if $self->server eq 'gtwy.ippay.com';
221             }
222 0         0 $self->content(%content);
223              
224 0         0 $self->required_fields(@required_fields);
225              
226             #quick validation because ippay dumps an error indecipherable to the end user
227 0 0       0 if (grep { /^routing_code$/ } @required_fields) {
  0         0  
228 0 0       0 unless( $content{routing_code} =~ /^\d{9}$/ ) {
229 0         0 $self->_error_response('Invalid routing code');
230 0         0 return;
231             }
232             }
233              
234 0         0 my $transaction_id = $content{'order_number'};
235 0 0       0 unless ($transaction_id) {
236 0         0 my ($page, $server_response, %headers) = $self->https_get('dummy' => 1);
237             warn "fetched transaction id: (HTTPS response: $server_response) ".
238             "(HTTPS headers: ".
239 0 0       0 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
  0         0  
240             "(Raw HTTPS content: $page)"
241             if $DEBUG > 1;
242 0 0       0 return unless $server_response=~ /^200/;
243 0         0 $transaction_id = $page;
244             }
245              
246 0         0 my $cardexpmonth = $self->expdate_month($content{expiration});
247 0         0 my $cardexpyear = $self->expdate_year($content{expiration});
248 0         0 my $cardstartmonth = $self->expdate_month($content{card_start});
249 0         0 my $cardstartyear = $self->expdate_year($content{card_start});
250            
251 0         0 my $amount;
252 0 0       0 if (defined($content{amount})) {
253 0         0 $amount = sprintf("%.2f", $content{amount});
254 0         0 $amount =~ s/\.//;
255             }
256              
257             my $check_number = $content{check_number} || "100" # make one up
258 0 0 0     0 if($content{account_number});
259              
260 0 0       0 my $terminalid = $content{login} if $type eq 'CC';
261 0 0       0 my $merchantid = $content{login} if $type eq 'ECHECK';
262              
263 0         0 my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
264             $country = country_code2code( $content{country},
265 0 0       0 LOCALE_CODE_ALPHA_2,
266             LOCALE_CODE_ALPHA_3
267             )
268             unless $country;
269             $country = $content{country}
270 0 0       0 unless $country;
271 0 0       0 $country = uc($country) if $country;
272              
273             my $ship_country =
274 0         0 country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
275             $ship_country = country_code2code( $content{ship_country},
276 0 0       0 LOCALE_CODE_ALPHA_2,
277             LOCALE_CODE_ALPHA_3
278             )
279             unless $ship_country;
280             $ship_country = $content{ship_country}
281 0 0       0 unless $ship_country;
282 0 0       0 $ship_country = uc($ship_country) if $ship_country;
283              
284 0         0 tie my %ach, 'Tie::IxHash',
285             $self->revmap_fields(
286             #wtf, this is a "Type"" attribute of the ACH element,
287             # not a child element like the others
288             #AccountType => 'account_type',
289             AccountNumber => 'account_number',
290             ABA => 'routing_code',
291             CheckNumber => \$check_number,
292             );
293              
294 0         0 tie my %industryinfo, 'Tie::IxHash',
295             $self->revmap_fields(
296             Type => 'IndustryInfo',
297             );
298              
299 0         0 tie my %shippingaddr, 'Tie::IxHash',
300             $self->revmap_fields(
301             Address => 'ship_address',
302             City => 'ship_city',
303             StateProv => 'ship_state',
304             Country => \$ship_country,
305             Phone => 'ship_phone',
306             );
307              
308 0 0 0     0 unless ( $type ne 'CC' || keys %shippingaddr ) {
309 0         0 tie %shippingaddr, 'Tie::IxHash',
310             $self->revmap_fields(
311             Address => 'address',
312             City => 'city',
313             StateProv => 'state',
314             Country => \$country,
315             Phone => 'phone',
316             );
317             }
318 0 0       0 delete $shippingaddr{Country} unless $shippingaddr{Country};
319              
320 0         0 tie my %shippinginfo, 'Tie::IxHash',
321             $self->revmap_fields(
322             CustomerPO => 'CustomerPO',
323             ShippingMethod => 'ShippingMethod',
324             ShippingName => 'ship_name',
325             ShippingAddr => \%shippingaddr,
326             );
327              
328 0         0 tie my %req, 'Tie::IxHash',
329             $self->revmap_fields(
330             TransactionType => 'TransactionType',
331             TerminalID => 'login',
332             # TerminalID => \$terminalid,
333             # MerchantID => \$merchantid,
334             TransactionID => \$transaction_id,
335             RoutingCode => 'RoutingCode',
336             Approval => 'authorization',
337             BatchID => 'BatchID',
338             Origin => 'Origin',
339             Password => 'password',
340             OrderNumber => 'invoice_number',
341             CardNum => 'card_number',
342             CVV2 => 'cvv2',
343             Issue => 'issue_number',
344             CardExpMonth => \$cardexpmonth,
345             CardExpYear => \$cardexpyear,
346             CardStartMonth => \$cardstartmonth,
347             CardStartYear => \$cardstartyear,
348             Track1 => 'track1',
349             Track2 => 'track2',
350             ACH => \%ach,
351             CardName => 'name',
352             DispositionType => 'DispositionType',
353             TotalAmount => \$amount,
354             FeeAmount => 'FeeAmount',
355             TaxAmount => 'TaxAmount',
356             BillingAddress => 'address',
357             BillingCity => 'city',
358             BillingStateProv => 'state',
359             BillingPostalCode => 'zip',
360             BillingCountry => \$country,
361             BillingPhone => 'phone',
362             Email => 'email',
363             UserIPAddress => 'customer_ip',
364             UserHost => 'UserHost',
365             UDField1 => 'UDField1',
366             UDField2 => 'UDField2',
367             UDField3 => \"$me $VERSION", #'UDField3',
368             ActionCode => 'ActionCode',
369             IndustryInfo => \%industryinfo,
370             ShippingInfo => \%shippinginfo,
371             );
372 0 0       0 delete $req{BillingCountry} unless $req{BillingCountry};
373              
374 0         0 my $post_data;
375 0         0 my $writer = new XML::Writer( OUTPUT => \$post_data,
376             DATA_MODE => 1,
377             DATA_INDENT => 1,
378             ENCODING => 'us-ascii',
379             );
380 0         0 $writer->xmlDecl();
381 0         0 $writer->startTag('ippay');
382 0         0 foreach ( keys ( %req ) ) {
383 0         0 $self->_xmlwrite($writer, $_, $req{$_});
384             }
385 0         0 $writer->endTag('ippay');
386 0         0 $writer->end();
387              
388 0 0       0 warn "$post_data\n" if $DEBUG > 1;
389              
390 0         0 my ($page,$server_response,%headers) = $self->https_post($post_data);
391              
392 0 0       0 warn "$page\n" if $DEBUG > 1;
393              
394 0         0 my $response = {};
395 0 0       0 if ($server_response =~ /^200/){
396 0         0 $response = XMLin($page);
397 0 0 0     0 if ( exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
398 0         0 $self->error_message($response->{ResponseText});
399             }else{
400 0         0 $self->error_message($response->{ErrMsg});
401             }
402             # }else{
403             # $self->error_message("Server Failed");
404             }
405              
406 0   0     0 $self->result_code($response->{ActionCode} || '');
407 0   0     0 $self->order_number($response->{TransactionID} || '');
408 0   0     0 $self->authorization($response->{Approval} || '');
409 0   0     0 $self->cvv2_response($response->{CVV2} || '');
410 0   0     0 $self->avs_code($response->{AVS} || '');
411              
412 0 0       0 $self->is_success($self->result_code() eq '000' ? 1 : 0);
413              
414 0 0       0 unless ($self->is_success()) {
415 0 0       0 unless ( $self->error_message() ) {
416 0 0       0 if ( $DEBUG ) {
417             #additional logging information, possibly too sensitive for an error msg
418             # (IPPay seems to have a failure mode where they return the full
419             # original request including card number)
420             $self->error_message(
421             "(HTTPS response: $server_response) ".
422             "(HTTPS headers: ".
423 0         0 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
  0         0  
424             "(Raw HTTPS content: $page)"
425             );
426             } else {
427 0         0 $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)');
428             }
429             }
430             }
431              
432             }
433              
434             sub _error_response {
435 0     0   0 my ($self, $error_message) = (shift, shift);
436 0         0 $self->result_code('');
437 0         0 $self->order_number('');
438 0         0 $self->authorization('');
439 0         0 $self->cvv2_response('');
440 0         0 $self->avs_code('');
441 0         0 $self->is_success( 0);
442 0         0 $self->error_message($error_message);
443             }
444              
445             sub _xmlwrite {
446 217     217   647 my ($self, $writer, $item, $value) = @_;
447              
448 217         1443 my %att = ();
449 217 100       469 if ( $item eq 'ACH' ) {
450             $att{'Type'} = $self->{_content}->{'account_type'}
451 2 50       16 if $self->{_content}->{'account_type'}; #necessary so we don't pass empty?
452             $att{'SEC'} = $self->{_content}->{'nacha_sec_code'}
453 2   33     25 || ( $att{'Type'} =~ /business/i ? 'CCD' : 'PPD' );
454             }
455              
456 217         658 $writer->startTag($item, %att);
457              
458 217 100       14409 if ( ref( $value ) eq 'HASH' ) {
459 18         79 foreach ( keys ( %$value ) ) {
460 46         1211 $self->_xmlwrite($writer, $_, $value->{$_});
461             }
462             }else{
463 199         529 $writer->characters($value);
464             }
465              
466 217         5472 $writer->endTag($item);
467             }
468              
469             1;
470              
471             __END__