[interchange] CyberSource SOAP toolkit payment module

Mark Johnson <[email protected]>
Newsgroups gmane.comp.web.interchange.cvs
Message-ID <[email protected]>
commit 627df179b11d3252783477a347939adc2473b119
Author: Mark Johnson <[email protected]>
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 <[email protected]>
    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 <[email protected]>
    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 <[email protected]>
    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 <[email protected]>
    Date:   Mon Aug 17 10:56:36 2009 -0400
    
        Adding electronic-check support.
    
    commit 27a0f38fb5c00aa8b698e506905e38577a31ab69
    Author: Mark Johnson <[email protected]>
    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 <[email protected]>
    Date:   Tue Aug 4 11:52:18 2009 -0400
    
        Paypal documentation added.
    
    commit 694e1d7dcefd6d027810471ac7e56cd04fc74333
    Author: Mark Johnson <[email protected]>
    Date:   Tue Aug 4 10:28:35 2009 -0400
    
        Paypal Support in Vend::Payment::CyberSource
    
    commit a6a7169bd3107506d88087b2c9221594c14185de
    Author: Mark Johnson <[email protected]>
    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 <[email protected]>
+# based on code by Sonny Cook <[email protected]>
+# and Mike Heins <[email protected]>
+#
+# 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 <[email protected]>
+(Based primarily off of Vend::Payment::ICS by Sonny Cook <[email protected]>)
+
+=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;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.