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