File Coverage

blib/lib/Business/OnlinePayment/InternetSecure.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::InternetSecure;
2              
3 9     9   90294 use 5.008;
  9         37  
  9         395  
4 9     9   52 use strict;
  9         78  
  9         343  
5 9     9   48 use warnings;
  9         23  
  9         343  
6              
7 9     9   63 use Carp;
  9         39  
  9         822  
8 9     9   10395 use Encode;
  9         126400  
  9         970  
9 9     9   19880 use Net::SSLeay qw(make_form post_https);
  9         154485  
  9         5658  
10 9     9   4578 use XML::Simple qw(xml_in xml_out);
  0            
  0            
11              
12             use base qw(Business::OnlinePayment Exporter);
13              
14              
15             our $VERSION = '0.10';
16              
17              
18             use constant SUCCESS_CODES => qw(2000 90000 900P1);
19              
20             use constant CARD_TYPES => {
21             AM => 'American Express',
22             JB => 'JCB',
23             MC => 'MasterCard',
24             NN => 'Discover',
25             VI => 'Visa',
26             };
27              
28              
29             # Convenience functions to avoid undefs and escape products strings
30             sub _def($) { defined $_[0] ? $_[0] : '' }
31             sub _esc($) { local $_ = shift; tr/|:/ /s; tr/"`/'/s; return $_ }
32              
33              
34             sub set_defaults {
35             my ($self) = @_;
36              
37             $self->server('secure.internetsecure.com');
38             $self->port(443);
39             $self->path('/process.cgi');
40              
41             $self->build_subs(qw(
42             receipt_number order_number uuid guid
43             date
44             card_type cardholder
45             total_amount tax_amounts
46             avs_code cvv2_response
47             ));
48              
49             # Just in case someone tries to call tax_amounts() *before* submit()
50             $self->tax_amounts( {} );
51             }
52              
53             # Backwards-compatible support for renamed fields
54             sub avs_response { shift()->avs_code(@_) }
55             sub sales_number { shift()->order_number(@_) }
56              
57              
58             # Combine get_fields and remap_fields for convenience. Unlike OnlinePayment's
59             # remap_fields, this doesn't modify content(), and can therefore be called
60             # more than once. Also, unlike OnlinePayment's get_fields in 3.x, this doesn't
61             # exclude undefs.
62             #
63             sub get_remap_fields {
64             my ($self, %map) = @_;
65              
66             my %content = $self->content();
67             my %data;
68              
69             while (my ($to, $from) = each %map) {
70             $data{$to} = $content{$from};
71             }
72              
73             return %data;
74             }
75              
76             # Since there's no standard format for expiration dates, we try to do our best
77             #
78             sub parse_expdate {
79             my ($self, $str) = @_;
80              
81             local $_ = $str;
82              
83             my ($y, $m);
84              
85             if (/^(\d{4})\W(\d{1,2})$/ || # YYYY.MM or YYYY-M
86             /^(\d\d)\W(\d)$/ || # YY/M or YY-M
87             /^(\d\d)[.-](\d\d)$/) { # YY-MM
88             ($y, $m) = ($1, $2);
89             } elsif (/^(\d{1,2})\W(\d{4})$/ || # MM-YYYY or M/YYYY
90             /^(\d)\W(\d\d)$/ || # M/YY or M-YY
91             /^(\d\d)\/?(\d\d)$/) { # MM/YY or MMYY
92             ($y, $m) = ($2, $1);
93             } else {
94             croak "Unable to parse expiration date: $str";
95             }
96              
97             $y += 2000 if $y < 2000; # Aren't we glad Y2K is behind us?
98              
99             return ($y, $m);
100             }
101              
102             # Convert a single product into a product string
103             #
104             sub prod_string {
105             my ($self, $currency, %data) = @_;
106              
107             croak "Missing amount in product" unless defined $data{amount};
108              
109             my @flags = ($currency);
110              
111             my @taxes;
112             if (ref $data{taxes}) {
113             @taxes = @{ $data{taxes} };
114             } elsif ($data{taxes}) {
115             @taxes = split ' ' => $data{taxes};
116             }
117              
118             foreach (@taxes) {
119             croak "Unknown tax code $_" unless /^(GST|PST|HST)$/i;
120             push @flags, uc $_;
121             }
122              
123             if ($self->test_transaction) {
124             push @flags, $self->test_transaction < 0 ? 'TESTD' : 'TEST';
125             }
126              
127             # recurring can come as string or hashref
128             if ($data{recurring}) {
129             if (ref $data{recurring}) {
130             my @options;
131             push @options, 'RB';
132             foreach my $key ( sort keys %{ $data{recurring} } ) {
133             push @options, $key . '=' . $data{recurring}{$key};
134             }
135             push @flags, join ' ', @options;
136             } else {
137             push @flags, "RB $data{recurring}";
138             }
139             }
140              
141             return join '::' =>
142             sprintf('%.2f' => $data{amount}),
143             $data{quantity} || 1,
144             _esc _def $data{sku},
145             _esc _def $data{description},
146             join('' => map "{$_}" => @flags),
147             ;
148             }
149              
150             # Generate the XML document for this transaction
151             #
152             sub to_xml {
153             my ($self) = @_;
154              
155             my %content = $self->content;
156              
157             # Backwards-compatible support for exp_date
158             if (exists $content{exp_date} && ! exists $content{expiration}) {
159             $content{expiration} = delete $content{exp_date};
160             $self->content(%content);
161             }
162              
163             $self->required_fields(qw(action card_number expiration));
164              
165             croak "Unsupported card type: $content{type}"
166             if $content{type} &&
167             ! grep lc($content{type}) eq lc($_),
168             values %{+CARD_TYPES}, 'CC';
169              
170             croak 'Unsupported action'
171             unless $content{action} =~ /^(Normal|Card) Authori[zs]ation$/i;
172              
173             $content{currency} = uc($content{currency} || 'CAD');
174             croak "Unknown currency code ", $content{currency}
175             unless $content{currency} =~ /^(CAD|USD)$/;
176              
177             my %data = $self->get_remap_fields(qw(
178             xxxCard_Number card_number
179              
180             xxxName name
181             xxxCompany company
182             xxxAddress address
183             xxxCity city
184             xxxProvince state
185             xxxPostal zip
186             xxxCountry country
187             xxxPhone phone
188             xxxEmail email
189              
190             xxxShippingName ship_name
191             xxxShippingCompany ship_company
192             xxxShippingAddress ship_address
193             xxxShippingCity ship_city
194             xxxShippingProvince ship_state
195             xxxShippingPostal ship_zip
196             xxxShippingCountry ship_country
197             xxxShippingPhone ship_phone
198             xxxShippingEmail ship_email
199              
200             xxxCustomerDB cimb_store
201             ));
202              
203             $data{MerchantNumber} = $self->merchant_id;
204              
205             $data{xxxCard_Number} =~ tr/- //d;
206             $data{xxxCard_Number} =~ s/^[^3-6]/4/ if $self->test_transaction;
207              
208             my ($y, $m) = $self->parse_expdate($content{expiration});
209             $data{xxxCCYear} = sprintf '%.4u' => $y;
210             $data{xxxCCMonth} = sprintf '%.2u' => $m;
211              
212             delete $data{xxxCustomerDB} unless $data{xxxCustomerDB};
213              
214             # Recurring
215             if (defined $content{recurring} && $content{recurring} ne '') {
216             $data{xxxCardInput} = 8;
217             }
218              
219             if (defined $content{cvv2} && $content{cvv2} ne '') {
220             $data{CVV2} = $content{cvv2};
221             $data{CVV2Indicator} = 1;
222             } else {
223             $data{CVV2} = '';
224             $data{CVV2Indicator} = 0;
225             }
226              
227             if ($content{action} =~ /^Card Authori[zs]ation$/i) {
228             $data{xxxTransType} = 22;
229              
230             $data{Products} = $self->prod_string(
231             $content{currency},
232             taxes => 0,
233             amount => 0.0,
234             description => 'CardAuth and Store',
235             );
236             } else {
237              
238             if (ref $content{description}) {
239             $data{Products} = join '|' => map $self->prod_string(
240             $content{currency},
241             taxes => $content{taxes},
242             %$_),
243             @{ $content{description} };
244             } else {
245             $self->required_fields(qw(amount));
246             $data{Products} = $self->prod_string(
247             $content{currency},
248             taxes => $content{taxes},
249             amount => $content{amount},
250             description => $content{description},
251             recurring => $content{recurring},
252             );
253             }
254              
255             }
256              
257             # The encode() makes sure to a) strip off non-Latin-1 characters, and
258             # b) turn off the utf8 flag, which confuses XML::Simple
259             encode('ISO-8859-1', xml_out(\%data,
260             NoAttr => 1,
261             RootName => 'TranxRequest',
262             SuppressEmpty => undef,
263             XMLDecl => '',
264             ));
265             }
266              
267             # Map the various fields from the response, and put their values into our
268             # object for retrieval.
269             #
270             sub infuse {
271             my ($self, $data, %map) = @_;
272              
273             while (my ($k, $v) = each %map) {
274             no strict 'refs';
275             $self->$k($data->{$v});
276             }
277             }
278              
279             sub extract_tax_amounts {
280             my ($self, $response) = @_;
281              
282             my %tax_amounts;
283              
284             my $products = $response->{Products};
285             return unless $products;
286              
287             foreach my $node (@$products) {
288             my $flags = $node->{flags};
289             if ($flags &&
290             grep($_ eq '{TAX}', @$flags) &&
291             grep($_ eq '{CALCULATED}', @$flags))
292             {
293             $tax_amounts{ $node->{code} } = $node->{subtotal};
294             }
295             }
296              
297             return %tax_amounts;
298             }
299              
300             # Parse the server's response and set various fields
301             #
302             sub parse_response {
303             my ($self, $response) = @_;
304              
305             $self->server_response($response);
306              
307             local $/ = "\n"; # Make sure to avoid bug #17687
308              
309             $response = xml_in($response,
310             ForceArray => [qw(product flag)],
311             GroupTags => { qw(Products product flags flag) },
312             KeyAttr => [],
313             SuppressEmpty => undef,
314             );
315              
316             $self->infuse($response,
317             result_code => 'Page',
318             error_message => 'Verbiage',
319             authorization => 'ApprovalCode',
320             avs_code => 'AVSResponseCode',
321             cvv2_response => 'CVV2ResponseCode',
322              
323             receipt_number => 'ReceiptNumber',
324             order_number => 'SalesOrderNumber',
325             uuid => 'GUID',
326             guid => 'GUID',
327              
328             date => 'Date',
329             cardholder => 'xxxName',
330             card_type => 'CardType',
331             total_amount => 'TotalAmount',
332             );
333              
334             $self->is_success(scalar grep $self->result_code eq $_, SUCCESS_CODES);
335              
336             # Completely undocumented field that sometimes override
337             $self->error_message($response->{Error}) if $response->{Error};
338              
339             # Delete error_message if transaction was successful
340             $self->error_message(undef) if $self->is_success;
341              
342             $self->card_type(CARD_TYPES->{$self->card_type});
343              
344             $self->tax_amounts( { $self->extract_tax_amounts($response) } );
345              
346             return $self;
347             }
348              
349             sub submit {
350             my ($self) = @_;
351              
352             croak "Missing required argument 'merchant_id'"
353             unless defined $self->{merchant_id};
354              
355             my ($page, $response, %headers) =
356             post_https(
357             $self->server,
358             $self->port,
359             $self->path,
360             undef,
361             make_form(
362             xxxRequestMode => 'X',
363             xxxRequestData => $self->to_xml,
364             )
365             );
366              
367             croak 'Error connecting to server' unless $page;
368             croak 'Server responded, but not in XML' unless $page =~ /^<\?xml/;
369              
370             # The response is marked UTF-8, but it's really Latin-1. Sigh.
371             $page =~ s/^(<\?xml.*?) encoding="utf-8"/$1 encoding="iso-8859-1"/si;
372              
373             $self->parse_response($page);
374             }
375              
376              
377             1;
378              
379             __END__