[interchange] CyberSource SOAP toolkit payment module

Mark Johnson interchange-cvs at icdevgroup.org
Mon Feb 8 15:25:40 UTC 2010


commit 627df179b11d3252783477a347939adc2473b119
Author: Mark Johnson <mark at endpoint.com>
Date:   Mon Feb 8 10:20:56 2010 -0500

    CyberSource SOAP toolkit payment module
    
    Provides full clientless (as in no Simple or SCMP
    API) support for the following CyberSource services:
    
    * Standard credit card
    * Bill Me Later
    * Paypal Express Checkout
    * Electronic check
    
    Squashed commit of the following:
    
    commit eb6e31d62115670cc2a0abe063cc3ff73831f986
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Mon Feb 8 10:13:03 2010 -0500
    
        Complete CyberSource documentation
    
        Fill in documentation missing for the newer Paypal and Electronic
        Check features.
    
    commit d5623a7287254d6df1f563221936ff4e287aa2b9
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Tue Oct 20 21:53:06 2009 -0400
    
        Fixed glitch on return_ec_set handling.
    
        * paypal_token was being wiped out since CYBS wasn't echoing the
          token in its reply.
    
    commit 1b6902c04914b784f6ad920c8c46068083148374
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Mon Aug 17 12:49:54 2009 -0400
    
        Converted check_num -> check_number
    
        The former was in standard, but the latter in map_actual(), which I
        took to be authoritative.
    
    commit 79f0292a912d6a95a66413b0c43aa085508b85fe
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Mon Aug 17 10:56:36 2009 -0400
    
        Adding electronic-check support.
    
    commit 27a0f38fb5c00aa8b698e506905e38577a31ab69
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Wed Aug 12 10:31:00 2009 -0400
    
        Adjustments from testing implementation
    
        * Added support for passing in generalized transaction_id, distinct
          from CYBS's transaction ID, that Paypal requires for follow-on
          transactions. So, CYBS tid = origid, PP tid = transaction_id.
    
        * Added documentation for transaction_id param, noting the relationship
          of the various PP services for its use.
    
        * Converted from using $Session to $::Session.
    
        * CYBS will enforce a conditional set of required features on the shipTo_*
          fields if any of them are present, despite them being technically optional.
          Since actual() tends to suck in anything floating around in $Values, added
          an address_ok requirement for using any address data at all.
    
        * Added conversion in request for the typically used UK to instead use GB
          for any UK address.
    
        * Restricted resetting $Session->{paypal_token} to only EC set transactions.
          Failures on dopayment attempts were (at least sometimes) coming back
          without echoing the token, which was killing the paypal session.
    
    commit fe2029d6869813962863d286c6a327806c089f46
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Tue Aug 4 11:52:18 2009 -0400
    
        Paypal documentation added.
    
    commit 694e1d7dcefd6d027810471ac7e56cd04fc74333
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Tue Aug 4 10:28:35 2009 -0400
    
        Paypal Support in Vend::Payment::CyberSource
    
    commit a6a7169bd3107506d88087b2c9221594c14185de
    Author: Mark Johnson <mark at endpoint.com>
    Date:   Mon Jul 20 10:07:05 2009 -0400
    
        Initial load of Vend::Payment::CyberSource

 lib/Vend/Payment/CyberSource.pm | 2469 +++++++++++++++++++++++++++++++++++++++
 1 files changed, 2469 insertions(+), 0 deletions(-)
---
diff --git a/lib/Vend/Payment/CyberSource.pm b/lib/Vend/Payment/CyberSource.pm
new file mode 100644
index 0000000..5050f5f
--- /dev/null
+++ b/lib/Vend/Payment/CyberSource.pm
@@ -0,0 +1,2469 @@
+# Vend::Payment::CyberSource - Interchange Cybersource SOAP Toolkit Support
+#
+# Copyright (C) 2009 Interchange Development Group and others
+#
+# Written by Mark Johnson <mark at endpoint.com>
+# based on code by Sonny Cook <sonny at endpoint.com>
+# and Mike Heins <mike at perusion.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public
+# License along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA  02111-1307  USA.
+
+package Vend::Payment::CyberSource;
+
+=head1 Interchange CyberSource Support
+
+Vend::Payment::CyberSource Revision: 1.0
+
+=head1 SYNOPSIS
+
+    &charge=cybersource
+ 
+        or
+ 
+    [charge gateway=cybersource param1=value1 param2=value2]
+
+       or, with a route named foo with gateway => cybersource
+
+    [charge route=foo]
+
+While you have free will, B<please> use a route. See below.
+
+=head1 PREREQUISITES
+
+ SOAP::Lite
+ XML::Pastor::Schema::Parser
+ LWP::Simple
+ OpenSSL
+
+=head1 DESCRIPTION
+
+The Vend::Payment::CyberSource module implements the cybersource() routine for
+use with Interchange. It is compatible on a call level with the other
+Interchange payment modules.
+
+To enable this module, place this directive in C<interchange.cfg>:
+
+    Require module Vend::Payment::CyberSource
+
+This I<must> be in interchange.cfg or a file included from it.
+
+Make sure CreditCardAuto is off (default in Interchange demos).
+
+This module supersedes Vend::Payment::ICS. With only minor adjustments to the
+payment route and some changes to the keys in $Session->{payment_result},
+Vend::Payment::CyberSource should function as a drop-in replacement for
+Vend::Payment::ICS. If you have legacy support for the SCMP API and don't need
+any of the new features supported in this module, you do not need to change.
+However, all new CyberSource clients will have to use this module as they do
+not allow new clients to use SCMP.
+
+If you do choose to replace the current use of Vend::Payment::ICS, you will
+have to abandon the merchant keys generated by ecert() (I know, it's a
+devastating blow) and instead generate a transaction key from within the online
+merchant account. That new transaction key will be added to your payment route.
+
+Whenever possible, please set your payment parameters using a payment route or
+passed as arguments to [charge] directly. No special effort has been made to
+consistently fall back on the charge_param() function, which plucks values out
+of the MV_PAYMENT_* variable space. It is not possible to isolate variables to
+a specific charge routine using charge_param() and, as such, it should be
+viewed with suspicion, rather than being used as a crutch. In particular, this
+module makes the assumption that certain critical route parameters will be
+present in $opt, and the best way to ensure they are in there is to use a
+payment route.
+
+=head2 Simple made Simple
+
+You may treat this gateway module as though it were the Simple API. The
+construction of the requests and responses within the Vend::Payment namespace
+will correspond either virtually (in the case of SOAP errors, it's probably
+different) or exactly (in the case of successful requests, even if the
+transaction itself is not successful) to the request/response name/value pairs
+documented for the Simple API.
+
+Unlike its name may imply, the Simple API is in fact not simple to install.
+Difficult becomes Impossible if you have a 64-bit OS. CyberSource as of this
+module's writing has not seen fit to port either of their client APIs (SCMP or
+Simple) to a 64-bit version. However, because this is actually the SOAP
+clientless toolkit wrapped to impersonate Simple, you get to by-pass the pain
+you'd otherwise have to feel to install Simple into your environment.
+
+B<What this means:> do I<NOT> install the Simple API! It will likely be a big
+pain and I<it is useless> for the purposes of this module. You only need to
+ensure the modules/software indicated in PREREQUISITES are installed.
+
+You can find the documentation for the Simple API at the following URL:
+
+http://apps.cybersource.com/library/documentation/sbc/api_guide/html/
+
+Again, do I<NOT> install the Simple API; just use the docs as though it is
+already installed.
+
+=head2 Options
+
+=over
+
+=item B<merchant_id>
+
+ID obtained from CyberSource. Likely the same value you use to log in to the
+online merchant interface for your account.
+
+=item B<transaction_key>
+
+Key generated from the CyberSource online merchant interface.
+
+=item B<api_version>
+
+Which version of Simple to use. When setting up, just pick the current one, and
+updates are likely unneeded unless new features come out that you need.
+
+You can see the full list of versions, and what features they each support (if
+you like reading XSD) here:
+
+https://ics2ws.ic3.com/commerce/1.x/transactionProcessor/
+
+The opt should be just the number, something like 1.44
+
+=item B<live>
+
+Calls go to production environment when value is 'true' (as in, the string
+true, and not just perly true, to give those already familiar with the actual
+Simple API warm fuzzy feelings). Any other value (or no value) is assumed to go
+to the test environment.
+
+=item B<xsd_cache_dir>
+
+Directory relative to catroot for caching the XSD files for each API version and
+from each environment (live or test). If not set, no caching will occur, but this
+is I<not> recommended.
+
+The module makes an initial request to CyberSource to get the XSD for whatever
+version of the API you're using, so that it knows what request values to look
+for and are valid.  Ideally, you don't want every single request repeating this
+process.
+
+If this option is set, the module will first look for the appropriate XSD
+associated with the correct version and environment in the cache dir. If it's
+not found, only then will it request the XSD from CyberSource. Once it's
+obtained from from CyberSource, it will cache it in the same location it
+initially looked, so all subsequent requests using the same API version in the
+same environment can use the cached XSD instead.
+
+There should be no reason to expire the cache, but if you wish to you'll have
+to do so manually. There's no built-in expiration mechanism. Because a
+published API will be static, the only reason you would need to expire the
+cached version is if it somehow became corrupted.
+
+Also, don't manually replace files in the cache. The XSD retrieved from
+CyberSource must be slightly tweaked to work with the XSD parsing module. That
+tweak is frozen into the cache. If you wish to refresh the cache, just remove
+the cache files and let the module itself pull down the correct version and
+cache it for you.
+
+=item B<acct_type>
+
+The type of the account for the customer's payment instrument. Currently
+supported:
+
+    * cc (standard credit card, the default)
+    * bml (Bill Me Later)
+    * pp (Paypal Express Checkout)
+    * ec (Electronic Check)
+
+=item B<transaction>
+
+Type of transaction to run.
+
+acct_type cc or bml:
+
+    * auth (or authorize, A)
+    * settle (or capture, D)
+    * sale (or S) - auth and settle in same request
+    * credit (or C)
+    * auth_reversal (or R) (not widely supported; use with caution)
+    * void (or V)
+
+For additional information on acct_type 'bml' transactions, see full B<Bill Me
+Later> section below.
+
+acct_type pp:
+
+    * pp_set (Exp. checkout set service)
+    * pp_get (Exp. checkout get service)
+    * pp_dopmt (Exp. Checkout do payment)
+    * pp_ord_setup (Exp. Checkout order setup)
+    * pp_auth (auth service)
+    * pp_bill (capture service)
+    * pp_sale (auth and capture together)
+    * pp_authrev (reverse auth)
+    * pp_refund (refund service)
+
+See B<Paypal> section below for full details
+
+acct_type ec:
+
+    * ec_debit
+    * ec_credit
+
+See B<Electronic Check> section below for full details
+
+=item B<origid>
+
+Original transaction ID referenced for follow-on transactions. E.g., the
+requestID of an auth that is to be captured.
+
+=item B<order_id or order_number>
+
+Passed over as merchantReferenceCode with request. Also very likely needed for
+any use of B<items_sub> (see below).
+
+=item B<apps>
+
+The list of allowable applications, space- or comma-separated, for this
+request. Added as a security measure to restrict use on any applications that
+aren't needed and might be a risk (such as credits if they'll never be issued
+through Interchange). All ics_* apps apply to cc and bml account types. pp_*
+and ec_* apply to account types pp and ec, respectively:
+
+    * ics_auth (needed for auth, sale)
+    * ics_auth_reversal (needed for auth_reversal)
+    * ics_bill (needed for settle, sale)
+    * ics_credit (needed for credit)
+    * ics_void (needed for void)
+    * pp_ec_set (needed for pp_set)
+    * pp_ec_get (needed for pp_get)
+    * pp_ec_dopmt (needed for pp_dopmt, pp_sale)
+    * pp_ec_ord_setup (needed for pp_ord_setup)
+    * pp_auth (needed for pp_auth)
+    * pp_capture (needed for pp_bill, pp_sale)
+    * pp_authrev (needed for pp_authrev)
+    * pp_refund (needed for pp_refund)
+    * ec_debit (needed for ec_debit)
+    * ec_credit (needed for ec_credit)
+
+It is recommended to exclude any apps you will not use from the list.
+
+=item B<ship_map>
+
+List of mv_shipmode values to map to the various allowable values in
+shipTo_shippingMethod. List is optional and defaults to 'lowcost'.
+
+The list should be in a form that is appropriate for perl's qw() to put the
+list into a hash. E.g.,:
+
+    GNDRES  lowcost
+    2DAY    twoday
+    ...
+
+List of possible CyberSource values:
+
+    * sameday
+    * oneday
+    * twoday
+    * threeday
+    * lowcost
+    * pickup
+    * other
+    * none
+
+=item B<mv_credit_card_number>
+
+Unlike many (if not the rest) of the gateway modules, you can pass in the card
+number as an option. However, the code prefers the value from the traditional
+source from $CGI via the actual opt. This option will mostly be unneeded.
+
+=item B<any Simple request keys>
+
+The code will take any of the Simple key names directly through $opt.  However,
+it will prefer any defined values associated with actual() over $opt, which may
+not be what you expect.
+
+=item B<items_sub>
+
+Custom subroutine that can be used to construct the order sent over to
+CyberSource. By default, module uses $Vend::Items. Routine will be essential
+for any processing of payments that are not directly associated with the
+immediate session. E.g., any payment interface developed for the admin to
+process transactions after the order already exists.
+
+Routine should return an array that matches the basics of an Interchange cart.
+Most likely use would be to construct a cart out of an order already in the
+database, but as long as the routine returns a cart-style array, it doesn't
+matter how it's constructed.
+
+Each line item needs the following attributes:
+
+    * code
+    * quantity
+
+To determine cost, each line-item hash is passed to Vend::Data::item_price().
+So, if your pricing demands more line-item attributes to calculate correctly,
+you'll need to ensure they are present.
+
+Routine can be either a catalog or global sub, with the code preferring the
+catalog sub. Args:
+
+    * Reference to copy of request hash
+    * $opt
+
+=item B<check_sub>
+
+Post-reply routine that can be used to alter the status and/or response hash
+returned from cybersource(). Examples of usage:
+
+=over
+
+=item *
+
+Auth succeeds, but we want it treat as a failure because of AVS response.
+
+=item *
+
+Auth fails, but error is a communication failure, so we want to treat it as
+success and follow up manually with customer to resolve.
+
+=item *
+
+Decision Manager result raises concerns, so we add a response parameter that
+indicates the order should not be fulfilled automatically, but rather funnel to
+a queue for manual review.
+
+=back
+
+Routine should return 'success' or 'failed' if it is to be authoritative as to
+that determination. Otherwise, it should return undef and let the code
+calculate 'success' or 'failed' based on the value of the 'decision' key in the
+response hash.
+
+It can be either a catalog or global sub, with the code preferring the catalog
+sub. Args:
+
+    * Reference to response hash (so it can be modified directly)
+    * Reference to a copy of the request hash
+    * $opt
+
+=item B<ip_address>
+
+IP to supply to CyberSource for the transaction. Optional and defaults to
+$Session->{shost} if defined, or $Session->{ohost} otherwise.
+
+=item B<shipping>
+
+Amount to supply CyberSource for shipping costs. Defaults to [shipping].
+
+=item B<amount>
+
+Amount to supply to CyberSource to apply to the transaction.
+
+Note that this value supersedes all the costs provided along with the order and
+shipping amounts. They do not have to agree. Supplying I<amount> means
+CyberSource will use that specific amount. If I<amount> is not supplied, I
+believe that CyberSource will construct a transaction amount from the sum of
+the order and shipping, but I do not recommend this.
+
+By default, I<amount> will be derived from [total-cost].
+
+=item B<timeout>
+
+Number of seconds for a request without a response before the process is
+killed.
+
+=item B<merrmsg, merrmsg_bml>
+
+Override the default error messages presented to users in the event of a failed
+transaction attempt, for B<acct_type> cc and bml, respectively. B<merrmsg> will
+run through sprintf() and can have the reason code and reason message, in that
+order, embedded in message with the use of %s as a placeholder. B<merrmsg_bml>
+has no such provision because the errors that come back from Paymentech for BML
+failures range between cryptic and useless.
+
+=back
+
+The following options are valid for Paypal usage only:
+
+=over
+
+=item B<returnurl>
+
+URL to which the customer's browser is returned after choosing to pay with
+PayPal.
+
+=item B<cancelurl>
+
+URL to which customers are returned if they do not approve the use of PayPal
+for payment.
+
+=item B<maxamount>
+
+Expected maximum total amount of the entire order, including shipping costs and
+tax charges.
+
+=item B<order_desc>
+
+Description of items the customer is purchasing.
+
+=item B<confirmshipping>
+
+Flag that indicates if you require the customer's shipping address on file with
+PayPal to be a confirmed address. 0 (default) not confirmed, 1 confirmed.
+
+=item B<noshipping>
+
+Flag that indicates if the shipping address should be displayed on the PayPal
+Web pages. 0 (default) show shipping, 1 suppress shipping display.
+
+=item B<addressoverride>
+
+Customer-supplied address sent in the SetExpressCheckout request rather than
+the address on file with PayPal for this customer.
+
+You can use this field only with the payment method, not with the shortcut
+method. See Overview of PayPal Express Checkout for a description of the PayPal
+methods.
+
+Possible values:
+
+0 (default): Display the address on file with PayPal. The customer cannot edit
+this address. 
+
+1: Display the customer-supplied address. The customer can edit this in PayPal
+Express Checkout.
+
+=item B<locale>
+
+Locale of pages displayed by PayPal during Express Checkout. 
+
+=item B<headerbackcolor>
+
+Background color for the header of the payment page. Format: HTML Hexadecimal
+color.
+
+=item B<headerbordercolor>
+
+Border color around the header of the payment page. Format: HTML Hexadecimal
+color.
+
+=item B<headerimg>
+
+URL for the image that will be displayed in the upper left area of the payment
+page.
+
+=item B<payflowcolor>
+
+Background color for the payment page. Format: HTML Hexadecimal color.
+
+=item B<pp_token>
+
+Timestamped token by which you identify to PayPal that you are processing this
+payment with Express Checkout. Corresponds with paypalToken description in
+CyberSource docs. Normally not needed as module will handle internally for most
+typical scenarios.
+
+=item B<pp_payer_id>
+
+Unique PayPal customer account identification number that was returned in the
+payPalEcGetDetailsService reply message. Corresponds with paypalPayerId
+description in CyberSource docs. Normally not needed as module will handle
+internally for most typical scenarios.
+
+=item B<return_ec_set>
+
+Boolean to indicate that an EC set call is to be issued as a return call; that
+is, a call to amend the user's Paypal data returned in a subsequent EC get call
+on the same session established by an initial EC set call.
+
+The same can be accomplished by calling EC set while explicitly supplying the
+paypal token and CyberSource request ID and token of the original call to EC
+set establishing that session.
+
+=item B<transaction_id>
+
+For use in captures, refunds, and auth reversals. Maps to the
+I<paypal-specific> transaction ID of the original request, and not the
+RequestID, which is the CyberSource transaction ID.
+
+Specifically, it should relate in the following manner:
+    I<Maps to Service Param>                               I<Obtained From>
+    payPalDoCaptureService_paypalAuthorizationId        payPalEcDoPaymentReply_transactionId
+    payPalAuthReversalService_paypalAuthorizationId     payPalAuthorizationReply_transactionId
+    payPalRefundService_paypalCaptureId                 payPalDoCaptureReply_transactionId
+
+=back
+
+=head2 Bill Me Later
+
+This module provides full support for Bill Me Later (BML) transactions. Some
+important notes on using BML with this module:
+
+See CyberSource's full BML documentation for details:
+
+http://apps.cybersource.com/library/documentation/dev_guides/CC_Svcs_IG_BML_Supplement/html/
+
+=over
+
+=item *
+
+You'll want to add in the extra fields collected for BML. Suggested process is
+to use the remap feature in Vend::Payment::charge(). Thus, add to your route:
+
+    Route foo remap <<EOV
+        bml_customer_registration_date bml_customer_registration_date
+        bml_customer_type_flag bml_customer_type_flag
+        bml_item_category bml_item_category
+        bml_product_delivery_type_indicator bml_product_delivery_type_indicator
+        bml_tc_version bml_tc_version
+        customer_ssn customer_ssn
+        date_of_birth date_of_birth
+        b_phone b_phone
+    EOV
+
+This will allow those fields to be used directly from your form and be picked
+up by map_actual() for you. The names listed above correspond with the values
+as managed through SCMP. You can just as easily use, instead, those documented
+with Simple.
+
+=item *
+
+Set acct_type option to 'bml'.
+
+=item *
+
+On a failed BML attempt, the module sets [value suppress_bml] to 1. It's
+recommended you use this session setting to disable the BML option so as to
+encourage your customers to try to complete their order with a credit card.
+Chances are extremely remote that a subsequent BML attempt will succeed after a
+failure. However, this is not a BML requirement; you may choose to let
+customers try again and again to check out with BML.
+
+=item *
+
+On a successful BML authorization, you can find the customer's BML account
+number (which looks like a credit card starting with the digits 5049) in
+$Session->{payment_result}{ccAuthReply_bmlAccountNumber}. Using this account
+number for repeat customers will speed up the authorization process, but it is
+not required.  Without it, your BML customers will have to re-enter their
+last-four SSN and DOB each time.
+
+Note that BML does not consider this account number sensitive. It can be freely
+stored unencrypted in the customer's user record.
+
+=back
+
+=head2 Paypal
+
+This module also fully implements CyberSource's integration for Express
+Checkout services. Note that CyberSource doesn't appear to support all Paypal
+services, so depending on your needs (e.g., Mass Pay), you may need another
+option. However, for typical Express Checkout usage, it is fully supported.
+
+See CyberSource's full Paypal documentation for details:
+
+http://apps.cybersource.com/library/documentation/dev_guides/PayPal_Express_IG/html/
+
+=over
+
+=item *
+
+Set acct_type option to 'pp'
+
+=item *
+
+Module is set up to handle most standard cases with minimal demand on the
+developer. Paypal requires toting around the paypal session ID and, in the case
+of dopayment, the payer ID. Further, CyberSource requires that the request ID
+and token from the EC set be passed back on each subsequent request associated
+with the same paypal session. Finally, using address override needs to be
+tracked between a set call and subsequent get call. The module will track these
+values in the user's IC session and most likely "do the right thing". You can,
+however, override any of these explicitly if you have the need or wish to
+control it explicitly.
+
+=item *
+
+The reply fields are stripped down to their canonical values for convenience.
+Many times, the same param merely differs in its reply field based on the
+application under which it was processed. E.g., the "amount" returned will come
+back in one of the following forms:
+
+    payPalEcSetReply_amount
+    payPalEcDoPaymentReply_amount
+    payPalEcOrderSetupReply_amount
+    payPalAuthorizationReply_amount
+    payPalDoCaptureReply_amount
+
+Any of these will be stripped down to just "amount" in the payment_result hash,
+but the fully qualified parameter is left in place, too, if needed.
+
+=item *
+
+It is recommended that you utilize B<check_sub> in conjunction with an EC get
+request for migrating response values from Paypal into the Interchange session.
+The following is a sample sub that works in conjunction with a typical
+configuration based off the standard demo:
+
+    Sub load_values <<EOS
+    sub {
+        my ($resp, $req, $opt) = @_;
+
+        return unless $resp->{decision} eq 'ACCEPT';
+
+        my $b_pre = $Values->{pp_use_billing_address}
+            ? 'b_'
+            : ''
+        ;
+
+        $Values->{$b_pre . 'phone_day'}     = $resp->{payerPhone};
+
+        $Values->{email}            = $resp->{payer};
+        $Values->{payerid}          = $resp->{PayerId};
+        $Values->{payerstatus}      = $resp->{payerStatus};
+        $Values->{payerbusiness}    = $resp->{payerBusiness};
+        $Values->{salutation}       = $resp->{payerSalutation};
+        $Values->{mname}            = $resp->{payerMiddlename};
+        $Values->{suffix}           = $resp->{payerSuffix};
+        $Values->{address_status}   = $resp->{addressStatus};
+        $Values->{countryname}      = $resp->{countryName};
+
+        unless ($Session->{paypal_override}) {
+            $Values->{$b_pre . 'fname'}     = $resp->{payerFirstname};
+            $Values->{$b_pre . 'lname'}     = $resp->{payerLastname};
+            $Values->{$b_pre . 'address1'}  = $resp->{shipToAddress1};
+            $Values->{$b_pre . 'address2'}  = $resp->{shipToAddress2};
+            $Values->{$b_pre . 'city'}      = $resp->{shipToCity};
+            $Values->{$b_pre . 'state'}     = $resp->{shipToState};
+            $Values->{$b_pre . 'zip'}       = $resp->{shipToZip};
+            $Values->{$b_pre . 'country'}   = $resp->{shipToCountry};
+        }
+
+        return;
+    }
+    EOS
+
+=item *
+
+When making an EC set call through [charge], the module will return the
+appropriate URL to which to redirect the user's browser rather than the typical
+value of the transaction ID. Thus, when running an EC set, it is appropriate to
+capture and test the return value from [charge] to determine both if the
+request was successful and, if so, where to direct the user.
+
+Example mv_click code:
+
+    [button
+        text="Checkout with Paypal"
+        form=basket
+    ]   
+        [tmp redirect][charge route=paypal_set][/tmp]
+        [if scratch redirect]
+            [bounce href="[scratch redirect]"]
+        [/if]
+    [/button]
+
+=back
+
+=head2 Electronic Checks
+
+This module fully implements CyberSource's electronic check support. See
+documentation for full details:
+
+http://apps.cybersource.com/library/documentation/dev_guides/Electronic_Checks_IG/html/
+
+=over
+
+=item *
+
+Set acct_type option to 'ec'
+
+=item *
+
+Includes mappings to use the standard demo's default check payment option.
+
+=back
+
+=head2 Troubleshooting
+
+Try the instructions above in test mode (live set to anything but string
+'true'). A test order should complete.
+
+Switch to production mode, then try an auth or sale with the card number C<4111
+1111 1111 1111> and a valid expiration date. The transaction should be denied,
+and the reason should be in [data session payment_error].
+
+If nothing works:
+
+=over 4
+
+=item *
+
+Make sure you "Require"d the module in interchange.cfg:
+
+    Require module Vend::Payment::CyberSource
+
+=item *
+
+Make sure all the modules/applications listed in PREREQUISITES are installed
+and working. You can test to see whether your Perl thinks they are:
+
+    perl -MSOAP::Lite \
+    -MXML::Pastor::Schema::Parser \
+    -MLWP::Simple \
+    -e 'print "It works\n"'
+
+If it prints "It works." and returns to the prompt you should be OK (presuming
+they are in working order otherwise).
+
+=item *
+
+Check the error logs, both catalog and global. Also set debugging on in
+C<interchange.cfg> and check the debug log!
+
+=item *
+
+Make sure you set your payment parameters properly, and of course you used a
+payment route, right?
+
+=item *
+
+Try an order, then put this code in a page:
+
+    <XMP>
+    [calc]
+        my $string = $Tag->uneval( { ref => $Session->{payment_result} });
+        $string =~ s/{/{\n/;
+        $string =~ s/,/,\n/g;
+        return $string;
+    [/calc]
+    </XMP>
+
+That should show what happened.
+
+=item *
+
+If all else fails, consultants are available to help with integration for a fee.
+See http://www.icdevgroup.org/ for mailing lists and other information.
+
+=back
+
+=head1 BUGS
+
+Naturally none. OK, at least none known. Be sure to post any found to the IC
+user list or you can send them to the author directly.
+
+=head1 AUTHOR
+
+Mark Johnson <mark at endpoint.com>
+(Based primarily off of Vend::Payment::ICS by Sonny Cook <sonny at endpoint.com>)
+
+=cut
+
+::logGlobal('Loading module ' . __PACKAGE__);
+
+package Vend::Payment;
+use strict;
+
+my $debug_scrub = sub {
+    # Utility to scrub out any mv_credit_card_* or
+    # *_ssn values from a hash containing those data for
+    # the purpose of writing to the debug log.
+
+    local $_ = ::uneval(shift);
+
+    my $scrub_keys =
+        join (
+            '|',
+            qw/
+                card_cvNumber
+                card_accountNumber
+                card_pin
+                mv_credit_card_[^']+
+                [^']+_ssn
+            /
+        )
+    ;
+
+    s{
+        (
+            '
+            (?: $scrub_keys )
+            ' \s+ => \s+ '
+        )
+        (
+            (?:
+                \\'
+              | \\
+              | [^\\']*
+            )*
+        )
+        (')
+    }{$1 . ('X' x length($2)) . $3}xsmge;
+
+    return $_;
+};
+
+sub cybersource {
+#::logDebug("cybersource called--in the begining");
+    my ($opt) = @_;
+    $opt->{order_number} ||= $opt->{order_id};
+
+::logDebug("cybersource opt hash: %s", $debug_scrub->($opt));
+
+    my %type_map = qw/
+        sale            auth_bill
+        auth            auth
+        auth_reversal   auth_reversal
+        authorize       auth
+        void            void
+        settle          bill
+        capture         bill
+        credit          credit
+        S               auth_bill
+        C               credit
+        D               bill
+        V               void
+        A               auth
+        R               auth_reversal
+        pp_set          pp_set
+        pp_get          pp_get
+        pp_dopmt        pp_dopmt
+        pp_ord_setup    pp_ord_setup
+        pp_auth         pp_auth
+        pp_bill         pp_bill
+        pp_sale         pp_sale
+        pp_authrev      pp_authrev
+        pp_refund       pp_refund
+        ec_debit        ec_debit
+        ec_credit       ec_credit
+    /;
+
+    my %inv_trans_map = qw/
+        auth          A
+        auth_bill     S
+        credit        C
+        auth_reversal R
+        void          V
+        bill          D
+    /;
+
+    my %app_map = (
+        auth            => [qw/ ics_auth /],
+        auth_bill       => [qw/ ics_auth ics_bill/],
+        auth_reversal   => [qw/ ics_auth_reversal /],
+        bill            => [qw/ ics_bill /],
+        credit          => [qw/ ics_credit /],
+        void            => [qw/ ics_void /],
+        pp_set          => [qw/ pp_ec_set /],
+        pp_get          => [qw/ pp_ec_get /],
+        pp_dopmt        => [qw/ pp_ec_dopmt /],
+        pp_ord_setup    => [qw/ pp_ec_ord_setup /],
+        pp_auth         => [qw/ pp_auth /],
+        pp_bill         => [qw/ pp_capture /],
+        pp_sale         => [qw/ pp_ec_dopmt pp_capture/],
+        pp_authrev      => [qw/ pp_authrev /],
+        pp_refund       => [qw/ pp_refund /],
+        ec_debit        => [qw/ ec_debit /],
+        ec_credit       => [qw/ ec_credit /],
+    );
+
+    my $transtype = $opt->{transaction}
+        || charge_param('transaction')
+        || 'auth';
+
+::logDebug("transaction type: $transtype");
+
+    $transtype = $type_map{$transtype}
+        or return (
+            MStatus => 'failure-hard',
+            MErrMsg => errmsg('Unrecognized transaction: %s', $transtype),
+        )
+    ;
+
+    my %acct_type_map = qw/
+        bml bml
+        cc  cc
+        pp  pp
+        ec  ec
+    /;
+
+    my $acct_type = $acct_type_map{ lc ($opt->{acct_type}) || 'cc' }
+        or return (
+            MStatus => 'failure-hard',
+            MErrMsg => errmsg('Unrecognized acct_type: %s', $opt->{acct_type}),
+        )
+    ;
+
+    ## get list of applications to use
+    my (@apps,%opt_apps);
+    @opt_apps{ grep { /\S/ } split ( /[,\s]+/, lc ($opt->{apps}) ) } = ();
+
+    for ( @{ $app_map{$transtype} } ) {
+        push (@apps, $_)
+            if exists $opt_apps{ lc ($_) };
+    }
+::logDebug ("Applications: " . ::uneval \@apps);
+
+    ## Allow IC-relative page paths for return and cancel URLs
+    if ($acct_type eq 'pp' && grep { /^pp_ec_set$/ } @apps) {
+        $opt->{$_} = $Tag->area({ href => $opt->{$_}, secure => 1 })
+            for (qw/returnurl cancelurl/);
+    }
+
+    ## Apps that require (or benefit from) a cart
+    my %items_apps = qw/
+        ics_auth        1
+        pp_ec_set       1
+        pp_ec_dopmt     1
+        pp_ec_ord_setup 1
+        pp_auth         1
+        ec_debit        1
+    /;
+
+    ## Required fields per app
+    my %required_map = (
+        all =>
+            [ qw/
+                    merchantID
+                    merchantReferenceCode
+                    purchaseTotals_currency
+                /
+            ],
+        ics_auth =>
+            [ qw/
+                    ccAuthService_run
+                    billTo_street1
+                    billTo_city
+                    billTo_country
+                    billTo_state
+                    billTo_postalCode
+                    card_expirationMonth
+                    card_expirationYear
+                    card_accountNumber
+                    billTo_email
+                    billTo_firstName
+                    billTo_lastName
+                    shipTo_street1
+                    shipTo_city
+                    shipTo_country
+                    shipTo_state
+                    shipTo_postalCode
+                /
+            ],
+        ics_auth_reversal =>
+            [ qw/
+                    ccAuthReversalService_run
+                    ccAuthReversalService_authRequestID
+                    ccAuthReversalService_authRequestToken
+                /
+            ],
+        ics_bill =>
+            [ qw/
+                    ccCaptureService_run
+                    ccCaptureService_authRequestID
+                    ccCaptureService_authRequestToken
+                /
+            ],
+        ics_credit =>
+            [ qw/
+                    ccCreditService_run
+                    billTo_street1
+                    billTo_city
+                    billTo_country
+                    billTo_state
+                    billTo_postalCode
+                    card_expirationMonth
+                    card_expirationYear
+                    card_accountNumber
+                    billTo_email
+                    billTo_firstName
+                    billTo_lastName
+                /
+            ],
+        ics_void =>
+            [ qw/
+                    voidService_run
+                    voidService_voidRequestID
+                    voidService_voidRequestToken
+                /
+            ],
+        pp_ec_set =>
+            [ qw/
+                    payPalEcSetService_run
+                    payPalEcSetService_paypalReturn
+                    payPalEcSetService_paypalCancelReturn
+                /
+            ],
+        pp_ec_get =>
+            [ qw/
+                    payPalEcGetDetailsService_run
+                    payPalEcGetDetailsService_paypalToken
+                    payPalEcGetDetailsService_paypalEcSetRequestID
+                    payPalEcGetDetailsService_paypalEcSetRequestToken
+                /
+            ],
+        pp_ec_dopmt =>
+            [ qw/
+                    payPalEcDoPaymentService_run
+                    payPalEcDoPaymentService_paypalToken
+                    payPalEcDoPaymentService_paypalPayerId
+                    payPalEcDoPaymentService_paypalEcSetRequestID
+                    payPalEcDoPaymentService_paypalEcSetRequestToken
+                    payPalEcDoPaymentService_paypalCustomerEmail
+                /
+            ],
+        pp_ec_ord_setup =>
+            [ qw/
+                    payPalEcOrderSetupService_run
+                    payPalEcOrderSetupService_paypalToken
+                    payPalEcOrderSetupService_paypalPayerId
+                    payPalEcOrderSetupService_paypalEcSetRequestID
+                    payPalEcOrderSetupService_paypalEcSetRequestToken
+                    payPalEcOrderSetupService_paypalCustomerEmail
+                /
+            ],
+        pp_auth =>
+            [ qw/
+                    payPalAuthorizationService_run
+                    payPalAuthorizationService_paypalCustomerEmail
+                    payPalAuthorizationService_paypalEcOrderSetupRequestID
+                    payPalAuthorizationService_paypalEcOrderSetupRequestToken
+                /
+            ],
+        pp_capture =>
+            [ qw/
+                    payPalDoCaptureService_run
+                    payPalDoCaptureService_completeType
+                    payPalDoCaptureService_paypalAuthorizationId
+                /
+            ],
+        pp_authrev =>
+            [ qw/
+                    payPalAuthReversalService_run
+                    payPalAuthReversalService_paypalAuthorizationId
+                /
+            ],
+        pp_refund =>
+            [ qw/
+                    payPalRefundService_run
+                    payPalRefundService_paypalCaptureId
+                    payPalRefundService_paypalDoCaptureRequestID
+                    payPalRefundService_paypalDoCaptureRequestToken
+                /
+            ],
+        ec_debit =>
+            [ qw/
+                    billTo_city
+                    billTo_country
+                    billTo_email
+                    billTo_firstName
+                    billTo_lastName
+                    billTo_phoneNumber
+                    billTo_postalCode
+                    billTo_state
+                    billTo_street1
+                    check_accountNumber
+                    check_accountType
+                    check_bankTransitNumber
+                    ecDebitService_run
+                /
+            ],
+        ec_credit =>
+            [ qw/
+                    ecCreditService_run
+                /
+            ],
+    );
+
+    my %exempt_map = (
+        billing_intl =>
+            [ qw/
+                    billTo_street1
+                    billTo_city
+                    billTo_country
+                    billTo_state
+                    billTo_postalCode
+                /
+            ],
+        shipping_intl =>
+            [ qw/
+                    shipTo_street1
+                    shipTo_city
+                    shipTo_country
+                    shipTo_state
+                    shipTo_postalCode
+                /
+            ],
+    );
+
+    ## These fields are not necessarily optional on our end,
+    ## they are just optional on CyberSource's end.
+    my %optional_map = (
+        all =>
+            [ qw/
+                    timeout
+                /
+            ],
+        ics_auth =>
+            [ qw/
+                    billTo_phoneNumber
+                    billTo_street2
+                    billTo_company
+                    businessRules_declineAVSFlags
+                    shipTo_street2
+                    card_cvNumber
+                    businessRules_ignoreAVSResult
+                    businessRules_ignoreCVResult
+                    invoiceHeader_merchantDescriptor
+                    invoiceHeader_merchantDescriptorContact
+                    shipTo_shippingMethod
+                /
+            ],
+        ics_auth_reversal => [],
+        ics_bill =>
+            [ qw/
+                    invoiceHeader_merchantDescriptor
+                    invoiceHeader_merchantDescriptorContact
+                /
+            ],
+        ics_credit =>
+            [ qw/
+                    invoiceHeader_merchantDescriptor
+                    invoiceHeader_merchantDescriptorContact
+                    ccCreditService_captureRequestID
+                    ccCreditService_captureRequestToken
+                /
+            ],
+        ics_void => [],
+        pp_ec_set =>
+            [ qw/
+                    payPalEcSetService_paypalEcSetRequestID
+                    payPalEcSetService_paypalEcSetRequestToken
+                    payPalEcSetService_invoiceNumber
+                    payPalEcSetService_paypalAddressOverride
+                    payPalEcSetService_paypalCustomerEmail
+                    payPalEcSetService_paypalDesc
+                    payPalEcSetService_paypalHdrbackcolor
+                    payPalEcSetService_paypalHdrbordercolor
+                    payPalEcSetService_paypalHdrimg
+                    payPalEcSetService_paypalLc
+                    payPalEcSetService_paypalMaxamt
+                    payPalEcSetService_paypalNoshipping
+                    payPalEcSetService_paypalPayflowcolor
+                    payPalEcSetService_paypalReqconfirmshipping
+                    payPalEcSetService_paypalToken
+                    payPalEcSetService_promoCode0
+                    payPalEcSetService_requestBillingAddress
+                    shipTo_street1
+                    shipTo_street2
+                    shipTo_city
+                    shipTo_country
+                    shipTo_state
+                    shipTo_phone
+                    shipTo_postalCode
+                    shipTo_firstName
+                    shipTo_lastName
+                /
+            ],
+        pp_ec_get => [],
+        pp_ec_dopmt =>
+            [ qw/
+                    payPalEcDoPaymentService_invoiceNumber
+                    payPalEcDoPaymentService_paypalAddressOverride
+                    payPalEcDoPaymentService_paypalDesc
+                    payPalEcDoPaymentService_promoCode0
+                    shipTo_street1
+                    shipTo_street2
+                    shipTo_city
+                    shipTo_country
+                    shipTo_state
+                    shipTo_phone
+                    shipTo_postalCode
+                    shipTo_firstName
+                    shipTo_lastName
+                /
+            ],
+        pp_ec_ord_setup =>
+            [ qw/
+                    payPalEcOrderSetupService_invoiceNumber
+                    payPalEcOrderSetupService_paypalDesc
+                    payPalEcOrderSetupService_promoCode0
+                /
+            ],
+        pp_auth =>
+            [ qw/
+                    payPalAuthorizationService_paypalOrderId
+                /
+            ],
+        pp_capture =>
+            [ qw/
+                    payPalDoCaptureService_invoiceNumber
+                    payPalDoCaptureService_paypalAuthorizationRequestID
+                    payPalDoCaptureService_paypalAuthorizationRequestToken
+                    payPalDoCaptureService_paypalEcDoPaymentRequestID
+                    payPalDoCaptureService_paypalEcDoPaymentRequestToken
+                /
+            ],
+        pp_authrev =>
+            [ qw/
+                    payPalAuthReversalService_paypalEcDoPaymentRequestID
+                    payPalAuthReversalService_paypalEcOrderSetupRequestID
+                    payPalAuthReversalService_paypalAuthorizationRequestID
+                    payPalAuthReversalService_paypalEcDoPaymentRequestToken
+                    payPalAuthReversalService_paypalEcOrderSetupRequestToken
+                    payPalAuthReversalService_paypalAuthorizationRequestToken
+                /
+            ],
+        pp_refund =>
+            [ qw/
+                    payPalRefundService_paypalNote
+                /
+            ],
+        ec_debit =>
+            [ qw/
+                    billTo_company
+                    billTo_companyTaxID
+                    billTo_dateOfBirth
+                    billTo_driversLicenseNumber
+                    billTo_driversLicenseState
+                    billTo_ipAddress
+                    billTo_street2
+                    businessRules_declineAVSFlags
+                    check_accountEncoderID
+                    check_checkNumber
+                    check_secCode
+                    ecDebitService_commerceIndicator
+                    ecDebitService_partialPaymentID
+                    ecDebitService_paymentMode
+                    ecDebitService_referenceNumber
+                    ecDebitService_settlementMethod
+                    ecDebitService_verificationLevel
+                    invoiceHeader_merchantDescriptor
+                    recurringSubscriptionInfo_subscriptionID
+                /
+            ],
+        ec_credit =>
+            [ qw/
+                    billTo_city
+                    billTo_country
+                    billTo_dateOfBirth
+                    billTo_email
+                    billTo_firstName
+                    billTo_ipAddress
+                    billTo_lastName
+                    billTo_phoneNumber
+                    billTo_postalCode
+                    billTo_state
+                    billTo_street1
+                    billTo_street2
+                    check_accountEncoderID
+                    check_accountNumber
+                    check_accountType
+                    check_bankTransitNumber
+                    check_checkNumber
+                    ecCreditService_commerceIndicator
+                    ecCreditService_debitRequestID
+                    ecCreditService_debitRequestToken
+                    ecCreditService_partialPaymentID
+                    ecCreditService_referenceNumber
+                    ecCreditService_settlementMethod
+                    invoiceHeader_merchantDescriptor
+                    orderRequestToken
+                    recurringSubscriptionInfo_subscriptionID
+                /
+            ],
+    );
+
+    my %default_map = qw/
+        timeout                             20
+        businessRules_ignoreAVSResult       true
+        businessRules_ignoreCVResult        true
+        purchaseTotals_currency             usd
+        bml_itemCategory                    3700
+        bml_productDeliveryTypeIndicator    shipping_and_handling
+        ccAuthService_run                   true
+        ccAuthReversalService_run           true
+        ccCaptureService_run                true
+        ccCreditService_run                 true
+        voidService_run                     true
+        payPalEcSetService_run              true
+        payPalEcGetDetailsService_run       true
+        payPalEcDoPaymentService_run        true
+        payPalEcOrderSetupService_run       true
+        payPalAuthorizationService_run      true
+        payPalDoCaptureService_run          true
+        payPalAuthReversalService_run       true
+        payPalRefundService_run             true
+        payPalDoCaptureService_completeType Complete
+        ecDebitService_run                  true
+        ecCreditService_run                 true
+        check_accountType                   C
+    /;
+
+    my %actual_map = qw/
+        billTo_street1          b_address1
+        billTo_street2          b_address2
+        billTo_city             b_city
+        billTo_country          b_country
+        billTo_state            b_state
+        billTo_postalCode       b_zip
+        card_expirationMonth    mv_credit_card_exp_month
+        card_expirationYear     mv_credit_card_exp_year
+        card_accountNumber      mv_credit_card_number
+        card_cvNumber           mv_credit_card_cvv2
+        billTo_email            email
+        billTo_firstName        b_fname
+        billTo_lastName         b_lname
+        billTo_phoneNumber      b_phone
+        shipTo_street1          address1
+        shipTo_street2          address2
+        shipTo_city             city
+        shipTo_country          country
+        shipTo_state            state
+        shipTo_postalCode       zip
+        shipTo_firstName        fname
+        shipTo_lastName         lname
+        shipTo_phoneNumber      phone_day
+        shipTo_shippingMethod   mv_shipmode
+
+        payPalEcDoPaymentService_paypalCustomerEmail    email
+        payPalEcOrderSetupService_paypalCustomerEmail   email
+        payPalAuthorizationService_paypalCustomerEmail  email
+        payPalEcSetService_paypalCustomerEmail          email
+
+        bml_customerRegistrationDate        bml_customer_registration_date
+        bml_customerTypeFlag                bml_customer_type_flag
+        bml_itemCategory                    bml_item_category
+        bml_productDeliveryTypeIndicator    bml_product_delivery_type_indicator
+        bml_tcVersion                       bml_tc_version
+        billTo_ssn                          customer_ssn
+        billTo_dateOfBirth                  date_of_birth
+
+        check_accountNumber     check_account
+        check_bankTransitNumber check_routing
+        check_checkNumber       check_number
+    /;
+
+    my %opt_map = qw/
+        merchantID                              merchant_id
+        invoiceHeader_merchantDescriptor        merchant_descriptor
+        invoiceHeader_merchantDescriptorContact merchant_descriptor_contact
+        merchantReferenceCode                   order_number
+
+        ccAuthReversalService_authRequestID     origid
+        ccCaptureService_authRequestID          origid
+        ccAuthReversalService_authRequestToken  request_token
+        ccCaptureService_authRequestToken       request_token
+        ccCreditService_captureRequestID        origid
+        ccCreditService_captureRequestToken     request_token
+        voidService_voidRequestID               origid
+        voidService_voidRequestToken            request_token
+        ecCreditService_debitRequestID          origid
+        orderRequestToken                       request_token
+
+        payPalEcGetDetailsService_paypalEcSetRequestID              origid
+        payPalEcGetDetailsService_paypalEcSetRequestToken           request_token
+        payPalEcDoPaymentService_paypalEcSetRequestID               origid
+        payPalEcDoPaymentService_paypalEcSetRequestToken            request_token
+        payPalEcOrderSetupService_paypalEcSetRequestID              origid
+        payPalEcOrderSetupService_paypalEcSetRequestToken           request_token
+        payPalAuthorizationService_paypalEcOrderSetupRequestID      origid
+        payPalAuthorizationService_paypalEcOrderSetupRequestToken   request_token
+        payPalRefundService_paypalDoCaptureRequestID                origid
+        payPalRefundService_paypalDoCaptureRequestToken             request_token
+
+        payPalEcSetService_invoiceNumber            order_number
+        payPalEcDoPaymentService_invoiceNumber      order_number
+        payPalEcOrderSetupService_invoiceNumber     order_number
+        payPalDoCaptureService_invoiceNumber        order_number
+
+        payPalDoCaptureService_paypalAuthorizationId        transaction_id
+        payPalAuthReversalService_paypalAuthorizationId     transaction_id
+        payPalRefundService_paypalCaptureId                 transaction_id
+
+        payPalEcSetService_paypalReturn         returnurl
+        payPalEcSetService_paypalCancelReturn   cancelurl
+        payPalEcSetService_paypalMaxamt         maxamount
+
+        payPalEcSetService_paypalDesc           order_desc
+        payPalEcDoPaymentService_paypalDesc     order_desc
+        payPalEcOrderSetupService_paypalDesc    order_desc
+
+        payPalEcSetService_paypalReqconfirmshipping     confirmshipping
+        payPalEcSetService_paypalNoshipping             noshipping
+
+        payPalEcSetService_paypalAddressOverride        addressoverride
+        payPalEcDoPaymentService_paypalAddressOverride  addressoverride
+
+        payPalEcSetService_paypalLc                 locale
+        payPalEcSetService_paypalHdrbackcolor       headerbackcolor
+        payPalEcSetService_paypalHdrbordercolor     headerbordercolor
+        payPalEcSetService_paypalHdrimg             headerimg
+        payPalEcSetService_paypalPayflowcolor       payflowcolor
+
+        payPalEcGetDetailsService_paypalToken       pp_token
+        payPalEcDoPaymentService_paypalToken        pp_token
+        payPalEcOrderSetupService_paypalToken       pp_token
+        payPalEcSetService_paypalToken              pp_token
+
+        payPalEcDoPaymentService_paypalPayerId      pp_payer_id
+        payPalEcOrderSetupService_paypalPayerId     pp_payer_id
+    /;
+
+    # Shipping Types Map - ours to Cybersource's
+    my %ship_method = $opt->{ship_map} =~ /\S/
+        ? $Vend::Interpolate::ready_safe->reval(qq{ qw( $opt->{ship_map} ) })
+        : ()
+    ;
+
+    ::logError("Error in ship_map route param: $@")
+        if $@;
+
+::logDebug('%%ship_method: %s', ::uneval(\%ship_method));
+
+    ## Special Cases
+    $required_map{ics_bill} = [ 'ccCaptureService_run' ]
+        if $transtype eq 'auth_bill';
+
+    $required_map{pp_capture} =
+        [ qw/
+                payPalDoCaptureService_run
+                payPalDoCaptureService_completeType
+            /
+        ]
+            if $transtype eq 'pp_sale';
+
+    my %actual = $opt->{actual} ? %{ $opt->{actual} } : map_actual();
+    $actual{mv_credit_card_number} ||= $opt->{mv_credit_card_number};
+
+::logDebug('%%actual: %s', $debug_scrub->(\%actual));
+
+    my @required_keys = (
+        @{ $required_map{all} },
+    );
+
+    push (@required_keys, @{ $required_map{$_} })
+        for @apps;
+
+    my @optional_keys = (
+        @{ $optional_map{all} },
+    );
+
+    push (@optional_keys, @{ $optional_map{$_} })
+        for @apps;
+
+#::logDebug('@required_keys: %s', ::uneval(\@required_keys));
+#::logDebug('@optional_keys: %s', ::uneval(\@optional_keys));
+#::logDebug('$opt: %s', $debug_scrub->($opt));
+
+    # BML
+    if ($acct_type eq 'bml') {
+
+        delete @actual{qw/mv_credit_card_exp_month mv_credit_card_exp_year/};
+
+        push (@required_keys, qw/
+            bml_customerTypeFlag
+            bml_itemCategory
+            bml_productDeliveryTypeIndicator
+            card_cardType
+        /);
+
+        push (@optional_keys, qw/
+            billTo_phoneNumber
+            bml_billToPhoneType
+            bml_customerBillingAddressChange
+            bml_customerEmailChange
+            bml_customerHasCheckingAccount
+            bml_customerHasSavingsAccount
+            bml_customerPasswordChange
+            bml_customerPhoneChange
+            bml_employerCity
+            bml_employerCompanyName
+            bml_employerCountry
+            bml_employerPhoneNumber
+            bml_employerPhoneType
+            bml_employerPostalCode
+            bml_employerState
+            bml_employerStreet1
+            bml_employerStreet2
+            bml_grossHouseholdIncome
+            bml_householdIncomeCurrency
+            bml_merchantPromotionCode
+            bml_preapprovalNumber
+            bml_residenceStatus
+            bml_shipToPhoneType
+            bml_yearsAtCurrentResidence
+            bml_yearsWithCurrentEmployer
+            shipTo_email
+            shipTo_firstName
+            shipTo_lastName
+            shipTo_phoneNumber
+        /);
+
+        $default_map{bml_customerRegistrationDate} =
+            POSIX::strftime('%Y%m%d',localtime(time));
+        $default_map{bml_customerTypeFlag} = 'N';
+        $default_map{card_cardType} = '028';
+        $default_map{card_expirationMonth} = '12';
+        $default_map{card_expirationYear} = '2021';
+        $default_map{card_accountNumber} = '5049900000000000';
+
+        if ( $opt->{origid} || $actual{mv_credit_card_number} ) {
+            push (@optional_keys, qw/
+                bml_customerRegistrationDate
+                bml_tcVersion
+            /);
+            $default_map{bml_customerTypeFlag} = 'E';
+        }
+
+        else {
+            push (@required_keys, qw/
+                billTo_dateOfBirth
+                billTo_ssn
+                bml_customerRegistrationDate
+                bml_tcVersion
+            /);
+        }
+     }
+
+    if ($acct_type eq 'pp') {
+        delete @{ $::Session }{qw/paypal_token paypal_request_id paypal_request_token/}
+            if $transtype eq 'pp_set'
+                && ! $opt->{return_ec_set};
+
+        $::Session->{paypal_override} = $opt->{addressoverride}
+            if grep { /^pp_ec_set$/ } @apps;
+
+        $opt->{pp_token} ||= $::Session->{paypal_token};
+        $opt->{origid} ||= $::Session->{paypal_request_id};
+        $opt->{request_token} ||= $::Session->{paypal_request_token};
+        $opt->{pp_payer_id} ||= $::Session->{paypal_payer_id};
+    }
+
+#::logDebug('After BML special block');
+#::logDebug('@required_keys: %s', ::uneval(\@required_keys));
+#::logDebug('@optional_keys: %s', ::uneval(\@optional_keys));
+
+    ## Build Request
+    my %request_keys;
+    my @request_keys = (@required_keys, @optional_keys);
+    @request_keys{ @request_keys } = ();
+
+    ## Only allow shipTo_* on 'pp' requests when
+    ## address_ok
+    if ($acct_type eq 'pp' && ! $opt->{address_ok}) {
+        delete $request_keys{$_}
+            for grep { /^shipTo_/ } keys %request_keys;
+    }
+
+    my %request;
+    for (keys %request_keys) {
+      
+        next if $transtype eq 'auth_bill' && /^invoiceHeader/;
+
+        $request{$_} = $actual{$_}
+            if defined $actual{$_};
+
+        $request{$_} = $actual{ $actual_map{$_} }
+            if defined $actual{ $actual_map{$_} };
+
+        $request{$_} = $opt->{$_}
+            if defined $opt->{$_};
+
+        $request{$_} = $opt->{ $opt_map{$_} }
+            if defined $opt->{ $opt_map{$_} };
+    }
+    
+    # Uses the {purchaseTotals_currency} -> MV_PAYMENT_CURRENCY options if set
+    $request{purchaseTotals_currency} = charge_param('currency')
+        || ($Vend::Cfg->{Locale} && $Vend::Cfg->{Locale}{currency_code})
+        || 'usd';
+
+    # Fix the shipmode
+    $request{shipTo_shippingMethod} =
+        $ship_method{ $request{shipTo_shippingMethod} }
+        || 'lowcost'
+            unless $acct_type eq 'pp';
+    
+    ## Add defaults
+    for (@request_keys) {
+        next if length ($request{$_});
+        next unless defined $default_map{$_};
+        $request{$_} = $default_map{$_};
+    }
+
+    ## build exempt keys hash
+    my %exempt_keys = ();
+    for (keys %exempt_map) {
+        ## these keys apply only if we are shipping outside the
+        ## US or CA
+        next if $_ eq 'billing_intl'
+            && (
+                lc $request{billTo_country} eq 'us'
+                ||
+                lc $request{billTo_country} eq 'ca'
+            )
+        ;
+
+        next if $_ eq 'shipping_intl'
+            && (
+                lc $request{shipTo_country} eq 'us'
+                ||
+                lc $request{shipTo_country} eq 'ca'
+            )
+        ;
+
+        $exempt_keys{$_} = 1
+            for @{ $exempt_map{$_} };
+    }
+
+    ## make sure that we have ALL required fields filled
+    ## exempt fields can be present, but are not required
+
+    for (@required_keys) {
+        unless ( defined $request{$_} || $exempt_keys{$_} ) {
+            return (
+                MStatus => 'failure-hard',
+                MErrMsg => errmsg("Missing value for >$_< field"),
+            );
+        }
+    }
+
+    my $idx = 0;
+    if ( scalar grep { $items_apps{$_} } @apps ) {
+        my @items;
+        if (my $sub_name = $opt->{items_sub}) {
+            my $sub = $Vend::Cfg->{Sub}{$sub_name}
+                || $Global::GlobalSub->{$sub_name};
+            if (ref ($sub) eq 'CODE') {
+                @items = $sub->( { %request }, $opt);
+            }
+            else {
+                ::logError('cybersource: non-existent items_sub routine %s', $sub_name);
+            }
+        }
+        @items = @$Vend::Items
+            unless @items;
+
+#::logDebug('building item lines based on @items');
+
+        for ( @items ) {
+            my $cost = Vend::Data::item_price($_);
+            $request{"item_${idx}_productCode"} = 'default';
+            $request{"item_${idx}_productSKU"} = $_->{code};
+            $request{"item_${idx}_unitPrice"} =
+                sprintf ('%0.2f', $cost);
+            $request{"item_${idx}_quantity"} = $_->{quantity};
+
+            ++$idx;
+        }
+
+        $request{billTo_ipAddress} =
+            $opt->{ip_address}
+            || $::Session->{shost}
+            || $::Session->{ohost}
+                unless $request{bml_tcVersion} eq '12103'
+                    || $acct_type eq 'pp';
+
+        $opt->{shipping} ||= $Tag->shipping({ noformat => 1 });
+        $opt->{handling} ||= $Tag->handling({ noformat => 1 });
+    }
+
+    # Adding shipping offer per BML requirement
+    if ($opt->{shipping} > 0) {
+        $request{"item_${idx}_productCode"} = 'shipping_only';
+        $request{"item_${idx}_unitPrice"} =
+            sprintf ('%0.2f', $opt->{shipping});
+        $request{"item_${idx}_quantity"} = 1;
+        ++$idx;
+    }
+
+    # Handling, if any
+    if ($opt->{handling} > 0) {
+        $request{"item_${idx}_productCode"} = 'handling_only';
+        $request{"item_${idx}_unitPrice"} =
+            sprintf ('%0.2f', $opt->{handling});
+        $request{"item_${idx}_quantity"} = 1;
+        ++$idx;
+    }
+
+    ## declare total cost
+    $request{purchaseTotals_grandTotalAmount} = $opt->{total_cost};
+
+    ## Specific data filters for BML that pass CS validation
+    ## and other miscellaneous hacks that have nothing to do
+    ## with CyberSource generally.
+    if ($acct_type eq 'bml') {
+    
+        for ( grep { /phone.*number/i } keys %request ) {
+            $request{$_} =~ s/\D+//g;
+            $request{$_} =~ s/^[10]+//;
+        }
+
+        for ( grep { /date/i } keys %request ) {
+            $request{$_} =~ s/-//g;
+        }
+
+        if ( $request{shipTo_country} !~ /^(?:US)?$/ ) {
+
+            for (keys %request) {
+                my ($stuff) = /^billTo_(.*)/ or next;
+                next unless exists $request_keys{"shipTo_$stuff"};
+                $request{"shipTo_$stuff"} = $request{$_};
+            }
+
+            $request{shipTo_street2} = 'Foreign Delivery';
+        }
+    }
+
+    # Strip any trailing digits from email fields
+    $request{$_} =~ s/\d+$//
+        for (grep { /email/i && defined $request{$_} } keys %request);
+
+    if ( length ($request{card_expirationYear}) == 2 ) {
+        $request{card_expirationYear} +=
+            $request{card_expirationYear} < 70
+                ? 2000
+                : 1900
+        ;
+    }
+
+    $request{bml_productDeliveryTypeIndicator} = 'electronic_software'
+        if $request{bml_itemCategory} eq '4700';
+
+    my $cybs = Vend::Payment::CyberSource->new($opt);
+    my %resp;
+
+    # Wrapping full call code to APIs in eval to
+    # implement interaction time limit on response.
+    eval {
+
+        my $timeout = delete $request{timeout};
+        $timeout = $opt->{timeout} if $opt->{timeout};
+
+        my $should_have_died;
+        my $sigalrm_die_msg = 'alarm-induced gateway timeout';
+
+        ## Scrounge up any country fields for PayPal requests
+        ## and convert them from UK -> GB. If you want to convert
+        ## back, you'll almost certainly want to write a check_sub
+        if ($acct_type eq 'pp') {
+            $request{$_} = 'GB'
+                for grep
+                    { /country/i && lc ($request{$_}) eq 'uk' }
+                        keys %request
+            ;
+        }
+
+        local ($SIG{ALRM}) = sub {
+            $should_have_died = 1;
+            die $sigalrm_die_msg;
+        };
+
+::logDebug("cybersource Sending Request...\n%s", $debug_scrub->(\%request));
+
+        my $start = time;
+        alarm $timeout;
+
+        my $rv = $cybs->send(\%request, \%resp);
+
+        alarm 0;
+        die $sigalrm_die_msg if $should_have_died;
+
+        my $end = time;
+
+::logDebug("cybersource Response (%ds):\n%s", $end - $start, ::uneval(\%resp));
+
+        # Initiate a timeout payment if the gateway response
+        # itself was a timeout error
+        die 'API-induced gateway timeout'
+            if $resp{reasonCode} == 151
+                || $resp{reasonCode} == 250;
+
+    }; # End eval
+
+    if ($@) {
+        ::logDebug("eval trapped die in gateway call: $@ ");
+    }
+
+    my %reason_code_map = (
+        100 => 'Successful transaction',
+        101 => 'The request is missing one or more required fields',
+        102 => 'One or more fields in the request contains invalid data',
+        150 => 'General system failure',
+        151 => 'The request was received but there was a server timeout',
+        152 => 'The request was received, but a service did not finish running in time',
+        200 => 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the Address Verification Service (AVS) check',
+        201 => 'The issuing bank has questions about the request',
+        202 => 'Card is expired or date is a mismatch with the date the issuing bank has on file',
+        203 => 'Card is declined',
+        204 => 'Insufficient funds in the account',
+        205 => 'Stolen or lost card',
+        207 => 'Issuing bank unavailable',
+        208 => 'Inactive card or card not authorized for card-not-present transactions',
+        209 => 'American Express Card Identification Digits (CID) did not match',
+        210 => 'The card has reached the credit limit',
+        211 => 'Invalid card verification number',
+        221 => "The customer matched an entry on the processor's negative file",
+        223 => 'PayPal rejected the transaction',
+        230 => 'The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification (CV) check',
+        231 => 'Invalid account number',
+        232 => 'The card type is not accepted by the payment processor',
+        233 => 'General decline by the processor',
+        234 => 'There is a problem with the merchant configuration',
+        235 => 'The requested amount exceeds the originally authorized amount',
+        236 => 'Processor failure',
+        237 => 'The authorization has already been reversed',
+        238 => 'The authorization has already been captured',
+        239 => 'The requested transaction amount must match the previous transaction amount',
+        240 => 'The card type sent is invalid or does not correlate with the credit card number',
+        241 => 'The request ID is invalid',
+        242 => 'Capture request has no corresponding, unsettled authorization record',
+        243 => 'The transaction has already been settled or reversed',
+        246 => 'The capture or credit is not voidable',
+        247 => 'You requested a credit for a capture that was previously voided',
+        250 => 'The request was received, but there was a timeout at the payment processor',
+        387 => 'PayPal authorization failed',
+    );
+
+    # Stripping out irritating prefix of each service run from each different
+    # type of transaction. Will leave in the unadulterated versions too, in
+    # case anyone actually needs that distinction present.
+
+    for (grep { /_run$/ && $request{$_} eq 'true'} keys %request) {
+        my ($base) = /(.*)Service_run/;
+        $base .= 'Reply_';
+        for (keys %resp) {
+            my ($ckey) = /^$base(.*)/
+                or next;
+            $resp{$ckey} ||= $resp{$_};
+        }
+    }
+
+    if ($acct_type eq 'pp') {
+
+        if (grep { /^pp_ec_set$/ } @apps) {
+            $::Session->{paypal_request_id} = $resp{requestID};
+            $::Session->{paypal_request_token} = $resp{requestToken};
+            $::Session->{paypal_token} = $resp{paypalToken}
+                unless $opt->{return_ec_set};
+        }
+        elsif (grep { /^pp_ec_get$/ } @apps) {
+            $::Session->{paypal_payer_id} = $resp{payerId};
+        }
+
+        $resp{pp_redirect} = sprintf ($resp{pp_redirect}, $resp{paypalToken} || $::Session->{paypal_token});
+    }
+
+    if ($transtype =~ /^auth(?:_bill)?$/) {
+        my ($avs_addr, $avs_zip) = $cybs->handle_avs($resp{ccAuthReply_avsCode});
+        my $cv = $cybs->handle_cv($resp{ccAuthReply_cvCode});
+        $resp{AVSADDR}   = $avs_addr;
+        $resp{AVSZIP}    = $avs_zip;
+        $resp{CVV2MATCH} = $cv;
+    }
+
+    my $status;
+    if (my $sub_name = $opt->{check_sub}) {
+        my $sub = $Vend::Cfg->{Sub}{$sub_name}
+            || $Global::GlobalSub->{$sub_name};
+        if (ref ($sub) eq 'CODE') {
+            $status = $sub->(\%resp, { %request }, $opt);
+        }
+        else {
+            ::logError('cybersource: non-existent check_sub routine %s', $sub_name);
+        }
+    }
+
+    if (!$status) {
+        $status = $resp{decision} eq 'ACCEPT'
+            ? 'success'
+            : 'failed'
+        ;
+    }
+
+    $resp{MStatus}    = $status;
+    $resp{'order-id'} = $transtype eq 'pp_set'
+        ? $resp{pp_redirect}
+        : $resp{requestID}
+    ;
+    $resp{PNREF}      = $resp{requestID};
+    $resp{transtype}  = $inv_trans_map{$transtype} || $transtype;
+    $resp{acct_type}  = $acct_type;
+    $resp{rc_msg}     = $reason_code_map{ $resp{reasonCode} } || 'Unknown';
+
+::logDebug("decision: $status, reason code: $resp{reasonCode}");
+
+    if ($status ne 'success') {
+        if ($acct_type eq 'bml') {
+
+            $::Values->{suppress_bml} = 1;
+
+            $resp{MErrMsg} = errmsg($opt->{merrmsg_bml} || <<EOP);
+We're sorry, Bill Me Later<sup>&reg;</sup> was unable to authorize your
+transaction. Please accept our sincerest apologies. You are welcome
+to complete your order using any of our other payment methods.
+EOP
+        }
+        else {
+
+            my $str = $opt->{merrmsg} || <<'EOP';
+Error code: %s (%s). Please call in your order or try again.
+EOP
+            my $msg = errmsg(
+                $str,
+                $resp{faultCode}
+                    || $resp{reasonCode}
+                    || 'no details available'
+                ,
+                $resp{faultDetail}
+                    || $resp{rc_msg}
+                    || 'unknown error'
+                ,
+            );
+            $resp{MErrMsg} = $resp{'pop.error-message'} = $msg;
+        }
+    }
+
+::logDebug("Returning %%resp: %s",::uneval(\%resp));
+    return %resp;
+}
+
+package Vend::Payment::CyberSource;
+
+BEGIN {
+    my @mod_failures;
+    eval {
+        require SOAP::Lite;
+    };
+    push (@mod_failures, "SOAP::Lite ($@)")
+        if $@;
+
+    eval {
+        require XML::Pastor::Schema::Parser;
+    };
+    push (@mod_failures, "XML::Pastor::Schema::Parser ($@)")
+        if $@;
+
+    eval {
+        require LWP::Simple;
+    };
+    push (@mod_failures, "LWP::Simple ($@)")
+        if $@;
+
+    if (@mod_failures) {
+        my $list = join ("\n\n* ", @mod_failures);
+        die <<EOP;
+Following required modules failed to load:
+
+* $list
+EOP
+    }
+}
+
+# new() expects to be called with a standard payment-route
+# $opt hash. If the route has been sufficiently defined (and one
+# doesn't rely on the unwise practice of charge_param()), the 
+# constructor will be able to build the object data with no special
+# effort on the part of the caller.
+sub new {
+    my $class = shift;
+    my $self = bless ({}, $class);
+    $self->init(shift);
+    return $self;
+}
+
+# Set up object attributes and helper objects from
+# XML::Pastor::Schema::Parser
+sub init {
+    my $self = shift;
+    my $opt = shift;
+    %$self = (
+        MERCHANT_ID => $opt->{merchant_id},
+        TRANSACTION_KEY => $opt->{transaction_key},
+        CYBS_HOST => $opt->{live} eq 'true' ? 'ics2ws.ic3.com' : 'ics2wstest.ic3.com',
+        PP_302_HOST => $opt->{live} eq 'true' ? 'www.paypal.com' : 'www.sandbox.paypal.com',
+        CYBS_VERSION => $opt->{api_version},
+        WSSE_NSURI => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
+        WSSE_PREFIX => 'wsse',
+        PASSWORD_TEXT => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText',
+    );
+
+    my ($xsd, $file);
+    if ($opt->{xsd_cache_dir}) {
+        (my $dir = $opt->{xsd_cache_dir}) =~ s{/+$}{};
+        $file = "$dir/"
+            . $self->attr('CYBS_HOST')
+            . "/cybs_v"
+            . $self->attr('CYBS_VERSION')
+            . ".xsd";
+
+        $xsd = Vend::File::readfile($file);
+::logDebug("cybersource: found cached xsd for version " . $self->attr('CYBS_VERSION') . " at $file") if $xsd;
+    }
+
+    unless ($xsd) {
+        $xsd =
+            LWP::Simple::get(
+                sprintf (
+                    'https://%s/commerce/1.x/transactionProcessor/CyberSourceTransaction_%s.xsd',
+                    $self->attr('CYBS_HOST'),
+                    $self->attr('CYBS_VERSION'),
+                )
+            )
+        ;
+
+        # XML::Pastor::Schema::Parser doesn't know what to do with xsd:any.
+        # Since it doesn't matter to us, just coerce it to xsd:element.
+        $xsd =~ s!(?<=<xsd:)any !element !g;
+
+        Vend::File::writefile(
+            ">$file",
+            \$xsd,
+            { auto_create_dir => 1 }
+        )
+            if $file;
+    }
+
+    $self->{_model} =
+        XML::Pastor::Schema::Parser
+            -> new
+            -> parse( schema => $xsd )
+    ;
+
+    # Top-level RequestMessage object is our primary interest.
+    $self->{_rmsg} = $self->model->xml_item('RequestMessage');
+
+    return;
+}
+
+#  Results of address verification. This field will contain one of the following values:
+
+#      * A: Street number matches, but 5-digit ZIP code and 9-digit ZIP code do not match.
+#      * B: Street address match for non-U.S. AVS transaction. Postal code not verified.
+#      * C: Street address and postal code not verified for non-U.S. AVS transaction.
+#      * D: Street address and postal code match for non-U.S. AVS transaction.
+#      * E: AVS data is invalid.
+#      * G: Non-U.S. card issuing bank does not support AVS.
+#      * I: Address information not verified for non-U.S. AVS transaction.
+#      * M: Street address and postal code match for non-U.S. AVS transaction.
+#      * N: Street number, 5-digit ZIP code, and 9-digit ZIP code do not match.
+#      * P: Postal code match for non-U.S. AVS transaction. Street address not verified.
+#      * R: System unavailable.
+#      * S: Issuing bank does not support AVS.
+#      * U: Address information unavailable. Returned if non-U.S. AVS is not available or if the AVS in a U.S. bank is not functioning properly.
+#      * W: Street number does not match, but 5-digit ZIP code and 9-digit ZIP code match.
+#      * X: Exact match. Street number, 5-digit ZIP code, and 9-digit ZIP code match.
+#      * Y: Both street number and 5-digit ZIP code match.
+#      * Z: 5-digit ZIP code matches.
+#      * 1: CyberSource does not support AVS for this processor or card type.
+#      * 2: The processor returned an unrecognized value for the AVS response.
+
+sub handle_avs {
+    my $self = shift;
+    my $c =  shift;
+    ## returns (address, zip)
+    ## D,M,H,V,X,Y
+    return ('Y', 'Y') if $c =~ /^[DMHVXY]$/;
+
+    ## A,O
+    return ('Y', 'N') if ($c eq 'A' || $c eq 'O');
+
+    ## L,W,Z
+    return ('N', 'Y') if $c =~ /^[LWZ]$/;
+
+    ## P,F
+    return ('', 'Y') if ($c eq 'P' || $c eq 'F');
+
+    ## B,T
+    return ('Y', '') if ($c eq 'B' || $c eq 'T');
+
+    ## N,K
+    return ('N', 'N') if ($c eq 'N' || $c eq 'K');
+
+    ## C,E,G,I,R,S,U,1,2,3,4
+    return ('', '');
+}
+
+#  Result of processing the card verification number. This field will contain one of the following values:
+
+#      * M: Card verification number matched.
+#      * N: Card verification number not matched.
+#      * P: Card verification number not processed.
+#      * S: Card verification number is on the card but was not included in the request.
+#      * U: Card verification is not supported by the issuing bank.
+#      * X: Card verification is not supported by the card association.
+#      * <space>: Deprecated. Ignore this value.
+#      * 1: CyberSource does not support card verification for this processor or card type.
+#      * 2: The processor returned an unrecognized value for the card verification response.
+#      * 3: The processor did not return a card verification result code.
+
+sub handle_cv {
+    my $self = shift;
+    my $c = shift;
+    return 'Y' if $c eq 'M';
+    return 'N' if $c eq 'N';
+    return '';
+}
+
+# Main function to send request to CyberSource and process response. The
+# request and response constructions should virtually or exactly match the
+# request/response value descriptions of CyberSource's "Simple" API. Note,
+# however, this does not depend on the Simple API, but is actually using the
+# SOAP toolkit. Thus, please do NOT install any of the Simple API as it is
+# /not/ Simple, and not necessary.
+
+sub send {
+    my $self = shift;
+    my ($req, $resp) = @_;
+
+    my $service =
+        SOAP::Lite
+            -> uri(
+                sprintf (
+                    'urn:schemas-cybersource-com:transaction-data-%s',
+                    $self->attr('CYBS_VERSION')
+                )
+            )
+            -> proxy(
+                sprintf (
+                    'https://%s/commerce/1.x/transactionProcessor',
+                    $self->attr('CYBS_HOST')
+                )
+            )
+            -> autotype(0)
+    ;
+
+#::logDebug('$service: %s', ::uneval($service));
+
+    my $header = $self->form_header;
+
+    my @request;
+    my (%items, %service);
+    $self->other(
+        {
+            clientLibrary => 'Perl',
+            clientLibraryVersion => $],
+            clientEnvironment => $^O,
+        }
+    );
+
+# Services and Items are two nodes that deviate from CyberSource's standard of
+# indicating complex data relations with an underscore in the key name. Thus,
+# they have to be handled specially, outside the underscore/complex-data
+# relationship. For the remainder, we assume the underscore/complex-data
+# relationship and store them away using other() for standard recursive
+# processing.
+
+    while (my ($k,$v) = each %$req) {
+        if ($k =~ /^item_(\d+)_(.*)/) {
+            $items{$1}{$2} = $v;
+        }
+        elsif ($k =~ /^([^_]+Service)_(.*)/) {
+            $service{$1}{$2} = $v;
+        }
+        else {
+            $self->other($k => $v);
+        }
+    }
+
+    my @service_keys = keys %service;
+    for (@service_keys) {
+        next if delete ($service{$_}{run}) eq 'true';
+        delete $service{$_};
+    }
+
+    for my $term (@{ $self->rmsg->elements }) {
+        if ($service{$term}) {
+            my @service;
+            my $hsh = $service{$term};
+            my $type = $self->rmsg->elementInfo->{$term}->type;
+            if (! defined ($type) ) {
+                ::logError("cybersource: No element info returned for service type $term. Check API version. Skipping");
+                next;
+            }
+            my $list = $self->model
+                            ->xml_item(split (/[|]/, $type, 2))
+                            ->elements;
+            for my $k (@$list) {
+                $self->add_field(\@service, $k, $hsh->{$k})
+                    if exists $hsh->{$k};
+            }
+            $self->add_service(\@request, $term, \@service, 'true');
+        }
+        elsif ($term eq 'item') {
+            my $type = $self->rmsg->elementInfo->{$term}->type;
+            if (! defined ($type) ) {
+                ::logError("cybersource: No element info returned for term $term. Check API version. Skipping");
+                next;
+            }
+            my $list = $self->model->xml_item(split (/[|]/, $type, 2))->elements;
+            for my $idx (sort {$a <=> $b} keys %items) {
+                my @item;
+                my $hsh = $items{$idx};
+                for my $k (@$list) {
+                    $self->add_field(\@item, $k, $hsh->{$k})
+                        if exists $hsh->{$k};
+                }
+                $self->add_item(\@request, $idx, \@item);
+            }
+        }
+        else {
+            $self->resolve_term(\@request, [ $term ]);
+        }
+    }
+
+#::logDebug('@request: %s', ::uneval(\@request));
+#::logDebug('$header: %s', ::uneval($header));
+
+    my $reply =
+        $service->call(
+            requestMessage => @request,
+            $header
+        )
+    ;
+
+#::logDebug('raw $reply: %s', ::uneval($reply));
+
+    my $reply_hash;
+    if ($reply->fault) {
+        $resp = {
+            faultCode => $reply->faultcode,
+            faultString => $reply->faultstring,
+            faultDetail => $reply->faultdetail,
+        };
+    }
+    else {
+        $reply_hash = $reply->valueof('//Body/replyMessage');
+        $self->resolve_reply($resp, $reply_hash );
+    }
+
+#::logDebug('valueof on replyMessage: %s', ::uneval ($reply_hash));
+    return 1;
+}
+
+# Handle standard underscore/complex-data relationships to convert
+# them into appropriate SOAP calls. Recursion allows function to
+# handle arbitrarily deep complex data. Essentially converts standard
+# Simple API calls to SOAP toolkit calls.
+sub resolve_term {
+    my $self = shift;
+    my $request = shift;
+    my $list = shift;
+    my $node = shift || $self->rmsg;
+    my $pre = shift || '';
+
+    for my $name (@$list) {
+        my $type = $node->elementInfo->{$name}->type;
+        my $subnode = defined $type
+            ? $self->model->xml_item(split (/[|]/, $type, 2))
+            : undef
+        ;
+        my $key = $pre . $name;
+        if (! defined ($subnode) || $subnode->contentType eq 'simple') {
+            # We are at a scalar node. Check for its existence in caller's
+            # request and, if present, add it to $request
+            $self->add_field($request, $name, $self->other($key))
+                if defined $self->other($key);
+        }
+        elsif ($subnode->contentType eq 'complex') {
+            # Start a recursion for a complex element.
+            my @complex;
+            $self->resolve_term(
+                \@complex,
+                $subnode->elements,
+                $subnode,
+                $key . '_'
+            );
+            # Only add if, somewhere within the complex
+            # type, at least one scalar was added.
+            $self->add_complex_type(
+                $request,
+                $name,
+                \@complex
+            )
+                if @complex;
+        }
+        else {
+            die sprintf (
+                "cybersource: resolve_term() failed on $key. Obj dump: %s",
+                ::uneval($subnode)
+            );
+        }
+    }
+    return;
+}
+
+# Store and retrieve Simple-API key/value pairs that will
+# later be dissected by resolve_term()
+sub other {
+    my $self = shift;
+    my $k = shift;
+
+    if (ref $k) {
+        $self->{_other} = $k;
+        return;
+    }
+
+    $self->{_other} = {}
+        unless ref $self->{_other};
+
+    if (@_) {
+        $self->{_other}{$k} = shift;
+    }
+
+    return $self->{_other}{$k};
+}
+
+sub model {
+    return $_[0]->{_model};
+}
+
+sub rmsg {
+    return $_[0]->{_rmsg};
+}
+
+sub attr {
+    return $_[0]->{$_[1]};
+}
+
+sub resolve_reply {
+    my $self = shift;
+    my $resp = shift;
+    my $reply = shift;
+    my $pre = shift || '';
+
+    while (my ($k, $v) = each %$reply) {
+        my $key = $pre . $k;
+        if (ref $v) {
+            $self->resolve_reply(
+                $resp,
+                $v,
+                $key . '_'
+            );
+        }
+        else {
+            $resp->{$key} = $v;
+        }
+    }
+    $resp->{pp_redirect} =
+        'https://'
+        . $self->attr('PP_302_HOST')
+        . '/cgi-bin/webscr?cmd=_express-checkout&token=%s'
+    ;
+    return;
+}
+
+# Following subs extracted out of CyberSource's sample processing script.
+#------------------------------------------------------------------------------
+# adds a tag to the referenced parent element with $name as the tagname and
+# $val its content. $val may be either text or a reference to a non-leaf node.
+#------------------------------------------------------------------------------
+sub add_field {
+    my $self = shift;
+    my ($parentRef, $name, $val) = @_;
+    push(@$parentRef, SOAP::Data->name($name => $val));
+}
+
+#------------------------------------------------------------------------------
+# adds a complex type (i.e. non-leaf node) to the referenced parent element
+# with $name as the tagname and $complexTypeRef the reference to the complex
+# type.
+#------------------------------------------------------------------------------
+sub add_complex_type {
+    my $self = shift;
+    my ($parentRef, $name, $complexTypeRef) = @_;
+    $self->add_field(
+        $parentRef,
+        $name,
+        \SOAP::Data->value(@$complexTypeRef)
+    );
+}
+
+#------------------------------------------------------------------------------
+# adds a line item to the referenced parent element with $index as the id
+# attribute and $itemRef as the reference to the item node.
+#------------------------------------------------------------------------------
+sub add_item  {
+    my $self = shift;
+    my ($parentRef, $index, $itemRef) = @_;
+  # note the leading space in ' id'.  Without it, the id attribute would not
+  # be added to the item tag.  SOAP::Lite seems to be using the "id" attribute
+  # for its own tracking/identification purposes.
+    push(
+        @$parentRef,
+        SOAP::Data
+            -> name( item => \SOAP::Data->value(@$itemRef) )
+            -> attr({' id' => $index})
+    );
+}
+
+#------------------------------------------------------------------------------
+# adds a service node to the referenced parent element with $name as the
+# tagname and $serviceRef as the reference to the service node. $run must be
+# either 'true' or 'false'.
+#------------------------------------------------------------------------------
+sub add_service {
+    my $self = shift;
+    my ($parentRef, $name, $serviceRef, $run) = @_;
+    push(
+        @$parentRef,
+        SOAP::Data
+            -> name( $name => \SOAP::Data->value(@$serviceRef) )
+            -> attr({run => $run})
+    );
+}
+
+#------------------------------------------------------------------------------
+# forms the SOAP header with the UsernameToken in it.
+#------------------------------------------------------------------------------
+sub form_header {
+    my $self = shift;
+    my %tokenHash;
+    $tokenHash{Username} =
+        SOAP::Data
+            -> type('' => $self->attr('MERCHANT_ID'))
+            -> prefix($self->attr('WSSE_PREFIX'))
+    ;
+
+    $tokenHash{Password} =
+        SOAP::Data
+            -> type('' => $self->attr('TRANSACTION_KEY'))
+            -> attr({'Type' => $self->attr('PASSWORD_TEXT')})
+            -> prefix($self->attr('WSSE_PREFIX'))
+    ;
+
+    my $usernameToken =
+        SOAP::Data
+            -> name( 'UsernameToken' => \%tokenHash )
+            -> prefix($self->attr('WSSE_PREFIX'))
+    ;
+
+    my $header =
+        SOAP::Header
+            -> name(
+                Security => {
+                    UsernameToken =>
+                        SOAP::Data->type(
+                            '' => $usernameToken
+                        )
+                }
+            )
+            -> uri($self->attr('WSSE_NSURI'))
+            -> prefix($self->attr('WSSE_PREFIX'))
+    ;
+
+    return $header;
+}
+
+1;



More information about the interchange-cvs mailing list