require 'securerandom'
require 'digest'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# ==== USA ePay Advanced SOAP Interface
#
# This class encapsulates USA ePay's Advanced SOAP Interface. The Advanced Soap Interface allows
# standard transactions, storing customer information, and recurring billing. Storing sensitive
# information on USA ePay's servers can help with PCI DSS compliance, since customer and card data
# do not need to be stored locally.
#
# Make sure you have enabled this functionality for your account with USA ePay.
#
# Information about the Advanced SOAP interface is available on the {USA ePay wiki}[http://wiki.usaepay.com/developer/soap].
#
# ==== Login, Password, and Software ID
#
# Please follow all of USA ePay's directions for acquiring all accounts and settings.
#
# The value used for :login is the Key value found in the Merchant Console under Settings > Source
# Key. You will have to add this key in the USA ePay Merchant Console.
#
# The value used for :password is the pin value also found and assigned in the Merchant Console under
# Settings > Source Key. The pin is required to use all but basic transactions in the SOAP interface.
# You will have to add the pin to your source key, as it defaults to none.
#
# The value used for the :software_id is found in the Developer's Login under the Developers Center
# in your WSDL. It is the 8 character value in tag. A masked example:
#
# It is also found in the link to your WSDL. This is required as every account has a different path
# SOAP requests are submitted to. Optionally, you can provide the entire urls via :live_url and :test_url, if your prefer.
#
# ==== Responses
# * #success? -- +true+ if transmitted and returned correctly
# * #message -- response or fault message
# * #authorization -- reference_number or nil
# * #params -- hash of entire soap response contents
#
# ==== Address Options
# * :billing_address/:shipping_address -- contains some extra options
# * :name -- virtual attribute; will split to first and last name
# * :first_name
# * :last_name
# * :address1
# * :address2
# * :city
# * :state
# * :zip
# * :country
# * :phone
# * :email
# * :fax
# * :company
#
# ==== Support:
# * Questions: post to {active_merchant google group}[http://groups.google.com/group/activemerchant]
# * Feedback/fixes: matt (at) nearapogee (dot) com
#
# ==== Links:
# * {USA ePay Merchant Console}[https://sandbox.usaepay.com/login]
# * {USA ePay Developer Login}[https://www.usaepay.com/developer/login]
#
class UsaEpayAdvancedGateway < Gateway
API_VERSION = '1.4'
TEST_URL_BASE = 'https://sandbox.usaepay.com/soap/gate/' #:nodoc:
LIVE_URL_BASE = 'https://www.usaepay.com/soap/gate/' #:nodoc:
self.test_url = TEST_URL_BASE
self.live_url = LIVE_URL_BASE
FAILURE_MESSAGE = 'Default Failure' #:nodoc:
self.supported_countries = ['US']
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
self.homepage_url = 'http://www.usaepay.com/'
self.display_name = 'USA ePay Advanced SOAP Interface'
CUSTOMER_PROFILE_OPTIONS = {
id: [:string, 'CustomerID'], # merchant assigned number
notes: [:string, 'Notes'],
data: [:string, 'CustomData'],
url: [:string, 'URL']
} #:nodoc:
CUSTOMER_RECURRING_BILLING_OPTIONS = {
enabled: [:boolean, 'Enabled'],
schedule: [:string, 'Schedule'],
number_left: [:integer, 'NumLeft'],
currency: [:string, 'Currency'],
description: [:string, 'Description'],
order_id: [:string, 'OrderID'],
user: [:string, 'User'],
source: [:string, 'Source'],
send_receipt: [:boolean, 'SendReceipt'],
receipt_note: [:string, 'ReceiptNote']
} #:nodoc:
CUSTOMER_POINT_OF_SALE_OPTIONS = {
price_tier: [:string, 'PriceTier'],
tax_class: [:string, 'TaxClass'],
lookup_code: [:string, 'LookupCode']
} #:nodoc:
CUSTOMER_OPTIONS = [
CUSTOMER_PROFILE_OPTIONS,
CUSTOMER_RECURRING_BILLING_OPTIONS,
CUSTOMER_POINT_OF_SALE_OPTIONS
].inject(:merge) #:nodoc:
COMMON_ADDRESS_OPTIONS = {
first_name: [:string, 'FirstName'],
last_name: [:string, 'LastName'],
city: [:string, 'City'],
state: [:string, 'State'],
zip: [:string, 'Zip'],
country: [:string, 'Country'],
phone: [:string, 'Phone'],
email: [:string, 'Email'],
fax: [:string, 'Fax'],
company: [:string, 'Company']
} #:nodoc:
ADDRESS_OPTIONS = [
COMMON_ADDRESS_OPTIONS,
{
address1: [:string, 'Street'],
address2: [:string, 'Street2'],
}
].inject(:merge) #:nodoc
CUSTOMER_UPDATE_DATA_FIELDS = [
CUSTOMER_PROFILE_OPTIONS,
CUSTOMER_RECURRING_BILLING_OPTIONS,
COMMON_ADDRESS_OPTIONS,
{
address1: [:string, 'Address'],
address2: [:string, 'Address2'],
},
{
card_number: [:string, 'CardNumber'],
card_exp: [:string, 'CardExp'],
account: [:string, 'Account'],
routing: [:string, 'Routing'],
check_format: [:string, 'CheckFormat'],
record_type: [:string, 'RecordType'],
}
].inject(:merge) #:nodoc
CUSTOMER_TRANSACTION_REQUEST_OPTIONS = {
command: [:string, 'Command'],
ignore_duplicate: [:boolean, 'IgnoreDuplicate'],
client_ip: [:string, 'ClientIP'],
customer_receipt: [:boolean, 'CustReceipt'],
customer_email: [:boolean, 'CustReceiptEmail'],
customer_template: [:boolean, 'CustReceiptName'],
merchant_receipt: [:boolean, 'MerchReceipt'],
merchant_email: [:boolean, 'MerchReceiptEmail'],
merchant_template: [:boolean, 'MerchReceiptName'],
recurring: [:boolean, 'isRecurring'],
verification_value: [:string, 'CardCode'],
software: [:string, 'Software']
} #:nodoc:
TRANSACTION_REQUEST_OBJECT_OPTIONS = {
command: [:string, 'Command'],
ignore_duplicate: [:boolean, 'IgnoreDuplicate'],
authorization_code: [:string, 'AuthCode'],
reference_number: [:string, 'RefNum'],
account_holder: [:string, 'AccountHolder'],
client_ip: [:string, 'ClientIP'],
customer_id: [:string, 'CustomerID'],
customer_receipt: [:boolean, 'CustReceipt'],
customer_template: [:boolean, 'CustReceiptName'],
software: [:string, 'Software']
} #:nodoc:
TRANSACTION_DETAIL_OPTIONS = {
invoice: [:string, 'Invoice'],
po_number: [:string, 'PONum'],
order_id: [:string, 'OrderID'],
clerk: [:string, 'Clerk'],
terminal: [:string, 'Terminal'],
table: [:string, 'Table'],
description: [:string, 'Description'],
comments: [:string, 'Comments'],
allow_partial_auth: [:boolean, 'AllowPartialAuth'],
currency: [:string, 'Currency'],
non_tax: [:boolean, 'NonTax'],
} #:nodoc:
TRANSACTION_DETAIL_MONEY_OPTIONS = {
amount: [:double, 'Amount'],
tax: [:double, 'Tax'],
tip: [:double, 'Tip'],
non_tax: [:boolean, 'NonTax'],
shipping: [:double, 'Shipping'],
discount: [:double, 'Discount'],
subtotal: [:double, 'Subtotal']
} #:nodoc:
CREDIT_CARD_DATA_OPTIONS = {
magnetic_stripe: [:string, 'MagStripe'],
dukpt: [:string, 'DUKPT'],
signature: [:string, 'Signature'],
terminal_type: [:string, 'TermType'],
magnetic_support: [:string, 'MagSupport'],
xid: [:string, 'XID'],
cavv: [:string, 'CAVV'],
eci: [:integer, 'ECI'],
internal_card_authorization: [:boolean, 'InternalCardAuth'],
pares: [:string, 'Pares']
} #:nodoc:
CHECK_DATA_OPTIONS = {
drivers_license: [:string, 'DriversLicense'],
drivers_license_state: [:string, 'DriversLicenseState'],
record_type: [:string, 'RecordType'],
aux_on_us: [:string, 'AuxOnUS'],
epc_code: [:string, 'EpcCode'],
front_image: [:string, 'FrontImage'],
back_image: [:string, 'BackImage']
} #:nodoc:
RECURRING_BILLING_OPTIONS = {
schedule: [:string, 'Schedule'],
number_left: [:integer, 'NumLeft'],
enabled: [:boolean, 'Enabled']
} #:nodoc:
AVS_RESULTS = {
'Y' => %w(YYY Y YYA YYD),
'Z' => %w(NYZ Z),
'A' => %w(YNA A YNY),
'N' => %w(NNN N NN),
'X' => %w(YYX X),
'W' => %w(NYW W),
'XXW' => %w(XXW),
'XXU' => %w(XXU),
'R' => %w(XXR R U E),
'S' => %w(XXS S),
'XXE' => %w(XXE),
'G' => %w(XXG G C I),
'B' => %w(YYG B M),
'D' => %w(GGG D),
'P' => %w(YGG P)
}.inject({}) do |map, (type, codes)|
codes.each { |code| map[code] = type }
map
end #:nodoc:
AVS_CUSTOM_MESSAGES = {
'XXW' => 'Card number not on file.',
'XXU' => 'Address information not verified for domestic transaction.',
'XXE' => 'Address verification not allowed for card type.'
} #:nodoc:
# Create a new gateway.
#
# ==== Required
# * At least the live_url OR the software_id must be present.
# * :software_id -- 8 character software id
# OR
# * :test_url -- full url for testing
# * :live_url -- full url for live/production
#
# ==== Optional
# * :soap_response -- set to +true+ to add :soap_response to the params hash containing the entire soap xml message
#
def initialize(options = {})
requires!(options, :login, :password)
if options[:software_id]
self.live_url = "#{LIVE_URL_BASE}#{options[:software_id]}"
self.test_url = "#{TEST_URL_BASE}#{options[:software_id]}"
else
self.live_url = options[:live_url].to_s
self.test_url = options[:test_url].to_s if options[:test_url]
end
super
end
# Standard Gateway Methods ======================================
# Make a purchase with a credit card. (Authorize and
# capture for settlement.)
#
# Note: See run_transaction for additional options.
#
def purchase(money, creditcard, options={})
run_sale(options.merge!(amount: money, payment_method: creditcard))
end
# Authorize an amount on a credit card or account.
#
# Note: See run_transaction for additional options.
#
def authorize(money, creditcard, options={})
run_auth_only(options.merge!(amount: money, payment_method: creditcard))
end
# Capture an authorized transaction.
#
# Note: See run_transaction for additional options.
#
def capture(money, identification, options={})
capture_transaction(options.merge!(amount: money, reference_number: identification))
end
# Void a previous transaction that has not been settled.
#
# Note: See run_transaction for additional options.
#
def void(identification, options={})
void_transaction(options.merge!(reference_number: identification))
end
# Refund a previous transaction.
#
# Note: See run_transaction for additional options.
#
def refund(money, identification, options={})
refund_transaction(options.merge!(amount: money, reference_number: identification))
end
def credit(money, identification, options={})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
end
# Customer ======================================================
# Add a customer.
#
# ==== Options
# * :id -- merchant assigned id
# * :notes -- notes about customer
# * :data -- base64 data about customer
# * :url -- customer website
# * :billing_address -- usual options
# * :payment_methods -- array of payment method hashes.
# * :method -- credit_card or check
# * :name -- optional name/label for the method
# * :sort -- optional integer value specifying the backup sort order, 0 is default
#
# ==== Recurring Options
# * :enabled -- +true+ enables recurring
# * :schedule -- daily, weekly, bi-weekly (every two weeks), monthly, bi-monthly (every two months), quarterly, bi-annually (every six months), annually, first of month, last day of month
# * :number_left -- number of payments left; -1 for unlimited
# * :next -- date of next payment (Date/Time)
# * :amount -- amount of recurring payment
# * :tax -- tax portion of amount
# * :currency -- numeric currency code
# * :description -- description of transaction
# * :order_id -- transaction order id
# * :user -- merchant username assigned to transaction
# * :source -- name of source key assigned to billing
# * :send_receipt -- +true+ to send client a receipt
# * :receipt_note -- leave a note on the receipt
#
# ==== Point of Sale Options
# * :price_tier -- name of customer price tier
# * :tax_class -- tax class
# * :lookup_code -- lookup code from customer/member id card; barcode or magnetic stripe; can be assigned by merchant; defaults to system assigned if blank
#
# ==== Response
# * #message -- customer number assigned by gateway
#
def add_customer(options={})
request = build_request(__method__, options)
commit(__method__, request)
end
# Update a customer by replacing all of the customer details.
#
# ==== Required
# * :customer_number -- customer to update
#
# ==== Options
# * Same as add_customer
#
def update_customer(options={})
requires! options, :customer_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Update a customer by replacing only the provided fields.
#
# ==== Required
# * :customer_number -- customer to update
# * :update_data -- FieldValue array of fields to retrieve
# * :first_name
# * :last_name
# * :id
# * :company
# * :address
# * :address2
# * :city
# * :state
# * :zip
# * :country
# * :phone
# * :fax
# * :email
# * :url
# * :receipt_note
# * :send_receipt
# * :notes
# * :description
# * :order_id
# * :enabled
# * :schedule
# * :next
# * :num_left
# * :amount
# * :custom_data
# * :source
# * :user
# * :card_number
# * :card_exp
# * :account
# * :routing
# * :check_format or :record_type
#
# ==== Response
# * #message -- boolean; Returns true if successful. Exception thrown all failures.
#
def quick_update_customer(options={})
requires! options, :customer_number
requires! options, :update_data
request = build_request(__method__, options)
commit(__method__, request)
end
# Enable a customer for recurring billing.
#
# Note: Customer does not need to have all recurring parameters to succeed.
#
# ==== Required
# * :customer_number
#
def enable_customer(options={})
requires! options, :customer_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Disable a customer for recurring billing.
#
# ==== Required
# * :customer_number
#
def disable_customer(options={})
requires! options, :customer_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Add a payment method to a customer.
#
# ==== Required
# * :customer_number -- number returned by add_customer response.message
# * :payment_method
# * :method -- credit_card or check
# * :name -- optional name/label for the method
# * :sort -- an integer value specifying the backup sort order, 0 is default
#
# ==== Optional
# * :make_default -- set +true+ to make default
# * :verify -- set +true+ to run auth_only verification; throws fault if cannot verify
#
# ==== Response
# * #message -- method_id of new customer payment method
#
def add_customer_payment_method(options={})
requires! options, :customer_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Retrieve all of the payment methods belonging to a customer
#
# ==== Required
# * :customer_number
#
# ==== Response
# * #message -- either a single hash or an array of hashes of payment methods
#
def get_customer_payment_methods(options={})
requires! options, :customer_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Retrieve one of the payment methods belonging to a customer
#
# ==== Required
# * :customer_number
# * :method_id
#
# ==== Response
# * #message -- hash of payment method
#
def get_customer_payment_method(options={})
requires! options, :customer_number, :method_id
request = build_request(__method__, options)
commit(__method__, request)
end
# Update a customer payment method.
#
# ==== Required
# * :method_id -- method_id to update
#
# ==== Options
# * :method -- credit_card or check
# * :name -- optional name/label for the method
# * :sort -- an integer value specifying the backup sort order, 0 is default
# * :verify -- set +true+ to run auth_only verification; throws fault if cannot verify
#
# ==== Response
# * #message -- hash of payment method
#
def update_customer_payment_method(options={})
requires! options, :method_id
request = build_request(__method__, options)
commit(__method__, request)
end
# Delete one the payment methods belonging to a customer
#
# ==== Required
# * :customer_number
# * :method_id
#
def delete_customer_payment_method(options={})
requires! options, :customer_number, :method_id
request = build_request(__method__, options)
commit(__method__, request)
end
# Delete a customer.
#
# ==== Required
# * :customer_number
#
def delete_customer(options={})
requires! options, :customer_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Run a transaction for an existing customer in the database.
#
# ==== Required Options
# * :customer_number -- gateway assigned identifier
# * :command -- Sale, AuthOnly, Credit, Check, CheckCredit
# * :amount -- total amount
#
# ==== Options
# * :method_id -- which payment method to use, 0/nil/omitted for default method
# * :ignore_duplicate -- +true+ overrides duplicate transaction
# * :client_ip -- client ip address
# * :customer_receipt -- +true+, sends receipt to customer. active_merchant defaults to +false+
# * :customer_email -- specify if different than customer record
# * :customer_template -- name of template
# * :merchant_receipt -- +true+, sends receipt to merchant. active_merchant defaults to +false+
# * :merchant_email -- required if :merchant_receipt set to +true+
# * :merchant_template -- name of template
# * :recurring -- defaults to +false+ *see documentation*
# * :verification_value -- pci forbids storage of this value, only required for CVV2 validation
# * :software -- active_merchant sets to required gateway option value
# * :line_items -- XXX not implemented yet
# * :custom_fields -- XXX not implemented yet
#
# ==== Transaction Options
# * :invoice -- transaction invoice number; truncated to 10 characters; defaults to reference_number
# * :po_number -- commercial purchase order number; upto 25 characters
# * :order_id -- should be used to assign a unique id; upto 64 characters
# * :clerk -- sales clerk
# * :terminal -- terminal name
# * :table -- table name/number
# * :description -- description
# * :comments -- comments
# * :allow_partial_auth -- allow partial authorization if full amount is not available; defaults +false+
# * :currency -- numeric currency code
# * :tax -- tax portion of amount
# * :tip -- tip portion of amount
# * :non_tax -- +true+ if transaction is non-taxable
# * :shipping -- shipping portion of amount
# * :discount -- amount of discount
# * :subtotal -- amount of transaction before tax, tip, shipping, and discount are applied
#
# ==== Response
# * #message -- transaction response hash
#
def run_customer_transaction(options={})
requires! options, :customer_number, :command, :amount
request = build_request(__method__, options)
commit(__method__, request)
end
# Transactions ==================================================
# Run a transaction.
#
# Note: run_sale, run_auth_only, run_credit, run_check_sale, run_check_credit
# methods are also available. Each takes the same options as
# run_transaction, but the :command option is not required.
#
# Recurring Note: If recurring options are included USA ePay will create a
# new customer record with the supplied information. The customer number
# will be returned in the response.
#
# ==== Options
# * :payment_method -- credit_card or check
# * :command -- sale, credit, void, creditvoid, authonly, capture, postauth, check, checkcredit; defaults to sale; only required for run_transaction when other than sale
# * :reference_number -- for the original transaction; obtained by sale or authonly
# * :authorization_code -- required for postauth; obtained offline
# * :ignore_duplicate -- set +true+ if you want to override the duplicate transaction handling
# * :account_holder -- name of account holder
# * :customer_id -- merchant assigned id
# * :customer_receipt -- set +true+ to email receipt to billing email address
# * :customer_template -- name of template
# * :software -- stamp merchant software version for tracking
# * :billing_address -- see UsaEpayCimGateway documentation for all address fields
# * :shipping_address -- see UsaEpayCimGateway documentation for all address fields
# * :recurring -- used for recurring billing transactions
# * :schedule -- disabled, daily, weekly, bi-weekly (every two weeks), monthly, bi-monthly (every two months), quarterly, bi-annually (every six months), annually
# * :next -- date customer billed next (Date/Time)
# * :expire -- date the recurring transactions end (Date/Time)
# * :number_left -- transactions remaining in billing cycle
# * :amount -- amount to be billed each recurring transaction
# * :enabled -- states if currently active
# * :line_items -- XXX not implemented yet
# * :custom_fields -- XXX not implemented yet
#
# ==== Transaction Options
# * :amount -- total amount
# * :invoice -- transaction invoice number; truncated to 10 characters; defaults to reference_number
# * :po_number -- commercial purchase order number; upto 25 characters
# * :order_id -- should be used to assign a unique id; upto 64 characters
# * :clerk -- sales clerk
# * :terminal -- terminal name
# * :table -- table name/number
# * :description -- description
# * :comments -- comments
# * :allow_partial_auth -- allow partial authorization if full amount is not available; defaults +false+
# * :currency -- numeric currency code
# * :tax -- tax portion of amount
# * :tip -- tip portion of amount
# * :non_tax -- +true+ if transaction is non-taxable
# * :shipping -- shipping portion of amount
# * :discount -- amount of discount
# * :subtotal -- amount of transaction before tax, tip, shipping, and discount are applied
#
# ==== Response
# * #message -- transaction response hash
#
def run_transaction(options={})
request = build_request(__method__, options)
commit(__method__, request)
end
TRANSACTION_METHODS = %i[
run_sale run_auth_only run_credit
run_check_sale run_check_credit
] #:nodoc:
TRANSACTION_METHODS.each do |method|
define_method method do |options|
request = build_request(method, options)
commit(method, request)
end
end
# Post an authorization code obtained offline.
#
# ==== Required
# * :authorization_code -- obtained offline
#
# ==== Options
# * Same as run_transaction
#
# ==== Response
# * #message -- transaction response hash
#
def post_auth(options={})
requires! options, :authorization_code
request = build_request(__method__, options)
commit(__method__, request)
end
# Capture an authorized transaction and move it into the current batch
# for settlement.
#
# Note: Check with merchant bank for details/restrictions on differing
# amounts than the original authorization.
#
# ==== Required
# * :reference_number
#
# ==== Options
# * :amount -- may be different than original amount; 0 will void authorization
#
# ==== Response
# * #message -- transaction response hash
#
def capture_transaction(options={})
requires! options, :reference_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Void a transaction.
#
# Note: Can only be voided before being settled.
#
# ==== Required
# * :reference_number
#
# ==== Response
# * #message -- transaction response hash
#
def void_transaction(options={})
requires! options, :reference_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Refund transaction.
#
# Note: Required after a transaction has been settled. Refunds
# both credit card and check transactions.
#
# ==== Required
# * :reference_number
# * :amount -- amount to refund; 0 will refund original amount
#
# ==== Response
# * #message -- transaction response hash
#
def refund_transaction(options={})
requires! options, :reference_number, :amount
request = build_request(__method__, options)
commit(__method__, request)
end
# Override transaction flagged for manager approval.
#
# Note: Checks only!
#
# ==== Required
# * :reference_number
#
# ==== Options
# * :reason
#
# ==== Response
# * #message -- transaction response hash
#
def override_transaction(options={})
requires! options, :reference_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Quick Transactions ============================================
# Run a sale transaction based off of a past transaction.
#
# Transfers referenced transaction's payment method to this
# transaction. As of 6/2011, USA ePay blocks credit card numbers
# at 3 years.
#
# ==== Required
# * :reference_number -- transaction to reference payment from
# * :amount -- total amount
#
# ==== Options
# * :authorize_only -- set +true+ if you just want to authorize
#
# ==== Transaction Options
# * :invoice -- transaction invoice number; truncated to 10 characters; defaults to reference_number
# * :po_number -- commercial purchase order number; upto 25 characters
# * :order_id -- should be used to assign a unique id; upto 64 characters
# * :clerk -- sales clerk
# * :terminal -- terminal name
# * :table -- table name/number
# * :description -- description
# * :comments -- comments
# * :allow_partial_auth -- allow partial authorization if full amount is not available; defaults +false+
# * :currency -- numeric currency code
# * :tax -- tax portion of amount
# * :tip -- tip portion of amount
# * :non_tax -- +true+ if transaction is non-taxable
# * :shipping -- shipping portion of amount
# * :discount -- amount of discount
# * :subtotal -- amount of transaction before tax, tip, shipping, and discount are applied
#
# ==== Response
# * #message -- transaction response hash
#
def run_quick_sale(options={})
requires! options, :reference_number, :amount
request = build_request(__method__, options)
commit(__method__, request)
end
# Run a credit based off of a past transaction.
#
# Transfers referenced transaction's payment method to this
# transaction. As of 6/2011, USA ePay blocks credit card numbers
# at 3 years.
#
# ==== Required
# * :reference_number -- transaction to reference payment from
#
# ==== Transaction Options
# * :amount -- total amount
# * :invoice -- transaction invoice number; truncated to 10 characters; defaults to reference_number
# * :po_number -- commercial purchase order number; upto 25 characters
# * :order_id -- should be used to assign a unique id; upto 64 characters
# * :clerk -- sales clerk
# * :terminal -- terminal name
# * :table -- table name/number
# * :description -- description
# * :comments -- comments
# * :allow_partial_auth -- allow partial authorization if full amount is not available; defaults +false+
# * :currency -- numeric currency code
# * :tax -- tax portion of amount
# * :tip -- tip portion of amount
# * :non_tax -- +true+ if transaction is non-taxable
# * :shipping -- shipping portion of amount
# * :discount -- amount of discount
# * :subtotal -- amount of transaction before tax, tip, shipping, and discount are applied
#
# ==== Response
# * #message -- transaction response hash
#
def run_quick_credit(options={})
requires! options, :reference_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Transaction Status ============================================
# Retrieve details of a specified transaction.
#
# ==== Required
# * :reference_number
#
# ==== Response
# * #message -- transaction hash
#
def get_transaction(options={})
requires! options, :reference_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Check status of a transaction.
#
# ==== Required
# * :reference_number
#
# ==== Response
# * response.success -- success of the referenced transaction
# * response.message -- message of the referenced transaction
# * response.authorization -- same as :reference_number in options
#
def get_transaction_status(options={})
requires! options, :reference_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Check status of a transaction (custom).
#
# ==== Required
# * :reference_number
# * :fields -- string array of fields to retrieve
# * Response.AuthCode
# * Response.AvsResult
# * Response.AvsResultCode
# * Response.BatchNum
# * Response.CardCodeResult
# * Response.CardCodeResultCode
# * Response.ConversionRate
# * Response.ConvertedAmount
# * Response.ConvertedAmountCurrency
# * Response.Error
# * Response.ErrorCode
# * Response.RefNum
# * Response.Result
# * Response.ResultCode
# * Response.Status
# * Response.StatusCode
# * CheckTrace.TrackingNum
# * CheckTrace.Effective
# * CheckTrace.Processed
# * CheckTrace.Settled
# * CheckTrace.Returned
# * CheckTrace.BankNote
# * DateTime
# * AccountHolder
# * Details.Invoice
# * Details.PoNum
# * Details.OrderID
# * Details.Clerk
# * Details.Terminal
# * Details.Table
# * Details.Description
# * Details.Amount
# * Details.Currency
# * Details.Tax
# * Details.Tip
# * Details.NonTax
# * Details.Shipping
# * Details.Discount
# * Details.Subtotal
# * CreditCardData.CardType
# * CreditCardData.CardNumber
# * CreditCardData.CardExpiration
# * CreditCardData.CardCode
# * CreditCardData.AvsStreet
# * CreditCardData.AvsZip
# * CreditCardData.CardPresent
# * CheckData.CheckNumber
# * CheckData.Routing
# * CheckData.Account
# * CheckData.SSN
# * CheckData.DriversLicense
# * CheckData.DriversLicenseState
# * CheckData.RecordType
# * User
# * Source
# * ServerIP
# * ClientIP
# * CustomerID
# * BillingAddress.FirstName
# * BillingAddress.LastName
# * BillingAddress.Company
# * BillingAddress.Street
# * BillingAddress.Street2
# * BillingAddress.City
# * BillingAddress.State
# * BillingAddress.Zip
# * BillingAddress.Country
# * BillingAddress.Phone
# * BillingAddress.Fax
# * BillingAddress.Email
# * ShippingAddress.FirstName
# * ShippingAddress.LastName
# * ShippingAddress.Company
# * ShippingAddress.Street
# * ShippingAddress.Street2
# * ShippingAddress.City
# * ShippingAddress.State
# * ShippingAddress.Zip
# * ShippingAddress.Country
# * ShippingAddress.Phone
# * ShippingAddress.Fax
# * ShippingAddress.Email
#
# ==== Response
# * #message -- hash; keys are the field values
#
def get_transaction_custom(options={})
requires! options, :reference_number, :fields
request = build_request(__method__, options)
commit(__method__, request)
end
# Check status of a check transaction.
#
# ==== Required
# * :reference_number
#
# ==== Response
# * #message -- check trace hash
#
def get_check_trace(options={})
requires! options, :reference_number
request = build_request(__method__, options)
commit(__method__, request)
end
# Account =======================================================
# Retrieve merchant account details
#
# ==== Response
# * #message -- account hash
#
def get_account_details
request = build_request(__method__)
commit(__method__, request)
end
# Builders ======================================================
private
# Build soap header, etc.
def build_request(action, options = {})
soap = Builder::XmlMarkup.new
soap.instruct!(:xml, version: '1.0', encoding: 'utf-8')
soap.tag! 'SOAP-ENV:Envelope',
'xmlns:SOAP-ENV' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:ns1' => 'urn:usaepay',
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:SOAP-ENC' => 'http://schemas.xmlsoap.org/soap/encoding/',
'SOAP-ENV:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' do
soap.tag! 'SOAP-ENV:Body' do
send("build_#{action}", soap, options)
end
end
soap.target!
end
# Build generic tag.
def build_tag(soap, type, tag, value)
soap.tag!(tag, value, 'xsi:type' => "xsd:#{type}") if value != nil
end
# Build token.
def build_token(soap, options)
seed = SecureRandom.base64(32)
hash = Digest::SHA1.hexdigest("#{@options[:login]}#{seed}#{@options[:password].to_s.strip}")
soap.Token 'xsi:type' => 'ns1:ueSecurityToken' do
build_tag soap, :string, 'ClientIP', options[:client_ip]
soap.PinHash 'xsi:type' => 'ns1:ueHash' do
build_tag soap, :string, 'HashValue', hash
build_tag soap, :string, 'Seed', seed
build_tag soap, :string, 'Type', 'sha1'
end
build_tag soap, :string, 'SourceKey', @options[:login]
end
end
# Customer ======================================================
def build_add_customer(soap, options)
soap.tag! 'ns1:addCustomer' do
build_token soap, options
build_customer_data soap, options
build_tag soap, :double, 'Amount', amount(options[:amount])
build_tag soap, :double, 'Tax', amount(options[:tax])
build_tag soap, :string, 'Next', options[:next].strftime('%Y-%m-%d') if options[:next]
end
end
def build_customer(soap, options, type, add_customer_data=false)
soap.tag! "ns1:#{type}" do
build_token soap, options
build_tag soap, :integer, 'CustNum', options[:customer_number]
build_customer_data soap, options if add_customer_data
end
end
def build_update_customer(soap, options)
build_customer(soap, options, 'updateCustomer', true)
end
def build_enable_customer(soap, options)
build_customer(soap, options, 'enableCustomer')
end
def build_disable_customer(soap, options)
build_customer(soap, options, 'disableCustomer')
end
def build_delete_customer(soap, options)
build_customer(soap, options, 'deleteCustomer')
end
def build_quick_update_customer(soap, options)
soap.tag! 'ns1:quickUpdateCustomer' do
build_token soap, options
build_tag soap, :integer, 'CustNum', options[:customer_number]
build_field_value_array soap, 'UpdateData', 'FieldValue', options[:update_data], CUSTOMER_UPDATE_DATA_FIELDS
end
end
def build_add_customer_payment_method(soap, options)
soap.tag! 'ns1:addCustomerPaymentMethod' do
build_token soap, options
build_tag soap, :integer, 'CustNum', options[:customer_number]
build_customer_payment_methods soap, options
build_tag soap, :boolean, 'MakeDefault', options[:make_default]
build_tag soap, :boolean, 'Verify', options[:verify]
end
end
def build_get_customer_payment_method(soap, options)
soap.tag! 'ns1:getCustomerPaymentMethod' do
build_token soap, options
build_tag soap, :integer, 'CustNum', options[:customer_number]
build_tag soap, :integer, 'MethodID', options[:method_id]
end
end
def build_get_customer_payment_methods(soap, options)
build_customer(soap, options, 'getCustomerPaymentMethods')
end
def build_update_customer_payment_method(soap, options)
soap.tag! 'ns1:updateCustomerPaymentMethod' do
build_token soap, options
build_customer_payment_methods soap, options
build_tag soap, :boolean, 'Verify', options[:verify]
end
end
def build_delete_customer_payment_method(soap, options)
soap.tag! 'ns1:deleteCustomerPaymentMethod' do
build_token soap, options
build_tag soap, :integer, 'Custnum', options[:customer_number]
build_tag soap, :integer, 'PaymentMethodID', options[:method_id]
end
end
def build_run_customer_transaction(soap, options)
soap.tag! 'ns1:runCustomerTransaction' do
build_token soap, options
build_tag soap, :integer, 'CustNum', options[:customer_number]
build_tag soap, :integer, 'PaymentMethodID', options[:method_id] || 0
build_customer_transaction soap, options
end
end
# Transactions ==================================================
def build_run_transaction(soap, options)
soap.tag! 'ns1:runTransaction' do
build_token soap, options
build_transaction_request_object soap, options, 'Parameters'
end
end
def build_run_sale(soap, options)
soap.tag! 'ns1:runSale' do
build_token soap, options
build_transaction_request_object soap, options
end
end
def build_run_auth_only(soap, options)
soap.tag! 'ns1:runAuthOnly' do
build_token soap, options
build_transaction_request_object soap, options
end
end
def build_run_credit(soap, options)
soap.tag! 'ns1:runCredit' do
build_token soap, options
build_transaction_request_object soap, options
end
end
def build_run_check_sale(soap, options)
soap.tag! 'ns1:runCheckSale' do
build_token soap, options
build_transaction_request_object soap, options
end
end
def build_run_check_credit(soap, options)
soap.tag! 'ns1:runCheckCredit' do
build_token soap, options
build_transaction_request_object soap, options
end
end
def build_post_auth(soap, options)
soap.tag! 'ns1:postAuth' do
build_token soap, options
build_transaction_request_object soap, options
end
end
def build_run_quick_sale(soap, options)
soap.tag! 'ns1:runQuickSale' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
build_transaction_detail soap, options
build_tag soap, :boolean, 'AuthOnly', options[:authorize_only] || false
end
end
def build_run_quick_credit(soap, options)
soap.tag! 'ns1:runQuickCredit' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
build_transaction_detail soap, options
end
end
def build_get_transaction(soap, options)
soap.tag! 'ns1:getTransaction' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
end
end
def build_get_transaction_status(soap, options)
soap.tag! 'ns1:getTransactionStatus' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
end
end
def build_get_transaction_custom(soap, options)
soap.tag! 'ns1:getTransactionCustom' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
build_transaction_field_array soap, options
end
end
def build_get_check_trace(soap, options)
soap.tag! 'ns1:getCheckTrace' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
end
end
def build_capture_transaction(soap, options)
soap.tag! 'ns1:captureTransaction' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
build_tag soap, :double, 'Amount', amount(options[:amount])
end
end
def build_void_transaction(soap, options)
soap.tag! 'ns1:voidTransaction' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
end
end
def build_refund_transaction(soap, options)
soap.tag! 'ns1:refundTransaction' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
build_tag soap, :integer, 'Amount', amount(options[:amount])
end
end
def build_override_transaction(soap, options)
soap.tag! 'ns1:overrideTransaction' do
build_token soap, options
build_tag soap, :integer, 'RefNum', options[:reference_number]
build_tag soap, :string, 'Reason', options[:reason]
end
end
# Account =======================================================
def build_get_account_details(soap, options)
soap.tag! 'ns1:getAccountDetails' do
build_token soap, options
end
end
# Customer Helpers ==============================================
def build_customer_data(soap, options)
soap.CustomerData 'xsi:type' => 'ns1:CustomerObject' do
CUSTOMER_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[k]
end
build_billing_address soap, options
build_customer_payments soap, options
build_custom_fields soap, options
end
end
def build_customer_payments(soap, options)
if options[:payment_methods]
length = options[:payment_methods].length
soap.PaymentMethods 'SOAP-ENC:arrayType' => "ns1:PaymentMethod[#{length}]",
'xsi:type' => 'ns1:PaymentMethodArray' do
build_customer_payment_methods soap, options
end
end
end
def extract_methods_and_tag(options)
case
when options[:payment_method] && !options[:payment_methods]
payment_methods = [options[:payment_method]]
tag_name = 'PaymentMethod'
when options[:payment_methods] && !options[:payment_method]
payment_methods = options[:payment_methods]
tag_name = 'item'
else
payment_methods = [options]
tag_name = 'PaymentMethod'
end
[payment_methods, tag_name]
end
def build_credit_card_or_check(soap, payment_method)
case
when payment_method[:method].kind_of?(ActiveMerchant::Billing::CreditCard)
build_tag soap, :string, 'CardNumber', payment_method[:method].number
build_tag soap, :string, 'CardExpiration',
"#{'%02d' % payment_method[:method].month}#{payment_method[:method].year.to_s[-2..-1]}"
if options[:billing_address]
build_tag soap, :string, 'AvsStreet', options[:billing_address][:address1]
build_tag soap, :string, 'AvsZip', options[:billing_address][:zip]
end
build_tag soap, :string, 'CardCode', payment_method[:method].verification_value
when payment_method[:method].kind_of?(ActiveMerchant::Billing::Check)
build_tag soap, :string, 'Account', payment_method[:method].account_number
build_tag soap, :string, 'Routing', payment_method[:method].routing_number
build_tag soap, :string, 'AccountType', payment_method[:method].account_type.capitalize unless payment_method[:method].account_type.nil?
build_tag soap, :string, 'DriversLicense', options[:drivers_license]
build_tag soap, :string, 'DriversLicenseState', options[:drivers_license_state]
build_tag soap, :string, 'RecordType', options[:record_type]
end
end
def build_customer_payment_methods(soap, options)
payment_methods, tag_name = extract_methods_and_tag(options)
payment_methods.each do |payment_method|
soap.tag! tag_name, 'xsi:type' => 'ns1:PaymentMethod' do
build_tag soap, :integer, 'MethodID', payment_method[:method_id]
build_tag soap, :string, 'MethodType', payment_method[:type]
build_tag soap, :string, 'MethodName', payment_method[:name]
build_tag soap, :integer, 'SecondarySort', payment_method[:sort]
build_credit_card_or_check(soap, payment_method)
end
end
end
def build_customer_transaction(soap, options)
soap.Parameters 'xsi:type' => 'ns1:CustomerTransactionRequest' do
build_transaction_detail soap, options
CUSTOMER_TRANSACTION_REQUEST_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[k]
end
build_custom_fields soap, options
build_line_items soap, options
end
end
# Transaction Helpers ===========================================
def build_transaction_request_object(soap, options, name='Params')
soap.tag! name, 'xsi:type' => 'ns1:TransactionRequestObject' do
TRANSACTION_REQUEST_OBJECT_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[k]
end
case
when options[:payment_method].nil?
nil
when options[:payment_method].kind_of?(ActiveMerchant::Billing::CreditCard)
build_credit_card_data soap, options
when options[:payment_method].kind_of?(ActiveMerchant::Billing::Check)
build_check_data soap, options
else
raise ArgumentError, 'options[:payment_method] must be a CreditCard or Check'
end
build_transaction_detail soap, options
build_billing_address soap, options
build_shipping_address soap, options
build_recurring_billing soap, options
build_line_items soap, options
build_custom_fields soap, options
end
end
def build_transaction_detail(soap, options)
soap.Details 'xsi:type' => 'ns1:TransactionDetail' do
TRANSACTION_DETAIL_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[k]
end
TRANSACTION_DETAIL_MONEY_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], amount(options[k])
end
end
end
def build_credit_card_data(soap, options)
soap.CreditCardData 'xsi:type' => 'ns1:CreditCardData' do
build_tag soap, :string, 'CardNumber', options[:payment_method].number
build_tag soap, :string, 'CardExpiration', build_card_expiration(options)
if options[:billing_address]
build_tag soap, :string, 'AvsStreet', options[:billing_address][:address1]
build_tag soap, :string, 'AvsZip', options[:billing_address][:zip]
end
build_tag soap, :string, 'CardCode', options[:payment_method].verification_value
build_tag soap, :boolean, 'CardPresent', options[:card_present] || false
CREDIT_CARD_DATA_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[k]
end
end
end
def build_card_expiration(options)
month = options[:payment_method].month
year = options[:payment_method].year
"#{'%02d' % month}#{year.to_s[-2..-1]}" unless month.nil? || year.nil?
end
def build_check_data(soap, options)
soap.CheckData 'xsi:type' => 'ns1:CheckData' do
build_tag soap, :integer, 'CheckNumber', options[:payment_method].number
build_tag soap, :string, 'Account', options[:payment_method].account_number
build_tag soap, :string, 'Routing', options[:payment_method].routing_number
build_tag soap, :string, 'AccountType', options[:payment_method].account_type.capitalize
CHECK_DATA_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[k]
end
end
end
def build_recurring_billing(soap, options)
if options[:recurring]
soap.RecurringBilling 'xsi:type' => 'ns1:RecurringBilling' do
build_tag soap, :double, 'Amount', amount(options[:recurring][:amount])
build_tag soap, :string, 'Next', options[:recurring][:next].strftime('%Y-%m-%d') if options[:recurring][:next]
build_tag soap, :string, 'Expire', options[:recurring][:expire].strftime('%Y-%m-%d') if options[:recurring][:expire]
RECURRING_BILLING_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[:recurring][k]
end
end
end
end
def build_transaction_field_array(soap, options)
soap.Fields 'SOAP-ENC:arryType' => "xsd:string[#{options[:fields].length}]", 'xsi:type' => 'ns1:stringArray' do
options[:fields].each do |field|
build_tag soap, :string, 'item', field
end
end
end
# General Helpers ===============================================
def build_billing_address(soap, options)
if options[:billing_address]
options[:billing_address][:first_name], options[:billing_address][:last_name] = split_names(options[:billing_address][:name]) if options[:billing_address][:name]
soap.BillingAddress 'xsi:type' => 'ns1:Address' do
ADDRESS_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[:billing_address][k]
end
end
end
end
def build_shipping_address(soap, options)
if options[:shipping_address]
options[:shipping_address][:first_name], options[:shipping_address][:last_name] = split_names(options[:shipping_address][:name]) if options[:shipping_address][:name]
soap.ShippingAddress 'xsi:type' => 'ns1:Address' do
ADDRESS_OPTIONS.each do |k, v|
build_tag soap, v[0], v[1], options[:shipping_address][k]
end
end
end
end
def build_field_value_array(soap, tag_name, type, custom_data, fields)
soap.tag! tag_name, 'SOAP-ENC:arryType' => "xsd:#{type}[#{options.length}]", 'xsi:type' => "ns1:#{type}Array" do
custom_data.each do |k, v|
build_field_value soap, fields[k][1], v, fields[k][0] if fields.key?(k)
end
end
end
def build_field_value(soap, field, value, value_type)
soap.FieldValue 'xsi:type' => 'ns1:FieldValue' do
build_tag soap, :string, 'Field', field
build_tag soap, value_type, 'Value', value
end
end
def build_line_items(soap, options) # TODO
end
def build_custom_fields(soap, options) # TODO
end
# Request =======================================================
def commit(action, request)
url = test? ? test_url : live_url
begin
soap = ssl_post(url, request, 'Content-Type' => 'text/xml')
rescue ActiveMerchant::ResponseError => error
soap = error.response.body
end
build_response(action, soap)
end
def build_response(action, soap)
response_params, success, message, authorization, avs, cvv = parse(action, soap)
response_params['soap_response'] = soap if @options[:soap_response]
Response.new(
success,
message,
response_params,
test: test?,
authorization: authorization,
avs_result: avs_from(avs),
cvv_result: cvv
)
end
def avs_from(avs)
avs_params = { code: avs }
avs_params[:message] = AVS_CUSTOM_MESSAGES[avs] if AVS_CUSTOM_MESSAGES.key?(avs)
avs_params
end
def parse(action, soap)
xml = REXML::Document.new(soap)
root = REXML::XPath.first(xml, '//SOAP-ENV:Body')
response = root ? parse_element(root[0]) : { response: soap }
success, message, authorization, avs, cvv = false, FAILURE_MESSAGE, nil, nil, nil
fault = !response || (response.length < 1) || response.has_key?('faultcode')
return [response, success, response['faultstring'], authorization, avs, cvv] if fault
if response.respond_to?(:[]) && p = response["#{action}_return"]
if p.respond_to?(:key?) && p.key?('result_code')
success = p['result_code'] == 'A'
authorization = p['ref_num']
avs = AVS_RESULTS[p['avs_result_code']]
cvv = p['card_code_result_code']
else
success = true
end
message =
case action
when :get_customer_payment_methods
p['item']
when :get_transaction_custom
items = p['item'].kind_of?(Array) ? p['item'] : [p['item']]
items.inject({}) { |hash, item| hash[item['field']] = item['value']; hash }
else
p
end
elsif response.respond_to?(:[]) && p = response[:response]
message = p # when response is html
end
[response, success, message, authorization, avs, cvv]
end
def parse_element(node)
if node.has_elements?
response = {}
node.elements.each do |e|
key = e.name.underscore
value = parse_element(e)
if response.has_key?(key)
if response[key].is_a?(Array)
response[key].push(value)
else
response[key] = [response[key], value]
end
else
response[key] = parse_element(e)
end
end
else
response = node.text
end
response
end
end
end
end