lib/active_merchant/billing/gateways/reach.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class ReachGateway < Gateway
self.test_url = 'https://checkout.rch.how/'
self.live_url = 'https://checkout.rch.io/'
self.supported_countries = %w(AE AG AL AM AT AU AW AZ BA BB BD BE BF BG BH BJ BM BN BO BR BS BW BZ CA CD CF
CH CI CL CM CN CO CR CU CV CY CZ DE DJ DK DM DO DZ EE EG ES ET FI FJ FK FR GA
GB GD GE GG GH GI GN GR GT GU GW GY HK HN HR HU ID IE IL IM IN IS IT JE JM JO
JP KE KG KH KM KN KR KW KY KZ LA LC LK LR LT LU LV LY MA MD MK ML MN MO MR MS
MT MU MV MW MX MY MZ NA NC NE NG NI NL NO NP NZ OM PA PE PF PG PH PK PL PT PY
QA RO RS RW SA SB SC SE SG SH SI SK SL SN SO SR ST SV SY SZ TD TG TH TN TO TR
TT TV TW TZ UG US UY UZ VC VN VU WF WS YE ZM)
self.default_currency = 'USD'
self.supported_cardtypes = %i[visa diners_club american_express jcb master discover maestro]
self.homepage_url = 'https://www.withreach.com/'
self.display_name = 'Reach'
self.currencies_without_fractions = %w(BIF BYR CLF CLP CVE DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF IDR MGA MRO)
API_VERSION = 'v2.22'.freeze
STANDARD_ERROR_CODE_MAPPING = {}
PAYMENT_METHOD_MAP = {
american_express: 'AMEX',
cabal: 'CABAL',
check: 'ACH',
dankort: 'DANKORT',
diners_club: 'DINERS',
discover: 'DISC',
elo: 'ELO',
jcb: 'JCB',
maestro: 'MAESTRO',
master: 'MC',
naranja: 'NARANJA',
union_pay: 'UNIONPAY',
visa: 'VISA'
}
def initialize(options = {})
requires!(options, :merchant_id, :secret)
super
end
def authorize(money, payment, options = {})
request = build_checkout_request(money, payment, options)
add_custom_fields_data(request, options)
add_customer_data(request, options, payment)
add_stored_credentials(request, options)
post = { request: request, card: add_payment(payment, options) }
if options[:stored_credential]
MultiResponse.run(:use_first_response) do |r|
r.process { commit('checkout', post) }
r.process do
r2 = get_network_payment_reference(r.responses[0])
r.params[:network_transaction_id] = r2.message
r2
end
end
else
commit('checkout', post)
end
end
def purchase(money, payment, options = {})
options[:capture] = true
authorize(money, payment, options)
end
def capture(money, authorization, options = {})
post = { request: { MerchantId: @options[:merchant_id], OrderId: authorization } }
commit('capture', post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r(((MerchantId)[% \w]+%\d{2})[\w -]+), '\1[FILTERED]').
gsub(%r((signature=)[\w%]+), '\1[FILTERED]\2').
gsub(%r((Number%22%3A%22)\d+), '\1[FILTERED]\2').
gsub(%r((VerificationCode%22%3A)\d+), '\1[FILTERED]\2')
end
def refund(amount, authorization, options = {})
currency = options[:currency] || currency(options[:amount])
post = {
request: {
MerchantId: @options[:merchant_id],
OrderId: authorization,
ReferenceId: options[:order_id] || options[:reference_id],
Amount: localized_amount(amount, currency)
}
}
commit('refund', post)
end
def void(authorization, options = {})
post = {
request: {
MerchantId: @options[:merchant_id],
OrderId: authorization
}
}
commit('cancel', post)
end
def verify(credit_card, options = {})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(100, credit_card, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end
private
def build_checkout_request(amount, payment, options)
currency = options[:currency] || currency(options[:amount])
{
MerchantId: @options[:merchant_id],
ReferenceId: options[:order_id],
ConsumerCurrency: currency,
Capture: options[:capture] || false,
PaymentMethod: PAYMENT_METHOD_MAP.fetch(payment.brand.to_sym, 'unsupported'),
Items: [
Sku: options[:item_sku] || SecureRandom.alphanumeric,
ConsumerPrice: localized_amount(amount, currency),
Quantity: (options[:item_quantity] || 1)
]
}
end
def add_payment(payment, options)
ntid = options.dig(:stored_credential, :network_transaction_id)
cvv_or_previos_reference = (ntid ? { PreviousNetworkPaymentReference: ntid } : { VerificationCode: payment.verification_value })
{
Name: payment.name,
Number: payment.number,
Expiry: { Month: payment.month, Year: payment.year }
}.merge!(cvv_or_previos_reference)
end
def add_customer_data(request, options, payment)
address = options[:billing_address] || options[:address]
return if address.blank?
request[:Consumer] = {
Name: payment.respond_to?(:name) ? payment.name : address[:name],
Email: options[:email],
Address: address[:address1],
City: address[:city],
Country: address[:country]
}.compact
end
def add_stored_credentials(request, options)
request[:PaymentModel] = payment_model(options) || ''
request[:DeviceFingerprint] = options[:device_fingerprint] if options[:device_fingerprint]
end
def payment_model(options)
stored_credential = options[:stored_credential]
return options[:payment_model] if options[:payment_model]
return 'CIT-One-Time' unless stored_credential
payment_model_options = {
initial_transaction: {
'cardholder' => {
'installment' => 'CIT-Setup-Scheduled',
'unscheduled' => 'CIT-Setup-Unscheduled-MIT',
'recurring' => 'CIT-Setup-Unscheduled'
}
},
no_initial_transaction: {
'cardholder' => {
'unscheduled' => 'CIT-Subsequent-Unscheduled'
},
'merchant' => {
'recurring' => 'MIT-Subsequent-Scheduled',
'unscheduled' => 'MIT-Subsequent-Unscheduled'
}
}
}
initial = stored_credential[:initial_transaction] ? :initial_transaction : :no_initial_transaction
payment_model_options[initial].dig(stored_credential[:initiator], stored_credential[:reason_type])
end
def add_custom_fields_data(request, options)
add_shipping_data(request, options) if options[:taxes].present?
request[:RateOfferId] = options[:rate_offer_id] if options[:rate_offer_id].present?
request[:Items] = options[:items] if options[:items].present?
end
def add_shipping_data(request, options)
request[:Shipping] = {
ConsumerPrice: options[:price],
ConsumerTaxes: options[:taxes],
ConsumerDuty: options[:duty]
}
request[:Consignee] = {
Name: options[:consignee_name],
Address: options[:consignee_address],
City: options[:consignee_city],
Country: options[:consignee_country]
}
end
def sign_body(body)
Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', @options[:secret].encode('utf-8'), body.encode('utf-8')))
end
def parse(body)
hash_response = URI.decode_www_form(body).to_h
hash_response['response'] = JSON.parse(hash_response['response'])
hash_response
end
def format_and_sign(post)
post[:request] = post[:request].to_json
post[:card] = post[:card].to_json if post[:card].present?
post[:signature] = sign_body(post[:request])
post
end
def get_network_payment_reference(response)
parameters = { request: { MerchantId: @options[:merchant_id], OrderId: response.params['response']['OrderId'] } }
body = post_data format_and_sign(parameters)
raw_response = ssl_request :post, url('query'), body, {}
response = parse(raw_response)
message = response.dig('response', 'Payment', 'NetworkPaymentReference')
Response.new(true, message, {})
end
def commit(action, parameters)
body = post_data format_and_sign(parameters)
raw_response = ssl_post url(action), body
response = parse(raw_response)
Response.new(
success_from(response),
message_from(response) || '',
response,
authorization: authorization_from(response['response']),
# avs_result: AVSResult.new(code: response['some_avs_response_key']),
# cvv_result: CVVResult.new(response['some_cvv_response_key']),
test: test?,
error_code: error_code_from(response)
)
rescue ActiveMerchant::ResponseError => e
Response.new(false, (e.response.body.present? ? e.response.body : e.response.msg), {}, test: test?)
end
def success_from(response)
response.dig('response', 'Error').blank?
end
def message_from(response)
success_from(response) ? '' : response.dig('response', 'Error', 'ReasonCode')
end
def authorization_from(response)
response['OrderId']
end
def post_data(params)
params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
end
def error_code_from(response)
response['response']['Error']['Code'] unless success_from(response)
end
def url(action)
"#{test? ? test_url : live_url}#{API_VERSION}/#{action}"
end
end
end
end