lib/active_merchant/billing/gateways/orbital.rb
require 'active_merchant/billing/gateways/orbital/orbital_soft_descriptors'
require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# For more information on Orbital, visit the {integration center}[http://download.chasepaymentech.com]
#
# ==== Authentication Options
#
# The Orbital Gateway supports two methods of authenticating incoming requests:
# Source IP authentication and Connection Username/Password authentication
#
# In addition, these IP addresses/Connection Usernames must be affiliated with the Merchant IDs
# for which the client should be submitting transactions.
#
# This does allow Third Party Hosting service organizations presenting on behalf of other
# merchants to submit transactions. However, each time a new customer is added, the
# merchant or Third-Party hosting organization needs to ensure that the new Merchant IDs
# or Chain IDs are affiliated with the hosting companies IPs or Connection Usernames.
#
# If the merchant expects to have more than one merchant account with the Orbital
# Gateway, it should have its IP addresses/Connection Usernames affiliated at the Chain
# level hierarchy within the Orbital Gateway. Each time a new merchant ID is added, as
# long as it is placed within the same Chain, it will simply work. Otherwise, the additional
# MIDs will need to be affiliated with the merchant IPs or Connection Usernames respectively.
# For example, we generally affiliate all Salem accounts [BIN 000001] with
# their Company Number [formerly called MA #] number so all MIDs or Divisions under that
# Company will automatically be affiliated.
class OrbitalGateway < Gateway
include Empty
API_VERSION = '9.0'
POST_HEADERS = {
'MIME-Version' => '1.1',
'Content-Type' => "application/PTI#{API_VERSION.delete('.')}",
'Content-transfer-encoding' => 'text',
'Request-number' => '1',
'Document-type' => 'Request',
'Interface-Version' => 'Ruby|ActiveMerchant|Proprietary Gateway'
}
SUCCESS = '0'
APPROVAL_SUCCESS = '1'
APPROVED = [
'00', # Approved
'08', # Approved authorization, honor with ID
'11', # Approved authorization, VIP approval
'24', # Validated
'26', # Pre-noted
'27', # No reason to decline
'28', # Received and stored
'29', # Provided authorization
'31', # Request received
'32', # BIN alert
'34', # Approved for partial
'91', # Approved low fraud
'92', # Approved medium fraud
'93', # Approved high fraud
'94', # Approved fraud service unavailable
'E7', # Stored
'PA', # Partial approval
'P1' # ECP - AVS - Account Status Verification and/or AOA data is in a positive status.
]
class_attribute :secondary_test_url, :secondary_live_url
self.test_url = 'https://orbitalvar1.chasepaymentech.com/authorize'
self.secondary_test_url = 'https://orbitalvar2.chasepaymentech.com/authorize'
self.live_url = 'https://orbital1.chasepaymentech.com/authorize'
self.secondary_live_url = 'https://orbital2.chasepaymentech.com/authorize'
self.supported_countries = %w[US CA]
self.default_currency = 'CAD'
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
self.display_name = 'Orbital Paymentech'
self.homepage_url = 'http://chasepaymentech.com/'
self.money_format = :cents
AVS_SUPPORTED_COUNTRIES = %w[US CA UK GB]
CURRENCY_CODES = {
'AUD' => '036',
'BRL' => '986',
'CAD' => '124',
'CLP' => '152',
'CZK' => '203',
'DKK' => '208',
'HKD' => '344',
'ICK' => '352',
'JPY' => '392',
'MXN' => '484',
'NZD' => '554',
'NOK' => '578',
'SGD' => '702',
'ZAR' => '710',
'SEK' => '752',
'CHF' => '756',
'GBP' => '826',
'USD' => '840',
'EUR' => '978'
}
CURRENCY_EXPONENTS = {
'AUD' => '2',
'BRL' => '2',
'CAD' => '2',
'CLP' => '2',
'CZK' => '2',
'DKK' => '2',
'HKD' => '2',
'ICK' => '2',
'JPY' => '0',
'MXN' => '2',
'NZD' => '2',
'NOK' => '2',
'SGD' => '2',
'ZAR' => '2',
'SEK' => '2',
'CHF' => '2',
'GBP' => '2',
'USD' => '2',
'EUR' => '2'
}
# INDUSTRY TYPES
ECOMMERCE_TRANSACTION = 'EC'
RECURRING_PAYMENT_TRANSACTION = 'RC'
MAIL_ORDER_TELEPHONE_ORDER_TRANSACTION = 'MO'
INTERACTIVE_VOICE_RESPONSE = 'IV'
# INTERACTIVE_VOICE_RESPONSE = 'IN'
# Auth Only No Capture
AUTH_ONLY = 'A'
# AC - Auth and Capture = 'AC'
AUTH_AND_CAPTURE = 'AC'
# F - Force Auth No Capture and no online authorization = 'F'
FORCE_AUTH_ONLY = 'F'
# FR - Force Auth No Capture and no online authorization = 'FR'
# FC - Force Auth and Capture no online authorization = 'FC'
FORCE_AUTH_AND_CAPTURE = 'FC'
# Refund and Capture no online authorization
REFUND = 'R'
# Tax Inds
TAX_NOT_PROVIDED = 0
TAX_INCLUDED = 1
NON_TAXABLE_TRANSACTION = 2
# Customer Profile Actions
CREATE = 'C'
RETRIEVE = 'R'
UPDATE = 'U'
DELETE = 'D'
RECURRING = 'R'
DEFERRED = 'D'
# Status
# Profile Status Flag
# This field is used to set the status of a Customer Profile.
ACTIVE = 'A'
INACTIVE = 'I'
MANUAL_SUSPEND = 'MS'
# CustomerProfileOrderOverrideInd
# Defines if any Order Data can be pre-populated from
# the Customer Reference Number (CustomerRefNum)
NO_MAPPING_TO_ORDER_DATA = 'NO'
USE_CRN_FOR_ORDER_ID = 'OI'
USE_CRN_FOR_COMMENTS = 'OD'
USE_CRN_FOR_ORDER_ID_AND_COMMENTS = 'OA'
# CustomerProfileFromOrderInd
# Method to use to Generate the Customer Profile Number
# When Customer Profile Action Type = Create, defines
# what the Customer Profile Number will be:
AUTO_GENERATE = 'A' # Auto-Generate the CustomerRefNum
USE_CUSTOMER_REF_NUM = 'S' # Use CustomerRefNum field
USE_ORDER_ID = 'O' # Use OrderID field
USE_COMMENTS = 'D' # Use Comments field
SENSITIVE_FIELDS = %i[account_num cc_account_num]
# Bank account types to be used for check processing
ACCOUNT_TYPE = {
'savings' => 'S',
'checking' => 'C'
}
# safetech token flags
GET_TOKEN = 'GT'
USE_TOKEN = 'UT'
def initialize(options = {})
requires!(options, :merchant_id)
requires!(options, :login, :password) unless options[:ip_authentication]
super
@options[:merchant_id] = @options[:merchant_id].to_s
@use_secondary_url = false
end
# A – Authorization request
def authorize(money, payment_source, options = {})
# ECP for Orbital requires $0 prenotes so ensure
# if we are doing a force capture with a check, that
# we do a purchase here
return purchase(money, payment_source, options) if force_capture_with_echeck?(payment_source, options)
order = build_new_auth_purchase_order(AUTH_ONLY, money, payment_source, options)
commit(order, :authorize, options[:retry_logic], options[:trace_number])
end
def verify(credit_card, options = {})
amount = options[:verify_amount] ? options[:verify_amount].to_i : default_verify_amount(credit_card)
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(amount, credit_card, options) }
r.process(:ignore_result) { void(r.authorization) } unless amount == 0
end
end
# AC – Authorization and Capture
def purchase(money, payment_source, options = {})
action = options[:force_capture] ? FORCE_AUTH_AND_CAPTURE : AUTH_AND_CAPTURE
order = build_new_auth_purchase_order(action, money, payment_source, options)
commit(order, :purchase, options[:retry_logic], options[:trace_number])
end
# MFC - Mark For Capture
def capture(money, authorization, options = {})
commit(build_mark_for_capture_xml(money, authorization, options), :capture, options[:retry_logic], options[:trace_number])
end
# R – Refund request
def refund(money, authorization, options = {})
payment_method = options[:payment_method]
order = build_new_order_xml(REFUND, money, payment_method, options.merge(authorization: authorization)) do |xml|
add_payment_source(xml, payment_method, options)
xml.tag! :CustomerRefNum, options[:customer_ref_num] if @options[:customer_profiles] && options[:profile_txn]
end
commit(order, :refund, options[:retry_logic], options[:trace_number])
end
def credit(money, payment_method, options = {})
order = build_new_order_xml(REFUND, money, payment_method, options) do |xml|
add_payment_source(xml, payment_method, options)
end
commit(order, :refund, options[:retry_logic], options[:trace_number])
end
# Orbital save a payment method if the TokenTxnType is 'GT', that's why we use this as the default value for store
def store(creditcard, options = {})
authorize(0, creditcard, options.merge({ token_txn_type: GET_TOKEN }))
end
def void(authorization, options = {}, deprecated = {})
if !options.kind_of?(Hash)
ActiveMerchant.deprecated('Calling the void method with an amount parameter is deprecated and will be removed in a future version.')
return void(options, deprecated.merge(amount: authorization))
end
order = build_void_request_xml(authorization, options)
commit(order, :void, options[:retry_logic], options[:trace_number])
end
def default_verify_amount(credit_card)
allow_zero_auth?(credit_card) ? 0 : 100
end
def allow_zero_auth?(credit_card)
# Discover does not support a $0.00 authorization instead use $1.00
%w(visa master american_express diners_club jcb).include?(credit_card.brand)
end
# ==== Customer Profiles
# :customer_ref_num should be set unless you're happy with Orbital providing one
#
# :customer_profile_order_override_ind can be set to map
# the CustomerRefNum to OrderID or Comments. Defaults to 'NO' - no mapping
#
# 'NO' - No mapping to order data
# 'OI' - Use <CustomerRefNum> for <OrderID>
# 'OD' - Use <CustomerRefNum> for <Comments>
# 'OA' - Use <CustomerRefNum> for <OrderID> and <Comments>
#
# :order_default_description can be set optionally. 64 char max.
#
# :order_default_amount can be set optionally. integer as cents.
#
# :status defaults to Active
#
# 'A' - Active
# 'I' - Inactive
# 'MS' - Manual Suspend
def add_customer_profile(credit_card, options = {})
options[:customer_profile_action] = CREATE
order = build_customer_request_xml(credit_card, options)
commit(order, :add_customer_profile)
end
def update_customer_profile(credit_card, options = {})
options[:customer_profile_action] = UPDATE
order = build_customer_request_xml(credit_card, options)
commit(order, :update_customer_profile)
end
def retrieve_customer_profile(customer_ref_num)
options = { customer_profile_action: RETRIEVE, customer_ref_num: customer_ref_num }
order = build_customer_request_xml(nil, options)
commit(order, :retrieve_customer_profile)
end
def delete_customer_profile(customer_ref_num)
options = { customer_profile_action: DELETE, customer_ref_num: customer_ref_num }
order = build_customer_request_xml(nil, options)
commit(order, :delete_customer_profile)
end
def supports_network_tokenization?
true
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((<OrbitalConnectionUsername>).+(</OrbitalConnectionUsername>)), '\1[FILTERED]\2').
gsub(%r((<OrbitalConnectionPassword>).+(</OrbitalConnectionPassword>)), '\1[FILTERED]\2').
gsub(%r((<AccountNum>).+(</AccountNum>)), '\1[FILTERED]\2').
# the response sometimes contains a new line that cuts off the end of the closing tag
gsub(%r((<CCAccountNum>).+(</CC)), '\1[FILTERED]\2').
gsub(%r((<CardSecVal>).+(</CardSecVal>)), '\1[FILTERED]\2').
gsub(%r((<MerchantID>).+(</MerchantID>)), '\1[FILTERED]\2').
gsub(%r((<CustomerMerchantID>).+(</CustomerMerchantID>)), '\1[FILTERED]\2').
gsub(%r((<CustomerProfileMessage>).+(</CustomerProfileMessage>)), '\1[FILTERED]\2').
gsub(%r((<CheckDDA>).+(</CheckDDA>)), '\1[FILTERED]\2').
gsub(%r((<BCRtNum>).+(</BCRtNum>)), '\1[FILTERED]\2').
gsub(%r((<DigitalTokenCryptogram>).+(</DigitalTokenCryptogram>)), '\1[FILTERED]\2')
end
private
def force_capture_with_echeck?(payment_source, options)
return false unless options[:force_capture]
return false unless payment_source.is_a?(Check)
%w(W8 W9 ND).include?(options[:action_code])
end
#=====REFERENCE FIELDS=====
def add_customer_data(xml, credit_card, options)
add_customer_ref_num(xml, options)
return if options[:profile_txn]
xml.tag! :CustomerProfileFromOrderInd, profile_number(options) if add_profile_number?(options, credit_card)
xml.tag! :CustomerProfileOrderOverrideInd, options[:customer_profile_order_override_ind] || NO_MAPPING_TO_ORDER_DATA
end
def add_profile_number?(options, credit_card)
return true unless options[:customer_ref_num] && credit_card.nil?
end
def profile_number(options)
options[:customer_ref_num] ? USE_CUSTOMER_REF_NUM : AUTO_GENERATE
end
def add_customer_ref_num(xml, options)
xml.tag! :CustomerRefNum, options[:customer_ref_num] if options[:customer_ref_num]
end
def add_tx_ref_num(xml, authorization)
return unless authorization
xml.tag! :TxRefNum, split_authorization(authorization).first
end
def authorization_string(*args)
args.compact.join(';')
end
def split_authorization(authorization)
authorization.split(';')
end
#=====DESCRIPTOR FIELDS=====
def add_soft_descriptors(xml, descriptors)
return unless descriptors
add_soft_descriptors_from_specialized_class(xml, descriptors) if descriptors.is_a?(OrbitalSoftDescriptors)
add_soft_descriptors_from_hash(xml, descriptors) if descriptors.is_a?(Hash)
end
def add_payment_action_ind(xml, payment_action_ind)
return unless payment_action_ind
xml.tag! :PaymentActionInd, payment_action_ind
end
def add_soft_descriptors_from_specialized_class(xml, soft_desc)
xml.tag! :SDMerchantName, soft_desc.merchant_name if soft_desc.merchant_name
xml.tag! :SDProductDescription, soft_desc.product_description if soft_desc.product_description
xml.tag! :SDMerchantCity, soft_desc.merchant_city if soft_desc.merchant_city
xml.tag! :SDMerchantPhone, soft_desc.merchant_phone if soft_desc.merchant_phone
xml.tag! :SDMerchantURL, soft_desc.merchant_url if soft_desc.merchant_url
xml.tag! :SDMerchantEmail, soft_desc.merchant_email if soft_desc.merchant_email
end
def add_soft_descriptors_from_hash(xml, soft_desc)
xml.tag! :SDMerchantName, soft_desc[:merchant_name] || nil
xml.tag! :SDProductDescription, soft_desc[:product_description] || nil
xml.tag! :SDMerchantCity, soft_desc[:merchant_city] || nil
xml.tag! :SDMerchantPhone, soft_desc[:merchant_phone] || nil
xml.tag! :SDMerchantURL, soft_desc[:merchant_url] || nil
xml.tag! :SDMerchantEmail, soft_desc[:merchant_email] || nil
end
def add_level2_tax(xml, options = {})
if (level2 = options[:level_2_data])
xml.tag! :TaxInd, level2[:tax_indicator] if [TAX_NOT_PROVIDED, TAX_INCLUDED, NON_TAXABLE_TRANSACTION].include?(level2[:tax_indicator].to_i)
xml.tag! :Tax, level2[:tax].to_i if level2[:tax]
end
end
def add_level3_tax(xml, options = {})
if (level3 = options[:level_3_data])
xml.tag! :PC3VATtaxAmt, byte_limit(level3[:vat_tax], 12) if level3[:vat_tax]
xml.tag! :PC3VATtaxRate, byte_limit(level3[:vat_rate], 4) if level3[:vat_rate]
xml.tag! :PC3AltTaxInd, byte_limit(level3[:alt_ind], 15) if level3[:alt_ind]
xml.tag! :PC3AltTaxAmt, byte_limit(level3[:alt_tax], 9) if level3[:alt_tax]
end
end
def add_level2_advice_addendum(xml, options = {})
if (level2 = options[:level_2_data])
xml.tag! :AMEXTranAdvAddn1, byte_limit(level2[:advice_addendum_1], 40) if level2[:advice_addendum_1]
xml.tag! :AMEXTranAdvAddn2, byte_limit(level2[:advice_addendum_2], 40) if level2[:advice_addendum_2]
xml.tag! :AMEXTranAdvAddn3, byte_limit(level2[:advice_addendum_3], 40) if level2[:advice_addendum_3]
xml.tag! :AMEXTranAdvAddn4, byte_limit(level2[:advice_addendum_4], 40) if level2[:advice_addendum_4]
end
end
def add_level2_purchase(xml, options = {})
if (level2 = options[:level_2_data])
xml.tag! :PCOrderNum, byte_limit(level2[:purchase_order], 17) if level2[:purchase_order]
xml.tag! :PCDestZip, byte_limit(format_address_field(level2[:zip]), 10) if level2[:zip]
xml.tag! :PCDestName, byte_limit(format_address_field(level2[:name]), 30) if level2[:name]
xml.tag! :PCDestAddress1, byte_limit(format_address_field(level2[:address1]), 30) if level2[:address1]
xml.tag! :PCDestAddress2, byte_limit(format_address_field(level2[:address2]), 30) if level2[:address2]
xml.tag! :PCDestCity, byte_limit(format_address_field(level2[:city]), 20) if level2[:city]
xml.tag! :PCDestState, byte_limit(format_address_field(level2[:state]), 2) if level2[:state]
end
end
def add_level3_purchase(xml, options = {})
if (level3 = options[:level_3_data])
xml.tag! :PC3FreightAmt, byte_limit(level3[:freight_amount], 12) if level3[:freight_amount]
xml.tag! :PC3DutyAmt, byte_limit(level3[:duty_amount], 12) if level3[:duty_amount]
xml.tag! :PC3DestCountryCd, byte_limit(level3[:dest_country], 3) if level3[:dest_country]
xml.tag! :PC3ShipFromZip, byte_limit(level3[:ship_from_zip], 10) if level3[:ship_from_zip]
xml.tag! :PC3DiscAmt, byte_limit(level3[:discount_amount], 12) if level3[:discount_amount]
end
end
def add_line_items(xml, options = {})
xml.tag! :PC3LineItemCount, byte_limit(options[:line_items].count, 2)
xml.tag! :PC3LineItemArray do
options[:line_items].each_with_index do |line_item, index|
xml.tag! :PC3LineItem do
xml.tag! :PC3DtlIndex, byte_limit(index + 1, 2)
line_item.each do |key, value|
if [:line_tot, 'line_tot'].include? key
formatted_key = :PC3Dtllinetot
else
formatted_key = "PC3Dtl#{key.to_s.camelize}".to_sym
end
xml.tag! formatted_key, value
end
end
end
end
end
#=====ADDRESS FIELDS=====
def add_address(xml, payment_source, options)
return unless (address = get_address(options))
if avs_supported?(address[:country]) || empty?(address[:country])
xml.tag! :AVSzip, byte_limit(format_address_field(address[:zip]), 10)
xml.tag! :AVSaddress1, byte_limit(format_address_field(address[:address1]), 30)
xml.tag! :AVSaddress2, byte_limit(format_address_field(address[:address2]), 30)
xml.tag! :AVScity, byte_limit(format_address_field(address[:city]), 20)
xml.tag! :AVSstate, byte_limit(format_address_field(address[:state]), 2)
xml.tag! :AVSphoneNum, (address[:phone] ? address[:phone].scan(/\d/).join.to_s[0..13] : nil)
end
xml.tag! :AVSname, billing_name(payment_source, options)
xml.tag! :AVScountryCode, byte_limit(format_address_field(filter_unsupported_countries(address[:country])), 2)
# Needs to come after AVScountryCode
add_destination_address(xml, address) if avs_supported?(address[:country]) || empty?(address[:country])
end
def add_destination_address(xml, address)
return unless address[:dest_zip]
xml.tag! :AVSDestzip, byte_limit(format_address_field(address[:dest_zip]), 10)
xml.tag! :AVSDestaddress1, byte_limit(format_address_field(address[:dest_address1]), 30)
xml.tag! :AVSDestaddress2, byte_limit(format_address_field(address[:dest_address2]), 30)
xml.tag! :AVSDestcity, byte_limit(format_address_field(address[:dest_city]), 20)
xml.tag! :AVSDeststate, byte_limit(format_address_field(address[:dest_state]), 2)
xml.tag! :AVSDestphoneNum, (address[:dest_phone] ? address[:dest_phone].scan(/\d/).join.to_s[0..13] : nil)
xml.tag! :AVSDestname, byte_limit(address[:dest_name], 30)
xml.tag! :AVSDestcountryCode, filter_unsupported_countries(address[:dest_country])
end
# For Profile requests
def add_customer_address(xml, options)
return unless (address = get_address(options))
xml.tag! :CustomerAddress1, byte_limit(format_address_field(address[:address1]), 30)
xml.tag! :CustomerAddress2, byte_limit(format_address_field(address[:address2]), 30)
xml.tag! :CustomerCity, byte_limit(format_address_field(address[:city]), 20)
xml.tag! :CustomerState, byte_limit(format_address_field(address[:state]), 2)
xml.tag! :CustomerZIP, byte_limit(format_address_field(address[:zip]), 10)
xml.tag! :CustomerEmail, byte_limit(address[:email], 50) if address[:email]
xml.tag! :CustomerPhone, (address[:phone] ? address[:phone].scan(/\d/).join.to_s : nil)
xml.tag! :CustomerCountryCode, filter_unsupported_countries(address[:country])
end
def billing_name(payment_source, options)
if !payment_source.is_a?(String) && payment_source&.name.present?
payment_source.name[0..29]
elsif options[:billing_address] && options[:billing_address][:name].present?
options[:billing_address][:name][0..29]
end
end
def avs_supported?(address)
AVS_SUPPORTED_COUNTRIES.include?(address.to_s)
end
def filter_unsupported_countries(address)
avs_supported?(address) ? address.to_s : ''
end
def get_address(options)
options[:billing_address] || options[:address]
end
def add_safetech_token_data(xml, payment_source, options)
payment_source_token = split_authorization(payment_source).values_at(2).first
xml.tag! :CardBrand, options[:card_brand]
xml.tag! :AccountNum, payment_source_token
end
#=====PAYMENT SOURCE FIELDS=====
# Payment can be done through either Credit Card or Electronic Check
def add_payment_source(xml, payment_source, options = {})
add_safetech_token_data(xml, payment_source, options) if payment_source.is_a?(String)
payment_source.is_a?(Check) ? add_echeck(xml, payment_source, options) : add_credit_card(xml, payment_source, options)
end
def add_echeck(xml, check, options = {})
return unless check
xml.tag! :CardBrand, 'EC'
add_currency_fields(xml, options[:currency])
xml.tag! :BCRtNum, check.routing_number
xml.tag! :CheckDDA, check.account_number if check.account_number
add_bank_account_type(xml, check)
xml.tag! :ECPAuthMethod, options[:auth_method] if options[:auth_method]
xml.tag! :BankPmtDelv, options[:payment_delivery] || 'B'
xml.tag! :AVSname, (check&.name ? check.name[0..29] : nil) if get_address(options).blank?
end
def add_credit_card(xml, credit_card, options)
xml.tag! :AccountNum, credit_card.number if credit_card.is_a?(CreditCard)
xml.tag! :Exp, expiry_date(credit_card) if credit_card.is_a?(CreditCard)
add_currency_fields(xml, options[:currency])
add_verification_value(xml, credit_card) if credit_card.is_a?(CreditCard)
end
def add_verification_value(xml, credit_card)
return unless credit_card&.verification_value?
# If you are trying to collect a Card Verification Number
# (CardSecVal) for a Visa or Discover transaction, pass one of these values:
# 1 Value is Present
# 2 Value on card but illegible
# 9 Cardholder states data not available
# If the transaction is not a Visa or Discover transaction:
# Null-fill this attribute OR
# Do not submit the attribute at all.
# - http://download.chasepaymentech.com/docs/orbital/orbital_gateway_xml_specification.pdf
xml.tag! :CardSecValInd, '1' if %w(visa discover diners_club).include?(credit_card.brand)
xml.tag! :CardSecVal, credit_card.verification_value
end
def add_currency_fields(xml, currency)
xml.tag! :CurrencyCode, currency_code(currency)
xml.tag! :CurrencyExponent, currency_exponents(currency)
end
def add_bank_account_type(xml, check)
bank_account_type =
if check.account_holder_type == 'business'
'X'
else
ACCOUNT_TYPE[check.account_type]
end
xml.tag! :BankAccountType, bank_account_type if bank_account_type
end
def add_card_indicators(xml, options)
xml.tag! :CardIndicators, options[:card_indicators] if options[:card_indicators]
end
def currency_code(currency)
CURRENCY_CODES[(currency || self.default_currency)].to_s
end
def currency_exponents(currency)
CURRENCY_EXPONENTS[(currency || self.default_currency)].to_s
end
def expiry_date(credit_card)
"#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
end
def bin
@options[:bin] || (salem_mid? ? '000001' : '000002')
end
def salem_mid?
@options[:merchant_id].length == 6
end
#=====BRAND-SPECIFIC FIELDS=====
def add_cavv(xml, credit_card, three_d_secure)
return unless three_d_secure && credit_card.brand == 'visa'
xml.tag!(:CAVV, three_d_secure[:cavv])
end
def add_aav(xml, credit_card, three_d_secure)
return unless three_d_secure && credit_card.brand == 'master'
xml.tag!(:AAV, three_d_secure[:cavv])
end
def add_aevv(xml, credit_card, three_d_secure)
return unless three_d_secure && credit_card.brand == 'american_express'
xml.tag!(:AEVV, three_d_secure[:cavv])
end
def add_xid(xml, credit_card, three_d_secure)
return unless three_d_secure && credit_card.brand == 'visa'
xml.tag!(:XID, three_d_secure[:xid]) if three_d_secure[:xid]
end
def add_pymt_brand_program_code(xml, credit_card, three_d_secure)
return unless three_d_secure && credit_card.brand == 'american_express'
xml.tag!(:PymtBrandProgramCode, 'ASK')
end
def mastercard?(payment_source)
payment_source.is_a?(CreditCard) && payment_source.brand == 'master'
end
def add_mastercard_fields(xml, credit_card, parameters, three_d_secure)
add_mc_sca_merchant_initiated(xml, credit_card, parameters, three_d_secure)
add_mc_sca_recurring(xml, credit_card, parameters, three_d_secure)
add_mc_program_protocol(xml, credit_card, three_d_secure)
add_mc_directory_trans_id(xml, credit_card, three_d_secure)
add_mc_ucafind(xml, credit_card, three_d_secure)
end
def add_mc_sca_merchant_initiated(xml, credit_card, parameters, three_d_secure)
return unless parameters.try(:[], :sca_merchant_initiated)
return unless three_d_secure.try(:[], :eci) == '7'
xml.tag!(:SCAMerchantInitiatedTransaction, parameters[:sca_merchant_initiated])
end
def add_mc_sca_recurring(xml, credit_card, parameters, three_d_secure)
return unless parameters.try(:[], :sca_recurring)
return unless three_d_secure.try(:[], :eci) == '7'
xml.tag!(:SCARecurringPayment, parameters[:sca_recurring])
end
def add_mc_program_protocol(xml, credit_card, three_d_secure)
return unless version = three_d_secure.try(:[], :version)
xml.tag!(:MCProgramProtocol, version.to_s[0])
end
def add_mc_directory_trans_id(xml, credit_card, three_d_secure)
return unless three_d_secure
xml.tag!(:MCDirectoryTransID, three_d_secure[:ds_transaction_id]) if three_d_secure[:ds_transaction_id]
end
def add_mc_ucafind(xml, credit_card, three_d_secure)
return unless three_d_secure
xml.tag! :UCAFInd, '4'
end
#=====SCA (STORED CREDENTIAL) FIELDS=====
def add_stored_credentials(xml, parameters)
return unless parameters[:mit_stored_credential_ind] == 'Y' || parameters[:stored_credential] && !parameters[:stored_credential].values.all?(&:nil?)
if msg_type = get_msg_type(parameters)
xml.tag! :MITMsgType, msg_type
end
xml.tag! :MITStoredCredentialInd, 'Y'
if parameters[:mit_submitted_transaction_id]
xml.tag! :MITSubmittedTransactionID, parameters[:mit_submitted_transaction_id]
elsif parameters.dig(:stored_credential, :network_transaction_id) && parameters.dig(:stored_credential, :initiator) == 'merchant'
xml.tag! :MITSubmittedTransactionID, parameters[:stored_credential][:network_transaction_id]
end
end
def get_msg_type(parameters)
return parameters[:mit_msg_type] if parameters[:mit_msg_type]
return 'CSTO' if parameters[:stored_credential][:initial_transaction]
return unless parameters[:stored_credential][:initiator] && parameters[:stored_credential][:reason_type]
initiator =
case parameters[:stored_credential][:initiator]
when 'cardholder', 'customer' then 'C'
when 'merchant' then 'M'
end
reason =
case parameters[:stored_credential][:reason_type]
when 'recurring' then 'REC'
when 'installment' then 'INS'
when 'unscheduled' then 'USE'
end
"#{initiator}#{reason}"
end
#=====NETWORK TOKENIZATION FIELDS=====
def add_eci(xml, credit_card, three_d_secure)
eci = if three_d_secure
three_d_secure[:eci]
elsif credit_card.is_a?(NetworkTokenizationCreditCard)
credit_card.eci
end
xml.tag!(:AuthenticationECIInd, eci) if eci
end
def add_dpanind(xml, credit_card, industry_type = nil)
return unless credit_card.is_a?(NetworkTokenizationCreditCard)
xml.tag! :DPANInd, 'Y' unless industry_type == 'RC'
end
def add_digital_token_cryptogram(xml, credit_card, three_d_secure)
return unless credit_card.is_a?(NetworkTokenizationCreditCard) || three_d_secure && credit_card.brand == 'discover'
cryptogram =
if three_d_secure && credit_card.brand == 'discover'
three_d_secure[:cavv]
else
credit_card.payment_cryptogram
end
xml.tag!(:DigitalTokenCryptogram, cryptogram)
end
#=====OTHER FIELDS=====
# For Canadian transactions on PNS Tampa on New Order
# RF - First Recurring Transaction
# RS - Subsequent Recurring Transactions
def set_recurring_ind(xml, parameters)
return unless parameters[:recurring_ind]
raise 'RecurringInd must be set to either "RF" or "RS"' unless %w(RF RS).include?(parameters[:recurring_ind])
xml.tag! :RecurringInd, parameters[:recurring_ind]
end
def add_managed_billing(xml, options)
return unless mb = options[:managed_billing]
ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE
# default to recurring (R). Other option is deferred (D).
xml.tag! :MBType, mb[:type] || RECURRING
# default to Customer Reference Number
xml.tag! :MBOrderIdGenerationMethod, mb[:order_id_generation_method] || 'IO'
# By default use MBRecurringEndDate, set to N.
# MMDDYYYY
xml.tag! :MBRecurringStartDate, mb[:start_date].scan(/\d/).join.to_s if mb[:start_date]
# MMDDYYYY
xml.tag! :MBRecurringEndDate, mb[:end_date].scan(/\d/).join.to_s if mb[:end_date]
# By default listen to any value set in MBRecurringEndDate.
xml.tag! :MBRecurringNoEndDateFlag, mb[:no_end_date_flag] || 'N' # 'Y' || 'N' (Yes or No).
xml.tag! :MBRecurringMaxBillings, mb[:max_billings] if mb[:max_billings]
xml.tag! :MBRecurringFrequency, mb[:frequency] if mb[:frequency]
xml.tag! :MBDeferredBillDate, mb[:deferred_bill_date] if mb[:deferred_bill_date]
xml.tag! :MBMicroPaymentMaxDollarValue, mb[:max_dollar_value] if mb[:max_dollar_value]
xml.tag! :MBMicroPaymentMaxBillingDays, mb[:max_billing_days] if mb[:max_billing_days]
xml.tag! :MBMicroPaymentMaxTransactions, mb[:max_transactions] if mb[:max_transactions]
end
def add_ews_details(xml, payment_source, parameters = {})
split_name = payment_source.first_name.split if payment_source.first_name
xml.tag! :EWSFirstName, split_name[0]
xml.tag! :EWSMiddleName, split_name[1..-1].join(' ')
xml.tag! :EWSLastName, payment_source.last_name
xml.tag! :EWSBusinessName, parameters[:company] if payment_source.first_name.empty? && payment_source.last_name.empty?
if (address = (parameters[:billing_address] || parameters[:address]))
xml.tag! :EWSAddressLine1, byte_limit(format_address_field(address[:address1]), 30)
xml.tag! :EWSAddressLine2, byte_limit(format_address_field(address[:address2]), 30)
xml.tag! :EWSCity, byte_limit(format_address_field(address[:city]), 20)
xml.tag! :EWSState, byte_limit(format_address_field(address[:state]), 2)
xml.tag! :EWSZip, byte_limit(format_address_field(address[:zip]), 10)
end
xml.tag! :EWSPhoneType, parameters[:phone_type]
xml.tag! :EWSPhoneNumber, parameters[:phone_number]
xml.tag! :EWSCheckSerialNumber, payment_source.account_number unless parameters[:auth_method].eql?('I')
end
# Adds ECP conditional attributes depending on other attribute values
def add_ecp_details(xml, payment_source, parameters = {})
requires!(payment_source.account_number) if parameters[:auth_method]&.eql?('A') || parameters[:auth_method]&.eql?('P')
xml.tag! :ECPActionCode, parameters[:action_code] if parameters[:action_code]
xml.tag! :ECPCheckSerialNumber, payment_source.account_number if parameters[:auth_method]&.eql?('A') || parameters[:auth_method]&.eql?('P')
if parameters[:auth_method]&.eql?('P')
xml.tag! :ECPTerminalCity, parameters[:terminal_city] if parameters[:terminal_city]
xml.tag! :ECPTerminalState, parameters[:terminal_state] if parameters[:terminal_state]
xml.tag! :ECPImageReferenceNumber, parameters[:image_reference_number] if parameters[:image_reference_number]
end
if parameters[:action_code]&.eql?('W3') || parameters[:action_code]&.eql?('W5') ||
parameters[:action_code]&.eql?('W7') || parameters[:action_code]&.eql?('W9')
add_ews_details(xml, payment_source, parameters)
end
end
def add_xml_credentials(xml)
unless ip_authentication?
xml.tag! :OrbitalConnectionUsername, @options[:login]
xml.tag! :OrbitalConnectionPassword, @options[:password]
end
end
def add_bin_merchant_and_terminal(xml, parameters)
xml.tag! :BIN, bin
xml.tag! :MerchantID, @options[:merchant_id]
xml.tag! :TerminalID, parameters[:terminal_id] || '001'
end
#=====REQUEST/RESPONSE METHODS=====
def commit(order, message_type, retry_logic = nil, trace_number = nil)
headers = POST_HEADERS.merge('Content-length' => order.size.to_s)
if (@options[:retry_logic] || retry_logic) && trace_number
headers['Trace-number'] = trace_number.to_s
headers['Merchant-Id'] = @options[:merchant_id]
end
request = ->(url) { parse(ssl_post(url, order, headers)) }
# Failover URL will be attempted in the event of a connection error
response =
begin
raise ConnectionError.new 'Should use secondary url', 500 if @use_secondary_url
request.call(remote_url)
rescue ConnectionError
request.call(remote_url(:secondary))
end
authorization = authorization_string(response[:tx_ref_num], response[:order_id], response[:safetech_token], response[:card_brand])
Response.new(
success?(response, message_type),
message_from(response),
response,
{
authorization: authorization,
test: self.test?,
avs_result: OrbitalGateway::AVSResult.new(response[:avs_resp_code]),
cvv_result: OrbitalGateway::CVVResult.new(response[:cvv2_resp_code])
}
)
end
def remote_url(url = :primary)
if url == :primary
(self.test? ? self.test_url : self.live_url)
else
(self.test? ? self.secondary_test_url : self.secondary_live_url)
end
end
def parse(body)
response = {}
xml = REXML::Document.new(strip_invalid_xml_chars(body))
root = REXML::XPath.first(xml, '//Response') ||
REXML::XPath.first(xml, '//ErrorResponse')
if root
root.elements.to_a.each do |node|
recurring_parse_element(response, node)
end
end
response.delete_if { |k, _| SENSITIVE_FIELDS.include?(k) }
end
def recurring_parse_element(response, node)
if node.has_elements?
node.elements.each { |e| recurring_parse_element(response, e) }
else
response[node.name.underscore.to_sym] = node.text
end
end
def success?(response, message_type)
if %i[void].include?(message_type)
response[:proc_status] == SUCCESS
elsif %i[refund].include?(message_type)
response[:proc_status] == SUCCESS && response[:approval_status] == APPROVAL_SUCCESS
elsif response[:customer_profile_action]
response[:profile_proc_status] == SUCCESS
else
response[:proc_status] == SUCCESS &&
APPROVED.include?(response[:resp_code])
end
end
def message_from(response)
response[:resp_msg] || response[:status_msg] || response[:customer_profile_message]
end
def ip_authentication?
@options[:ip_authentication] == true
end
#=====BUILDER METHODS=====
def build_new_auth_purchase_order(action, money, payment_source, options)
build_new_order_xml(action, money, payment_source, options) do |xml|
add_payment_source(xml, payment_source, options)
add_address(xml, payment_source, options)
if @options[:customer_profiles]
add_customer_data(xml, payment_source, options)
add_managed_billing(xml, options)
end
end
end
def build_new_order_xml(action, money, payment_source, parameters = {})
requires!(parameters, :order_id)
@use_secondary_url = parameters[:use_secondary_url] if parameters[:use_secondary_url]
xml = xml_envelope
xml.tag! :Request do
xml.tag! :NewOrder do
add_xml_credentials(xml)
xml.tag! :IndustryType, parameters[:industry_type] || ECOMMERCE_TRANSACTION
xml.tag! :MessageType, action
add_bin_merchant_and_terminal(xml, parameters)
yield xml if block_given?
three_d_secure = parameters[:three_d_secure]
add_eci(xml, payment_source, three_d_secure)
add_cavv(xml, payment_source, three_d_secure)
add_xid(xml, payment_source, three_d_secure)
xml.tag! :OrderID, format_order_id(parameters[:order_id])
xml.tag! :Amount, amount(money)
xml.tag! :Comments, parameters[:comments] if parameters[:comments]
add_level2_tax(xml, parameters)
add_level2_advice_addendum(xml, parameters)
add_aav(xml, payment_source, three_d_secure)
# CustomerAni, AVSPhoneType and AVSDestPhoneType could be added here.
add_soft_descriptors(xml, parameters[:soft_descriptors])
add_payment_action_ind(xml, parameters[:payment_action_ind])
add_dpanind(xml, payment_source, parameters[:industry_type])
add_aevv(xml, payment_source, three_d_secure)
add_digital_token_cryptogram(xml, payment_source, three_d_secure)
xml.tag! :ECPSameDayInd, parameters[:same_day] if parameters[:same_day] && payment_source.is_a?(Check)
set_recurring_ind(xml, parameters)
# Append Transaction Reference Number at the end for Refund transactions
add_tx_ref_num(xml, parameters[:authorization]) if action == REFUND && payment_source.nil?
add_level2_purchase(xml, parameters)
add_level3_purchase(xml, parameters)
add_level3_tax(xml, parameters)
add_line_items(xml, parameters) if parameters[:line_items]
add_ecp_details(xml, payment_source, parameters) if payment_source.is_a?(Check)
add_card_indicators(xml, parameters)
add_stored_credentials(xml, parameters)
add_pymt_brand_program_code(xml, payment_source, three_d_secure)
add_mastercard_fields(xml, payment_source, parameters, three_d_secure) if mastercard?(payment_source)
xml.tag! :TokenTxnType, parameters[:token_txn_type] if parameters[:token_txn_type]
end
end
xml.target!
end
def build_mark_for_capture_xml(money, authorization, parameters = {})
tx_ref_num, order_id = split_authorization(authorization)
xml = xml_envelope
xml.tag! :Request do
xml.tag! :MarkForCapture do
add_xml_credentials(xml)
xml.tag! :OrderID, format_order_id(order_id)
xml.tag! :Amount, amount(money)
add_level2_tax(xml, parameters)
add_bin_merchant_and_terminal(xml, parameters)
xml.tag! :TxRefNum, tx_ref_num
add_level2_purchase(xml, parameters)
add_level2_advice_addendum(xml, parameters)
add_level3_purchase(xml, parameters)
add_level3_tax(xml, parameters)
add_line_items(xml, parameters) if parameters[:line_items]
end
end
xml.target!
end
def build_void_request_xml(authorization, parameters = {})
tx_ref_num, order_id = split_authorization(authorization)
xml = xml_envelope
xml.tag! :Request do
xml.tag! :Reversal do
add_xml_credentials(xml)
xml.tag! :TxRefNum, tx_ref_num
xml.tag! :TxRefIdx, parameters[:transaction_index]
xml.tag! :AdjustedAmt, parameters[:amount] # setting adjusted amount to nil will void entire amount
xml.tag! :OrderID, format_order_id(order_id || parameters[:order_id])
add_bin_merchant_and_terminal(xml, parameters)
xml.tag! :ReversalRetryNumber, parameters[:reversal_retry_number] if parameters[:reversal_retry_number]
xml.tag! :OnlineReversalInd, parameters[:online_reversal_ind] if parameters[:online_reversal_ind]
end
end
xml.target!
end
def xml_envelope
xml = Builder::XmlMarkup.new(indent: 2)
xml.instruct!(:xml, version: '1.0', encoding: 'UTF-8')
xml
end
# Null characters are possible in some responses (namely, the respMsg field), causing XML parsing errors
# Prevent by substituting these with a valid placeholder string
def strip_invalid_xml_chars(xml)
xml.gsub(/\u0000/, '[null]')
end
# The valid characters include:
#
# 1. all letters and digits
# 2. - , $ @ & and a space character, though the space character cannot be the leading character
# 3. PINless Debit transactions can only use uppercase and lowercase alpha (A-Z, a-z) and numeric (0-9)
def format_order_id(order_id)
illegal_characters = /[^,$@&\- \w]/
order_id = order_id.to_s.tr('.', '-')
order_id.gsub!(illegal_characters, '')
order_id.lstrip!
order_id[0...22]
end
# Address-related fields cannot contain % | ^ \ /
# Returns the value with these characters removed, or nil
def format_address_field(value)
value.gsub(/[%\|\^\\\/]/, '') if value.respond_to?(:gsub)
end
# Field lengths should be limited by byte count instead of character count
# Returns the truncated value or nil
def byte_limit(value, byte_length)
limited_value = ''
value.to_s.each_char do |c|
break if (limited_value.bytesize + c.bytesize) > byte_length
limited_value << c
end
limited_value
end
def build_customer_request_xml(credit_card, options = {})
ActiveMerchant.deprecated 'Customer Profile support in Orbital is non-conformant to the ActiveMerchant API and will be removed in its current form in a future version. Please contact the ActiveMerchant maintainers if you have an interest in modifying it to conform to the store/unstore/update API.'
xml = xml_envelope
xml.tag! :Request do
xml.tag! :Profile do
xml.tag! :OrbitalConnectionUsername, @options[:login] unless ip_authentication?
xml.tag! :OrbitalConnectionPassword, @options[:password] unless ip_authentication?
xml.tag! :CustomerBin, bin
xml.tag! :CustomerMerchantID, @options[:merchant_id]
xml.tag! :CustomerName, credit_card.name if credit_card
xml.tag! :CustomerRefNum, options[:customer_ref_num] if options[:customer_ref_num]
add_customer_address(xml, options)
xml.tag! :CustomerProfileAction, options[:customer_profile_action] # C, R, U, D
# NO No mapping to order data
# OI Use <CustomerRefNum> for <OrderID>
# OD Use <CustomerReferNum> for <Comments>
# OA Use <CustomerRefNum> for <OrderID> and <Comments>
xml.tag! :CustomerProfileOrderOverrideInd, options[:customer_profile_order_override_ind] || NO_MAPPING_TO_ORDER_DATA
if options[:customer_profile_action] == CREATE
# A Auto-Generate the CustomerRefNum
# S Use CustomerRefNum field
# O Use OrderID field
# D Use Comments field
xml.tag! :CustomerProfileFromOrderInd, (options[:customer_ref_num] ? USE_CUSTOMER_REF_NUM : AUTO_GENERATE)
end
xml.tag! :OrderDefaultDescription, options[:order_default_description][0..63] if options[:order_default_description]
xml.tag! :OrderDefaultAmount, options[:order_default_amount] if options[:order_default_amount]
if [CREATE, UPDATE].include? options[:customer_profile_action]
xml.tag! :CustomerAccountType, 'CC' # Only credit card supported
xml.tag! :Status, options[:status] || ACTIVE # Active
end
xml.tag! :CCAccountNum, credit_card.number if credit_card
xml.tag! :CCExpireDate, credit_card.expiry_date.expiration.strftime('%m%y') if credit_card
# This has to come after CCExpireDate.
add_managed_billing(xml, options)
end
end
xml.target!
end
# Unfortunately, Orbital uses their own special codes for AVS responses
# that are different than the standard codes defined in
# <tt>ActiveMerchant::Billing::AVSResult</tt>.
#
# This class encapsulates the response codes shown on page 240 of their spec:
# http://download.chasepaymentech.com/docs/orbital/orbital_gateway_xml_specification.pdf
#
class AVSResult < ActiveMerchant::Billing::AVSResult
CODES = {
'1' => 'No address supplied',
'2' => 'Bill-to address did not pass Auth Host edit checks',
'3' => 'AVS not performed',
'4' => 'Issuer does not participate in AVS',
'5' => 'Edit-error - AVS data is invalid',
'6' => 'System unavailable or time-out',
'7' => 'Address information unavailable',
'8' => 'Transaction Ineligible for AVS',
'9' => 'Zip Match/Zip 4 Match/Locale match',
'A' => 'Zip Match/Zip 4 Match/Locale no match',
'B' => 'Zip Match/Zip 4 no Match/Locale match',
'C' => 'Zip Match/Zip 4 no Match/Locale no match',
'D' => 'Zip No Match/Zip 4 Match/Locale match',
'E' => 'Zip No Match/Zip 4 Match/Locale no match',
'F' => 'Zip No Match/Zip 4 No Match/Locale match',
'G' => 'No match at all',
'H' => 'Zip Match/Locale match',
'J' => 'Issuer does not participate in Global AVS',
'JA' => 'International street address and postal match',
'JB' => 'International street address match. Postal code not verified',
'JC' => 'International street address and postal code not verified',
'JD' => 'International postal code match. Street address not verified',
'M1' => 'Cardholder name matches',
'M2' => 'Cardholder name, billing address, and postal code matches',
'M3' => 'Cardholder name and billing code matches',
'M4' => 'Cardholder name and billing address match',
'M5' => 'Cardholder name incorrect, billing address and postal code match',
'M6' => 'Cardholder name incorrect, billing postal code matches',
'M7' => 'Cardholder name incorrect, billing address matches',
'M8' => 'Cardholder name, billing address and postal code are all incorrect',
'N3' => 'Address matches, ZIP not verified',
'N4' => 'Address and ZIP code not verified due to incompatible formats',
'N5' => 'Address and ZIP code match (International only)',
'N6' => 'Address not verified (International only)',
'N7' => 'ZIP matches, address not verified',
'N8' => 'Address and ZIP code match (International only)',
'N9' => 'Address and ZIP code match (UK only)',
'R' => 'Issuer does not participate in AVS',
'UK' => 'Unknown',
'X' => 'Zip Match/Zip 4 Match/Address Match',
'Z' => 'Zip Match/Locale no match'
}
# Map vendor's AVS result code to a postal match code
ORBITAL_POSTAL_MATCH_CODE = {
'Y' => %w(9 A B C H JA JD M2 M3 M5 N5 N8 N9 X Z),
'N' => %w(D E F G M8),
'X' => %w(4 J R),
nil => %w(1 2 3 5 6 7 8 JB JC M1 M4 M6 M7 N3 N4 N6 N7 UK)
}.inject({}) do |map, (type, codes)|
codes.each { |code| map[code] = type }
map
end
# Map vendor's AVS result code to a street match code
ORBITAL_STREET_MATCH_CODE = {
'Y' => %w(9 B D F H JA JB M2 M4 M5 M6 M7 N3 N5 N7 N8 N9 X),
'N' => %w(A C E G M8 Z),
'X' => %w(4 J R),
nil => %w(1 2 3 5 6 7 8 JC JD M1 M3 N4 N6 UK)
}.inject({}) do |map, (type, codes)|
codes.each { |code| map[code] = type }
map
end
def self.messages
CODES
end
def initialize(code)
@code = (code.blank? ? nil : code.to_s.strip.upcase)
if @code
@message = CODES[@code]
@postal_match = ORBITAL_POSTAL_MATCH_CODE[@code]
@street_match = ORBITAL_STREET_MATCH_CODE[@code]
end
end
end
# Unfortunately, Orbital uses their own special codes for CVV responses
# that are different than the standard codes defined in
# <tt>ActiveMerchant::Billing::CVVResult</tt>.
#
# This class encapsulates the response codes shown on page 255 of their spec:
# http://download.chasepaymentech.com/docs/orbital/orbital_gateway_xml_specification.pdf
#
class CVVResult < ActiveMerchant::Billing::CVVResult
MESSAGES = {
'M' => 'Match',
'N' => 'No match',
'P' => 'Not processed',
'S' => 'Should have been present',
'U' => 'Unsupported by issuer/Issuer unable to process request',
'I' => 'Invalid',
'Y' => 'Invalid',
'' => 'Not applicable'
}
def self.messages
MESSAGES
end
def initialize(code)
@code = code.blank? ? '' : code.upcase
@message = MESSAGES[@code]
end
end
end
end
end