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