f4e68da5ae2bbfee6a60dbb530cb1f0be254bd31
[Business-OnlinePayment-InternetSecure.git] / InternetSecure.pm
1 package Business::OnlinePayment::InternetSecure;
2
3 use 5.008;
4 use strict;
5 use warnings;
6
7 use Carp;
8 use Encode;
9 use Net::SSLeay qw(make_form post_https);
10 use XML::Simple qw(xml_in xml_out);
11
12 use base qw(Business::OnlinePayment Exporter);
13
14
15 our $VERSION = '0.01';
16
17
18 use constant CARD_TYPES => {
19                                 VI => 'Visa',
20                                 MC => 'MasterCard',
21                                 AX => 'American Express', # FIXME: AM?
22                                 NN => 'Discover',
23                                 # JB?
24                         };
25
26
27 # Convenience functions to avoid undefs and escape products strings
28 sub _def($) { defined $_[0] ? $_[0] : '' }
29 sub _esc($) { local $_ = shift; tr/|:/ /s; tr/"`/'/s; return $_ }
30
31
32 sub set_defaults {
33         my ($self) = @_;
34
35         $self->server('secure.internetsecure.com');
36         $self->port(443);
37         $self->path('/process.cgi');
38
39         $self->build_subs(qw(
40                                 receipt_number  sales_number
41                                 date
42                                 card_type
43                                 total_amount
44                                 avs_response    cvv2_response
45                         ));
46 }
47
48 # OnlinePayment's get_fields now filters out undefs in 3.x. :(
49 #
50 sub get_fields {
51         my ($self, @fields) = @_;
52
53         my %content = $self->content;
54
55         my %new = map +($_ => $content{$_}), @fields;
56
57         return %new;
58 }
59
60 # OnlinePayment's remap_fields is buggy, so we simply rewrite it
61 #
62 sub remap_fields {
63         my ($self, %map) = @_;
64
65         my %content = $self->content();
66         foreach (keys %map) {
67                 $content{$map{$_}} = delete $content{$_};
68         }
69         $self->content(%content);
70 }
71
72 # Combine get_fields and remap_fields for convenience
73 #
74 sub get_remap_fields {
75         my ($self, %map) = @_;
76
77         $self->remap_fields(reverse %map);
78         my %data = $self->get_fields(keys %map);
79
80         return %data;
81 }
82
83 # Since there's no standard format for expiration dates, we try to do our best
84 #
85 sub parse_expdate {
86         my ($self, $str) = @_;
87
88         local $_ = $str;
89
90         my ($y, $m);
91
92         if (/^(\d{4})\W(\d{1,2})$/ ||           # YYYY.MM  or  YYYY-M
93                         /^(\d\d)\W(\d)$/ ||     # YY/M  or  YY-M
94                         /^(\d\d)[.-](\d\d)$/) { # YY-MM
95                 ($y, $m) = ($1, $2);
96         } elsif (/^(\d{1,2})\W(\d{4})$/ ||      # MM-YYYY  or  M/YYYY
97                         /^(\d)\W(\d\d)$/ ||     # M/YY  or  M-YY
98                         /^(\d\d)\/?(\d\d)$/) {  # MM/YY  or  MMYY
99                 ($y, $m) = ($2, $1);
100         } else {
101                 croak "Unable to parse expiration date: $str";
102         }
103
104         $y += 2000 if $y < 2000;  # Aren't we glad Y2K is behind us?
105
106         return ($y, $m);
107 }
108
109 # Convert a single product into a product string
110 #
111 sub prod_string {
112         my ($self, $currency, %data) = @_;
113
114         croak "Missing amount in product" unless defined $data{amount};
115
116         my @flags = ($currency);
117
118         foreach (split ' ' => uc($data{taxes} || '')) {
119                 croak "Unknown tax code $_" unless /^(GST|PST|HST)$/;
120                 push @flags, $_;
121         }
122
123         if ($self->test_transaction) {
124                 push @flags, $self->test_transaction < 0 ? 'TESTD' : 'TEST';
125         }
126
127         return join '::' =>
128                                 sprintf('%.2f' => $data{amount}),
129                                 $data{quantity} || 1,
130                                 _esc _def $data{sku},
131                                 _esc _def $data{description},
132                                 join('' => map "{$_}" => @flags),
133                                 ;
134 }
135
136 # Generate the XML document for this transaction
137 #
138 sub to_xml {
139         my ($self) = @_;
140
141         my %content = $self->content;
142
143         $self->required_fields(qw(action card_number exp_date));
144
145         croak 'Unsupported transaction type'
146                 if $content{type} && $content{type} !~
147                         /^(Visa|MasterCard|American Express|Discover)$/i;
148         
149         croak 'Unsupported action'
150                 unless $content{action} =~ /^Normal Authori[zs]ation$/i;
151         
152         $content{currency} = uc($content{currency} || 'CAD');
153         croak "Unknown currency code ", $content{currency}
154                 unless $content{currency} =~ /^(CAD|USD)$/;
155         
156         $content{taxes} = uc($content{taxes} || '');
157
158         my %data = $self->get_remap_fields(qw(
159                         xxxCard_Number          card_number
160
161                         xxxName                 name
162                         xxxCompany              company
163                         xxxAddress              address
164                         xxxCity                 city
165                         xxxProvince             state
166                         xxxPostal               zip
167                         xxxCountry              country
168                         xxxPhone                phone
169                         xxxEmail                email
170
171                         xxxShippingName         ship_name
172                         xxxShippingCompany      ship_company
173                         xxxShippingAddress      ship_address
174                         xxxShippingCity         ship_city
175                         xxxShippingProvince     ship_state
176                         xxxShippingPostal       ship_zip
177                         xxxShippingCountry      ship_country
178                         xxxShippingPhone        ship_phone
179                         xxxShippingEmail        ship_email
180                 ));
181         
182         $data{MerchantNumber} = $self->merchant_id;
183
184         $data{xxxCard_Number} =~ tr/ //d;
185         $data{xxxCard_Number} =~ s/^[^3-6]/4/ if $self->test_transaction;
186
187         my ($y, $m) = $self->parse_expdate($content{exp_date});
188         $data{xxxCCYear} = sprintf '%.4u' => $y;
189         $data{xxxCCMonth} = sprintf '%.2u' => $m;
190
191         if (defined $content{cvv2} && $content{cvv2} ne '') {
192                 $data{CVV2} = 1;
193                 $data{CVV2Indicator} = $content{cvv2};
194         } else {
195                 $data{CVV2} = 0;
196                 $data{CVV2Indicator} = '';
197         }
198         
199         if (ref $content{description}) {
200                 $data{Products} = join '|' => map $self->prod_string(
201                                                 $content{currency},
202                                                 taxes => $content{taxes},
203                                                 %$_),
204                                         @{ $content{description} };
205         } else {
206                 $self->required_fields(qw(amount));
207                 $data{Products} = $self->prod_string(
208                                         $content{currency},
209                                         taxes       => $content{taxes},
210                                         amount      => $content{amount},
211                                         description => $content{description},
212                                 );
213         }
214
215         xml_out(\%data,
216                 NoAttr          => 1,
217                 NumericEscape   => 2,
218                 RootName        => 'TranxRequest',
219                 SuppressEmpty   => undef,
220                 XMLDecl         => '<?xml version="1.0" encoding="utf-8" standalone="yes"?>',
221         );
222 }
223
224 # Map the various fields from the response, and put their values into our
225 # object for retrieval.
226 #
227 sub infuse {
228         my ($self, $data, %map) = @_;
229
230         while (my ($k, $v) = each %map) {
231                 no strict 'refs';
232                 $self->$v($data->{$k});
233         }
234 }
235
236 # Parse the server's response and set various fields
237 #
238 sub parse_response {
239         my ($self, $response) = @_;
240
241         $self->server_response($response);
242
243         local $/ = "\n";  # Make sure to avoid bug #17687
244         
245         $response = xml_in($response,
246                         ForceArray => [qw(product flag)],
247                         GroupTags => { qw(Products product flags flag) },
248                         KeyAttr => [],
249                         SuppressEmpty => undef,
250                 );
251         
252         my $code = $self->result_code($response->{Page});
253         $self->is_success($code eq '2000' || $code eq '90000' || $code eq '900P1');
254
255         $self->infuse($response, qw(
256                         ReceiptNumber           receipt_number
257                         SalesOrderNumber        sales_number
258                         Date                    date
259                         CardType                card_type
260                         Page                    result_code
261                         ApprovalCode            authorization
262                         Verbiage                error_message
263                         TotalAmount             total_amount
264                         AVSResponseCode         avs_response
265                         CVV2ResponseCode        cvv2_response
266                 ));
267         
268         # Completely undocumented field that sometimes override <Verbiage>
269         $self->error_message($response->{Error}) if $response->{Error};
270         
271         $self->card_type(CARD_TYPES->{$self->card_type});
272         
273         $self->{products_raw} = $response->{Products};
274
275         return $self;
276 }
277
278 sub submit {
279         my ($self) = @_;
280
281         croak "Missing required argument 'merchant_id'"
282                 unless defined $self->{merchant_id};
283
284         my ($page, $response, %headers) = 
285                 post_https(
286                                 $self->server,
287                                 $self->port,
288                                 $self->path,
289                                 undef,
290                                 make_form(
291                                         xxxRequestMode => 'X',
292                                         xxxRequestData => $self->to_xml,
293                                 )
294                         );
295
296         croak 'Error connecting to server' unless $page;
297         croak 'Server responded, but not in XML' unless $page =~ /^<\?xml/;
298
299         $self->parse_response($page);
300 }
301
302
303 1;
304
305 __END__
306
307
308 =head1 NAME
309
310 Business::OnlinePayment::InternetSecure - InternetSecure backend for Business::OnlinePayment
311
312 =head1 SYNOPSIS
313
314   use Business::OnlinePayment;
315
316   $txn = new Business::OnlinePayment 'InternetSecure',
317                                         merchant_id => '0000';
318
319   $txn->content(
320         action          => 'Normal Authorization',
321
322         type            => 'Visa',
323         card_number     => '0000000000000000',
324         exp_date        => '2004-07',
325         cvv2            => '000',               # Optional
326
327         name            => "Fr\x{e9}d\x{e9}ric Bri\x{e8}re",
328         company         => '',
329         address         => '123 Street',
330         city            => 'Metropolis',
331         state           => 'ZZ',
332         zip             => 'A1A 1A1',
333         country         => 'CA',
334         phone           => '(555) 555-1212',
335         email           => 'fbriere@fbriere.net',
336
337         description     => 'Online purchase',
338         amount          => 49.95,
339         currency        => 'CAD',
340         taxes           => 'GST PST',
341         );
342
343   $txn->submit;
344
345   if ($txn->is_success) {
346         print "Card processed successfully: " . $tx->authorization . "\n";
347   } else {
348         print "Card was rejected: " . $tx->error_message . "\n";
349   }
350
351 =head1 DESCRIPTION
352
353 Business::OnlinePayment::InternetSecure is an implementation of
354 L<Business::OnlinePayment> that allows for processing online credit card
355 payments through InternetSecure.
356
357 See L<Business::OnlinePayment> for more information about the generic
358 Business::OnlinePayment interface.
359
360 =head1 CREATOR
361
362 Object creation is done via L<Business::OnlinePayment>; see its manpage for
363 details.  The I<merchant_id> processor option is required, and corresponds
364 to the merchant ID assigned to you by InternetSecure.
365
366 =head1 METHODS
367
368 (Other methods are also available -- see L<Business::OnlinePayment> for more
369 details.)
370
371 =head2 Before order submission
372
373 =over 4
374
375 =item content( CONTENT )
376
377 Sets up the data prior to a transaction (overwriting any previous data by the
378 same occasion).  CONTENT is an associative array (hash), containing some of
379 the following fields:
380
381 =over 4
382
383 =item action (required)
384
385 What to do with the transaction.  Only C<Normal Authorization> is supported
386 for the moment.
387
388 =item type
389
390 Transaction type, being one of the following:
391
392 =over 4
393
394 =item - Visa
395
396 =item - MasterCard
397
398 =item - American Express
399
400 =item - Discover
401
402 =back
403
404 (This is actually ignored for the moment, and can be left blank or undefined.)
405
406 =item card_number (required)
407
408 Credit card number.  Spaces are allowed, and will be automatically removed.
409
410 =item exp_date (required)
411
412 Credit card expiration date.  Since L<Business::OnlinePayment> does not specify
413 any syntax, this module is rather lax regarding what it will accept.  The
414 recommended syntax is I<YYYY-MM>, but forms such as I<MM/YYYY> or I<MMYY> are
415 allowed as well.
416
417 =item cvv2
418
419 Three- or four-digit verification code printed on the card.  This can be left
420 blank or undefined, in which case no check will be performed.  Whether or not a
421 transaction will be declined in case of a mismatch depends on the merchant
422 account configuration.
423
424 This number may be called Card Verification Value (CVV2), Card Validation
425 Code (CVC2) or Card Identification number (CID), depending on the card issuer.
426
427 =item description
428
429 A short description of the purchase.  See L<"Products list syntax"> for
430 an alternate syntax that allows a list of products to be specified.
431
432 =item amount (usually required)
433
434 Total amount to be billed, excluding taxes if they are to be added separately
435 by InternetSecure.
436
437 This field is required if B<description> is a string, but should be left
438 undefined if B<description> contains a list of products instead, as outlined
439 in L<"Products list syntax">.
440
441 =item currency
442
443 Currency of all amounts for this order.  This can currently be either
444 C<CAD> (default) or C<USD>.
445
446 =item taxes
447
448 Taxes to be added automatically to B<amount> by InternetSecure.
449
450 Available taxes are C<GST>, C<PST> and C<HST>.  Multiple taxes can specified
451 by concatenating them with spaces, such as C<GST HST>.
452
453 =item name / company / address / city / state / zip / country / phone / email
454
455 Customer information.  B<Country> should be a two-letter code taken from ISO
456 3166-1.
457
458 =back
459
460 =back
461
462 =head2 After order submission
463
464 =over 4
465
466 =item receipt_number()
467
468 Receipt number of this transaction; this is actually a string, unique to all
469 InternetSecure transactions.
470
471 =item sales_number()
472
473 Sales order number of this transaction.  This is a number, unique to each
474 merchant, which is incremented by 1 each time.
475
476 =item date()
477
478 Date and time of the transaction.  Format is C<YYYY/MM/DD hh:mm:ss>.
479
480 =item total_amount()
481
482 Total amount billed for this order, including taxes.
483
484 =item card_type()
485
486 Type of the credit card used for the submitted order, being one of the
487 following:
488
489 =over 4
490
491 =item - Visa
492
493 =item - MasterCard
494
495 =item - American Express
496
497 =item - Discover
498
499 =back
500
501 =item avs_response() / cvv2_response()
502
503 Results of the AVS and CVV2 checks.  See the InternetSecure documentation for
504 the list of possible values.
505
506 =item products_raw()
507
508 ...
509
510
511 =back
512
513
514 =head1 NOTES
515
516 =head2 Products list syntax
517
518 Optionally, the B<description> field of B<content()> can contain a reference
519 to an array of products, instead of a simple string.  Each element of this
520 array represents a different product, and must be a reference to a hash with
521 the following fields:
522
523 =over 4
524
525 =item amount
526
527 Unit price of this product.
528
529 =item quantity
530
531 Ordered quantity of this product.
532
533 =item sku
534
535 Internal code for this product.
536
537 =item description
538
539 Description of this product
540
541 =item taxes
542
543 Taxes that should be automatically added to this product.  If specified, this
544 overrides the B<taxes> field passed to B<content()>.
545
546 =back
547
548 When using a products list, the B<amount> field passed to B<content()> should
549 be left undefined.
550
551
552 =head2 Character encoding
553
554 Since communication to/from InternetSecure is encoded with UTF-8, all Unicode
555 characters are theoretically available when submitting information via
556 B<submit()>.  (Further restrictions may be imposed by InternetSecure itself.)
557
558 When using non-ASCII characters, all data provided to B<submit()> should either
559 be in the current native encoding (typically latin-1, unless it was modified
560 via the C<encoding> pragma), or be decoded via the C<Encode> module.
561 Conversely, all data returned after calling B<submit()> will be automatically
562 decoded.
563
564
565 =head1 EXPORT
566
567 None by default.
568
569
570 =head1 SEE ALSO
571
572 L<Business::OnlinePayment>
573
574 =head1 AUTHOR
575
576 Frédéric Brière, E<lt>fbriere@fbriere.netE<gt>
577
578 =head1 COPYRIGHT AND LICENSE
579
580 Copyright (C) 2006 by Frédéric Brière
581
582 This library is free software; you can redistribute it and/or modify
583 it under the same terms as Perl itself, either Perl version 5.8.4 or,
584 at your option, any later version of Perl 5 you may have available.
585
586
587 =cut