X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=InternetSecure.pm;h=b6632edaca0bccbc994058ecae7046ac809f4a3a;hb=df677a47383944a67d88371a83d18d557602f3cd;hp=86e16e20409a20f97cc0038141e930f487b668af;hpb=4f7f63abf30377deba653d2704df43c11061bb66;p=Business-OnlinePayment-InternetSecure.git diff --git a/InternetSecure.pm b/InternetSecure.pm index 86e16e2..b6632ed 100755 --- a/InternetSecure.pm +++ b/InternetSecure.pm @@ -12,14 +12,17 @@ use XML::Simple qw(xml_in xml_out); use base qw(Business::OnlinePayment Exporter); -our $VERSION = '0.01'; +our $VERSION = '0.03'; +use constant SUCCESS_CODES => qw(2000 90000 900P1); + use constant CARD_TYPES => { - VI => 'Visa', + AM => 'American Express', + JB => 'JCB', MC => 'MasterCard', - AX => 'American Express', NN => 'Discover', + VI => 'Visa', }; @@ -36,44 +39,36 @@ sub set_defaults { $self->path('/process.cgi'); $self->build_subs(qw( - receipt_number sales_order_number - cardholder card_type - total_amount - avs_response cvv2_response + receipt_number order_number uuid guid + date + card_type cardholder + total_amount tax_amounts + avs_code cvv2_response )); + + # Just in case someone tries to call tax_amounts() *before* submit() + $self->tax_amounts( {} ); } -# OnlinePayment's get_fields now filters out undefs in 3.x. :( -# -sub get_fields { - my ($self, @fields) = @_; +# Backwards-compatible support for renamed fields +sub avs_response { shift()->avs_code(@_) } +sub sales_number { shift()->order_number(@_) } - my %content = $self->content; - - my %new = map +($_ => $content{$_}), @fields; - - return %new; -} -# OnlinePayment's remap_fields is buggy, so we simply rewrite it +# Combine get_fields and remap_fields for convenience. Unlike OnlinePayment's +# remap_fields, this doesn't modify content(), and can therefore be called +# more than once. Also, unlike OnlinePayment's get_fields in 3.x, this doesn't +# exclude undefs. # -sub remap_fields { +sub get_remap_fields { my ($self, %map) = @_; my %content = $self->content(); - foreach (keys %map) { - $content{$map{$_}} = delete $content{$_}; - } - $self->content(%content); -} + my %data; -# Combine get_fields and remap_fields for convenience -# -sub get_remap_fields { - my ($self, %map) = @_; - - $self->remap_fields(reverse %map); - my %data = $self->get_fields(keys %map); + while (my ($to, $from) = each %map) { + $data{$to} = $content{$from}; + } return %data; } @@ -107,16 +102,22 @@ sub parse_expdate { # Convert a single product into a product string # sub prod_string { - my ($self, $currency, $taxes, %data) = @_; + my ($self, $currency, %data) = @_; croak "Missing amount in product" unless defined $data{amount}; my @flags = ($currency); - $taxes = uc $data{taxes} if defined $data{taxes}; - foreach (split ' ' => $taxes) { - croak "Unknown tax code $_" unless /^(GST|PST|HST)$/; - push @flags, $_; + my @taxes; + if (ref $data{taxes}) { + @taxes = @{ $data{taxes} }; + } elsif ($data{taxes}) { + @taxes = split ' ' => $data{taxes}; + } + + foreach (@taxes) { + croak "Unknown tax code $_" unless /^(GST|PST|HST)$/i; + push @flags, uc $_; } if ($self->test_transaction) { @@ -139,25 +140,28 @@ sub to_xml { my %content = $self->content; - $self->required_fields(qw(action card_number exp_date)); + # Backwards-compatible support for exp_date + if (exists $content{exp_date} && ! exists $content{expiration}) { + $content{expiration} = delete $content{exp_date}; + $self->content(%content); + } + + $self->required_fields(qw(action card_number expiration)); - croak 'Unsupported transaction type' - if $content{type} && $content{type} !~ - /^(Visa|MasterCard|American Express|Discover)$/i; + croak "Unsupported transaction type: $content{type}" + if $content{type} && + ! grep lc($content{type}) eq lc($_), + values %{+CARD_TYPES}; croak 'Unsupported action' unless $content{action} =~ /^Normal Authori[zs]ation$/i; - $content{currency} ||= 'CAD'; - $content{currency} = uc $content{currency}; + $content{currency} = uc($content{currency} || 'CAD'); croak "Unknown currency code ", $content{currency} unless $content{currency} =~ /^(CAD|USD)$/; - $content{taxes} ||= ''; - $content{taxes} = uc $content{taxes}; - my %data = $self->get_remap_fields(qw( - xxxCardNumber card_number + xxxCard_Number card_number xxxName name xxxCompany company @@ -182,9 +186,10 @@ sub to_xml { $data{MerchantNumber} = $self->merchant_id; - $data{xxxCardNumber} =~ tr/ //d; + $data{xxxCard_Number} =~ tr/- //d; + $data{xxxCard_Number} =~ s/^[^3-6]/4/ if $self->test_transaction; - my ($y, $m) = $self->parse_expdate($content{exp_date}); + my ($y, $m) = $self->parse_expdate($content{expiration}); $data{xxxCCYear} = sprintf '%.4u' => $y; $data{xxxCCMonth} = sprintf '%.2u' => $m; @@ -198,26 +203,28 @@ sub to_xml { if (ref $content{description}) { $data{Products} = join '|' => map $self->prod_string( - $content{currency}, - $content{taxes}, - %$_), - @{ $content{description} }; + $content{currency}, + taxes => $content{taxes}, + %$_), + @{ $content{description} }; } else { $self->required_fields(qw(amount)); $data{Products} = $self->prod_string( $content{currency}, - $content{taxes}, - amount => $content{amount}, + taxes => $content{taxes}, + amount => $content{amount}, description => $content{description}, ); } - xml_out(\%data, - NoAttr => 1, - RootName => 'TranxRequest', - SuppressEmpty => undef, - XMLDecl => '', - ); + # The encode() makes sure to a) strip off non-Latin-1 characters, and + # b) turn off the utf8 flag, which confuses XML::Simple + encode('ISO-8859-1', xml_out(\%data, + NoAttr => 1, + RootName => 'TranxRequest', + SuppressEmpty => undef, + XMLDecl => '', + )); } # Map the various fields from the response, and put their values into our @@ -228,8 +235,29 @@ sub infuse { while (my ($k, $v) = each %map) { no strict 'refs'; - $self->$v($data->{$k}); + $self->$k($data->{$v}); + } +} + +sub extract_tax_amounts { + my ($self, $response) = @_; + + my %tax_amounts; + + my $products = $response->{Products}; + return unless $products; + + foreach my $node (@$products) { + my $flags = $node->{flags}; + if ($flags && + grep($_ eq '{TAX}', @$flags) && + grep($_ eq '{CALCULATED}', @$flags)) + { + $tax_amounts{ $node->{code} } = $node->{subtotal}; + } } + + return %tax_amounts; } # Parse the server's response and set various fields @@ -238,6 +266,8 @@ sub parse_response { my ($self, $response) = @_; $self->server_response($response); + + local $/ = "\n"; # Make sure to avoid bug #17687 $response = xml_in($response, ForceArray => [qw(product flag)], @@ -246,25 +276,35 @@ sub parse_response { SuppressEmpty => undef, ); - my $code = $self->result_code($response->{Page}); - $self->is_success($code eq '2000' || $code eq '90000' || $code eq '900P1'); - - $self->infuse($response, qw( - ReceiptNumber receipt_number - SalesOrderNumber sales_order_number - xxxName cardholder - CardType card_type - Page result_code - ApprovalCode authorization - Verbiage error_message - TotalAmount total_amount - AVSResponseCode avs_response - CVV2ResponseCode cvv2_response - )); + $self->infuse($response, + result_code => 'Page', + error_message => 'Verbiage', + authorization => 'ApprovalCode', + avs_code => 'AVSResponseCode', + cvv2_response => 'CVV2ResponseCode', + + receipt_number => 'ReceiptNumber', + order_number => 'SalesOrderNumber', + uuid => 'GUID', + guid => 'GUID', + + date => 'Date', + cardholder => 'xxxName', + card_type => 'CardType', + total_amount => 'TotalAmount', + ); + + $self->is_success(scalar grep $self->result_code eq $_, SUCCESS_CODES); + + # Completely undocumented field that sometimes override + $self->error_message($response->{Error}) if $response->{Error}; + + # Delete error_message if transaction was successful + $self->error_message(undef) if $self->is_success; $self->card_type(CARD_TYPES->{$self->card_type}); - $self->{products_raw} = $response->{Products}; + $self->tax_amounts( { $self->extract_tax_amounts($response) } ); return $self; } @@ -283,15 +323,16 @@ sub submit { undef, make_form( xxxRequestMode => 'X', - xxxRequestData => Encode::encode_utf8( - $self->to_xml - ), + xxxRequestData => $self->to_xml, ) ); croak 'Error connecting to server' unless $page; croak 'Server responded, but not in XML' unless $page =~ /^<\?xml/; + # The response is marked UTF-8, but it's really Latin-1. Sigh. + $page =~ s/^(<\?xml.*?) encoding="utf-8"/$1 encoding="iso-8859-1"/si; + $self->parse_response($page); } @@ -315,10 +356,10 @@ Business::OnlinePayment::InternetSecure - InternetSecure backend for Business::O $txn->content( action => 'Normal Authorization', - type => 'Visa', - card_number => '0000000000000000', - exp_date => '2004-07', - cvv2 => '000', # Optional + type => 'Visa', # Optional + card_number => '4111 1111 1111 1111', + expiration => '2004-07', + cvv2 => '000', # Optional name => "Fr\x{e9}d\x{e9}ric Bri\x{e8}re", company => '', @@ -330,10 +371,10 @@ Business::OnlinePayment::InternetSecure - InternetSecure backend for Business::O phone => '(555) 555-1212', email => 'fbriere@fbriere.net', - description => 'Online purchase', amount => 49.95, currency => 'CAD', taxes => 'GST PST', + description => 'Test transaction', ); $txn->submit; @@ -346,8 +387,8 @@ Business::OnlinePayment::InternetSecure - InternetSecure backend for Business::O =head1 DESCRIPTION -Business::OnlinePayment::InternetSecure is an implementation of -L that allows for processing online credit card +C is an implementation of +C that allows for processing online credit card payments through InternetSecure. See L for more information about the generic @@ -355,30 +396,27 @@ Business::OnlinePayment interface. =head1 CREATOR -Object creation is done via L; see its manpage for -details. The I processor option is required, and corresponds +Object creation is done via C; see its manpage for +details. The B processor option is required, and corresponds to the merchant ID assigned to you by InternetSecure. =head1 METHODS -(See L for more methods.) - -=head2 Before order submission +=head2 Transaction setup and transmission =over 4 =item content( CONTENT ) -Sets up the data prior to a transaction (overwriting any previous data by the -same occasion). CONTENT is an associative array (hash), containing some of -the following fields: +Sets up the data prior to a transaction. CONTENT is an associative array +(hash), containing some of the following fields: =over 4 =item action (required) What to do with the transaction. Only C is supported -for the moment. +at the moment. =item type @@ -394,19 +432,21 @@ Transaction type, being one of the following: =item - Discover +=item - JCB + =back (This is actually ignored for the moment, and can be left blank or undefined.) =item card_number (required) -Credit card number. Spaces are allowed, and will be automatically removed. +Credit card number. Spaces and dashes are automatically removed. -=item exp_date (required) +=item expiration (required) -Credit card expiration date. Since L does not specify +Credit card expiration date. Since C does not specify any syntax, this module is rather lax regarding what it will accept. The -recommended syntax is I, but forms such as I or I are +recommended syntax is C, but forms such as C or C are allowed as well. =item cvv2 @@ -421,15 +461,17 @@ Code (CVC2) or Card Identification number (CID), depending on the card issuer. =item description -A short description of the purchase. See L<"Products list syntax"> for +A short description of the transaction. See L<"Products list syntax"> for an alternate syntax that allows a list of products to be specified. -=item amount +=item amount (usually required) -Total amount to be billed, excluding taxes if they are to be added separately. -This field is required if B is a string, and should be left -undefined if B contains a list of products, as outlined in -L<"Products list syntax">. +Total amount to be billed, excluding taxes if they are to be added separately +by InternetSecure. + +This field is required if B is a string, but should be left +undefined if B contains a list of products instead, as outlined +in L<"Products list syntax">. =item currency @@ -438,38 +480,90 @@ C (default) or C. =item taxes -Taxes to be added automatically. These should not be included in B; -they will be automatically added by InternetSecure later on. +Taxes to be added automatically to B by InternetSecure. Available +taxes are C, C and C. -Available taxes are C, C and C. Taxes can be combined by -separating them with spaces, such as C. +This argument can either be a single string of taxes concatenated with spaces +(such as C), or a reference to an array of taxes (such as C<[ "GST", +"PST" ]>). =item name / company / address / city / state / zip / country / phone / email -Facultative customer information. B should be either a postal -abbreviation or a two-letter code taken from ISO 3166-2, and B should -be a two-letter code taken from ISO 3166-1. +Customer information. B should be a two-letter code taken from ISO +3166-1. =back +=item submit() + +Submit the transaction to InternetSecure. + =back -=head2 After order submission +=head2 Post-submission methods =over 4 -=item receipt_number() / sales_order_number() +=item is_success() + +Returns true if the transaction was submitted successfully. + +=item result_code() + +Response code returned by InternetSecure. + +=item error_message() + +Error message if the transaction was unsuccessful; C otherwise. (You +should not rely on this to test whether a transaction was successful; use +B() instead.) + +=item receipt_number() + +Receipt number (a string, actually) of this transaction, unique to all +InternetSecure transactions. + +=item order_number() + +Sales order number of this transaction. This is a number, unique to each +merchant, which is incremented by 1 each time. + +=item uuid() -Receipt number and sales order number of submitted order. +Universally Unique Identifier associated to this transaction. This is a +128-bit value returned as a 36-character string such as +C. See RFC 4122 for more details on +UUIDs. + +B() is provided as an alias to this method. + +=item authorization() + +Authorization code for this transaction. + +=item avs_code() / cvv2_response() + +Results of the AVS and CVV2 checks. See the InternetSecure documentation for +the list of possible values. + +=item date() + +Date and time of the transaction. Format is C. =item total_amount() Total amount billed for this order, including taxes. +=item tax_amounts() + +Returns a I to a hash that maps taxes, which were listed under the +B argument to B(), to the amount that was calculated by +InternetSecure. + =item cardholder() Cardholder's name. This is currently a mere copy of the B field passed -to B. +to B(). =item card_type() @@ -486,16 +580,9 @@ following: =item - Discover -=back +=item - JCB -=item avs_response() / cvv2_response() - -Results of the AVS and CVV2 checks. See the InternetSecure documentation for -the list of possible values. - -=item products_raw() - -... +=back =back @@ -505,20 +592,20 @@ the list of possible values. =head2 Products list syntax -Optionally, the B field of B can contain a reference +Optionally, the B field of B() can contain a reference to an array of products, instead of a simple string. Each element of this array represents a different product, and must be a reference to a hash with the following fields: =over 4 -=item amount +=item amount (required) Unit price of this product. =item quantity -Ordered quantity of this product. This can be a decimal value. +Ordered quantity of this product. =item sku @@ -531,25 +618,23 @@ Description of this product =item taxes Taxes that should be automatically added to this product. If specified, this -overrides the B field passed to B. +overrides the B field passed to B(). =back -When using a products list, the B field passed to B should +When using a products list, the B field passed to B() should be left undefined. =head2 Character encoding -Since communication to/from InternetSecure is encoded with UTF-8, all Unicode -characters are theoretically available when submitting information via -B. (Further restrictions may be imposed by InternetSecure itself.) +When using non-ASCII characters, all data provided to B() should +have been decoded beforehand via the C module, unless your data is in +ISO-8859-1 and you haven't meddled with the C pragma. (Please +don't.) -When using non-ASCII characters, all data provided to B should either -be in the current native encoding (typically latin-1, unless it was modified -via the C pragma), or be decoded via the C module. -Conversely, all data returned after calling B will be automatically -decoded. +InternetSecure currently does not handle characters outside of ISO-8859-1, so +these will be replaced with C before being transmitted. =head1 EXPORT