lib/active_merchant/billing/gateways/flex_charge.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class FlexChargeGateway < Gateway
self.test_url = 'https://api-sandbox.flex-charge.com/v1/'
self.live_url = 'https://api.flex-charge.com/v1/'
self.supported_countries = ['US']
self.default_currency = 'USD'
self.supported_cardtypes = %i[visa master american_express discover]
self.money_format = :cents
self.homepage_url = 'https://www.flex-charge.com/'
self.display_name = 'FlexCharge'
ENDPOINTS_MAPPING = {
authenticate: 'oauth2/token',
purchase: 'evaluate',
sync: 'outcome',
refund: 'orders/%s/refund',
store: 'tokenize',
inquire: 'orders/%s'
}
SUCCESS_MESSAGES = %w(APPROVED CHALLENGE SUBMITTED SUCCESS PROCESSING).freeze
def initialize(options = {})
requires!(options, :app_key, :app_secret, :site_id, :mid)
super
end
def purchase(money, credit_card, options = {})
post = {}
address = options[:billing_address] || options[:address]
add_merchant_data(post, options)
add_base_data(post, options)
add_invoice(post, money, credit_card, options)
add_mit_data(post, options)
add_payment_method(post, credit_card, address, options)
add_address(post, credit_card, address)
add_customer_data(post, options)
add_three_ds(post, options)
commit(:purchase, post)
end
def refund(money, authorization, options = {})
commit(:refund, { amountToRefund: (money.to_f / 100).round(2) }, authorization)
end
def store(credit_card, options = {})
address = options[:billing_address] || options[:address] || {}
first_name, last_name = address_names(address[:name], credit_card)
post = {
payment_method: {
credit_card: {
first_name: first_name,
last_name: last_name,
month: credit_card.month,
year: credit_card.year,
number: credit_card.number,
verification_value: credit_card.verification_value
}.compact
}
}
commit(:store, post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Bearer )[a-zA-Z0-9._-]+)i, '\1[FILTERED]').
gsub(%r(("AppKey\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("AppSecret\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("accessToken\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("mid\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("siteId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("environment\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("cardNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
gsub(%r(("verification_value\\?":\\?")\d+), '\1[FILTERED]')
end
def inquire(authorization, options = {})
commit(:inquire, {}, authorization, :get)
end
private
def add_three_ds(post, options)
return unless three_d_secure = options[:three_d_secure]
post[:threeDSecure] = {
threeDsVersion: three_d_secure[:version],
EcommerceIndicator: three_d_secure[:eci],
authenticationValue: three_d_secure[:cavv],
directoryServerTransactionId: three_d_secure[:ds_transaction_id],
xid: three_d_secure[:xid],
authenticationValueAlgorithm: three_d_secure[:cavv_algorithm],
directoryResponseStatus: three_d_secure[:directory_response_status],
authenticationResponseStatus: three_d_secure[:authentication_response_status],
enrolled: three_d_secure[:enrolled]
}
end
def add_merchant_data(post, options)
post[:siteId] = @options[:site_id]
post[:mid] = @options[:mid]
end
def add_base_data(post, options)
post[:isDeclined] = cast_bool(options[:is_declined])
post[:orderId] = options[:order_id]
post[:idempotencyKey] = options[:idempotency_key] || options[:order_id]
end
def add_mit_data(post, options)
return if options[:is_mit].nil?
post[:isMIT] = cast_bool(options[:is_mit])
post[:isRecurring] = cast_bool(options[:is_recurring])
post[:expiryDateUtc] = options[:mit_expiry_date_utc]
end
def add_customer_data(post, options)
post[:payer] = { email: options[:email] || 'NA', phone: phone_from(options) }.compact
end
def add_address(post, payment, address)
first_name, last_name = address_names(address[:name], payment)
post[:billingInformation] = {
firstName: first_name,
lastName: last_name,
country: address[:country],
phone: address[:phone],
countryCode: address[:country],
addressLine1: address[:address1],
state: address[:state],
city: address[:city],
zipCode: address[:zip]
}.compact
end
def add_invoice(post, money, credit_card, options)
post[:transaction] = {
id: options[:order_id],
dynamicDescriptor: options[:description],
timestamp: Time.now.utc.iso8601,
timezoneUtcOffset: options[:timezone_utc_offset],
amount: money,
currency: (options[:currency] || currency(money)),
responseCode: options[:response_code],
responseCodeSource: options[:response_code_source] || '',
avsResultCode: options[:avs_result_code],
cvvResultCode: options[:cvv_result_code],
cavvResultCode: options[:cavv_result_code],
cardNotPresent: credit_card.is_a?(String) ? false : credit_card.verification_value.blank?
}.compact
end
def add_payment_method(post, credit_card, address, options)
payment_method = case credit_card
when String
{ Token: true, cardNumber: credit_card }
else
{
holderName: credit_card.name,
cardType: 'CREDIT',
cardBrand: credit_card.brand&.upcase,
cardCountry: address[:country],
expirationMonth: credit_card.month,
expirationYear: credit_card.year,
cardBinNumber: credit_card.number[0..5],
cardLast4Digits: credit_card.number[-4..-1],
cardNumber: credit_card.number,
Token: false
}
end
post[:paymentMethod] = payment_method.compact
end
def address_names(address_name, payment_method)
split_names(address_name).tap do |names|
names[0] = payment_method&.first_name unless names[0].present?
names[1] = payment_method&.last_name unless names[1].present?
end
end
def phone_from(options)
options[:phone] || options.dig(:billing_address, :phone_number)
end
def access_token_valid?
@options[:access_token].present? && @options.fetch(:token_expires, 0) > DateTime.now.strftime('%Q').to_i
end
def fetch_access_token
params = { AppKey: @options[:app_key], AppSecret: @options[:app_secret] }
response = parse(ssl_post(url(:authenticate), params.to_json, headers))
@options[:access_token] = response[:accessToken]
@options[:token_expires] = response[:expires]
@options[:new_credentials] = true
Response.new(
response[:accessToken].present?,
message_from(response),
response,
test: test?,
error_code: response[:statusCode]
)
rescue ResponseError => e
raise OAuthResponseError.new(e)
end
def url(action, id = nil)
"#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action] % id}"
end
def headers
{ 'Content-Type' => 'application/json' }.tap do |headers|
headers['Authorization'] = "Bearer #{@options[:access_token]}" if @options[:access_token]
end
end
def parse(body)
JSON.parse(body).with_indifferent_access
rescue JSON::ParserError
{
errors: body,
status: 'Unable to parse JSON response'
}.with_indifferent_access
end
def commit(action, post, authorization = nil, method = :post)
MultiResponse.run do |r|
r.process { fetch_access_token } unless access_token_valid?
r.process do
api_request(action, post, authorization, method).tap do |response|
response.params.merge!(@options.slice(:access_token, :token_expires)) if @options[:new_credentials]
end
end
end
end
def api_request(action, post, authorization = nil, method = :post)
response = parse ssl_request(method, url(action, authorization), post.to_json, headers)
Response.new(
success_from(action, response),
message_from(response),
response,
authorization: authorization_from(action, response),
test: test?,
error_code: error_code_from(action, response)
)
rescue ResponseError => e
response = parse(e.response.body)
# if current access_token is invalid then clean it
if e.response.code == '401'
@options[:access_token] = ''
@options[:new_credentials] = true
end
Response.new(false, message_from(response), response, test: test?)
end
def success_from(action, response)
case action
when :store then response.dig(:transaction, :payment_method, :token).present?
when :inquire then response[:id].present? && SUCCESS_MESSAGES.include?(response[:statusName])
else
response[:success] && SUCCESS_MESSAGES.include?(response[:status])
end
end
def message_from(response)
response[:title] || response[:responseMessage] || response[:statusName] || response[:status]
end
def authorization_from(action, response)
action == :store ? response.dig(:transaction, :payment_method, :token) : response[:orderId]
end
def error_code_from(action, response)
(response[:statusName] || response[:status]) unless success_from(action, response)
end
def cast_bool(value)
![false, 0, '', '0', 'f', 'F', 'false', 'FALSE'].include?(value)
end
end
end
end