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