[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>®</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