lib/active_merchant/billing/gateways/airwallex.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class AirwallexGateway < Gateway
self.test_url = 'https://api-demo.airwallex.com/api/v1'
self.live_url = 'https://pci-api.airwallex.com/api/v1'
# per https://www.airwallex.com/docs/online-payments__overview, cards are accepted in all EU countries
self.supported_countries = %w[AT AU BE BG CY CZ DE DK EE GR ES FI FR GB HK HR HU IE IT LT LU LV MT NL PL PT RO SE SG SI SK]
self.default_currency = 'AUD'
self.supported_cardtypes = %i[visa master]
self.homepage_url = 'https://airwallex.com/'
self.display_name = 'Airwallex'
ENDPOINTS = {
login: '/authentication/login',
setup: '/pa/payment_intents/create',
sale: '/pa/payment_intents/%{id}/confirm',
capture: '/pa/payment_intents/%{id}/capture',
refund: '/pa/refunds/create',
void: '/pa/payment_intents/%{id}/cancel'
}
# Provided by Airwallex for testing purposes
TEST_NETWORK_TRANSACTION_IDS = {
visa: '123456789012345',
master: 'MCC123ABC0101'
}
def initialize(options = {})
requires!(options, :client_id, :client_api_key)
@client_id = options[:client_id]
@client_api_key = options[:client_api_key]
super
@access_token = options[:access_token] || setup_access_token
end
def purchase(money, card, options = {})
payment_intent_id = create_payment_intent(money, options)
post = {
'request_id' => request_id(options),
'merchant_order_id' => merchant_order_id(options)
}
add_card(post, card, options)
add_descriptor(post, options)
add_stored_credential(post, options)
add_return_url(post, options)
post['payment_method_options'] = { 'card' => { 'auto_capture' => false } } if authorization_only?(options)
add_three_ds(post, options)
commit(:sale, post, payment_intent_id)
end
def authorize(money, payment, options = {})
# authorize is just a purchase w/o an auto capture
purchase(money, payment, options.merge({ auto_capture: false }))
end
def capture(money, authorization, options = {})
raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
post = {
'request_id' => request_id(options),
'merchant_order_id' => merchant_order_id(options),
'amount' => amount(money)
}
add_descriptor(post, options)
commit(:capture, post, authorization)
end
def refund(money, authorization, options = {})
raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
post = {}
post[:amount] = amount(money)
post[:payment_intent_id] = authorization
post[:request_id] = request_id(options)
post[:merchant_order_id] = merchant_order_id(options)
commit(:refund, post)
end
def void(authorization, options = {})
raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
post = {}
post[:request_id] = request_id(options)
post[:merchant_order_id] = merchant_order_id(options)
add_descriptor(post, options)
commit(:void, post, authorization)
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
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(/(\\\"number\\\":\\\")\d+/, '\1[REDACTED]').
gsub(/(\\\"cvc\\\":\\\")\d+/, '\1[REDACTED]')
end
private
def request_id(options)
options[:request_id] || generate_uuid
end
def merchant_order_id(options)
options[:merchant_order_id] || options[:order_id] || generate_uuid
end
def add_return_url(post, options)
post[:return_url] = options[:return_url] if options[:return_url]
end
def generate_uuid
SecureRandom.uuid
end
def setup_access_token
token_headers = {
'Content-Type' => 'application/json',
'x-client-id' => @client_id,
'x-api-key' => @client_api_key
}
begin
raw_response = ssl_post(build_request_url(:login), nil, token_headers)
rescue ResponseError => e
raise OAuthResponseError.new(e)
else
response = JSON.parse(raw_response)
if (token = response['token'])
token
else
oauth_response = Response.new(false, response['message'])
raise OAuthResponseError.new(oauth_response)
end
end
end
def build_request_url(action, id = nil)
base_url = (test? ? test_url : live_url)
endpoint = ENDPOINTS[action].to_s
endpoint = id.present? ? endpoint % { id: id } : endpoint
base_url + endpoint
end
def add_referrer_data(post)
post[:referrer_data] = { type: 'spreedly' }
end
def create_payment_intent(money, options = {})
post = {}
add_invoice(post, money, options)
add_order(post, options)
post[:request_id] = "#{request_id(options)}_setup"
post[:merchant_order_id] = merchant_order_id(options)
add_referrer_data(post)
add_descriptor(post, options)
post['payment_method_options'] = { 'card' => { 'risk_control' => { 'three_ds_action' => 'SKIP_3DS' } } } if options[:skip_3ds]
response = commit(:setup, post)
raise ArgumentError.new(response.message) unless response.success?
response.params['id']
end
def add_billing(post, card, options = {})
return unless has_name_info?(card)
billing = post['payment_method']['card']['billing'] || {}
billing['email'] = options[:email] if options[:email]
billing['phone'] = options[:phone] if options[:phone]
billing['first_name'] = card.first_name
billing['last_name'] = card.last_name
billing_address = options[:billing_address]
billing['address'] = build_address(billing_address) if has_required_address_info?(billing_address)
post['payment_method']['card']['billing'] = billing
end
def has_name_info?(card)
# These fields are required if billing data is sent.
card.first_name && card.last_name
end
def has_required_address_info?(address)
# These fields are required if address data is sent.
return unless address
address[:address1] && address[:country]
end
def build_address(address)
return unless address
address_data = {} # names r hard
address_data[:country_code] = address[:country]
address_data[:street] = address[:address1]
address_data[:city] = address[:city] if address[:city] # required per doc, not in practice
address_data[:postcode] = address[:zip] if address[:zip]
address_data[:state] = address[:state] if address[:state]
address_data
end
def add_invoice(post, money, options)
post[:amount] = amount(money)
post[:currency] = (options[:currency] || currency(money))
end
def add_card(post, card, options = {})
post['payment_method'] = {
'type' => 'card',
'card' => {
'expiry_month' => format(card.month, :two_digits),
'expiry_year' => card.year.to_s,
'number' => card.number.to_s,
'name' => card.name,
'cvc' => card.verification_value,
'brand' => card.brand
}
}
add_billing(post, card, options)
end
def add_order(post, options)
return unless shipping_address = options[:shipping_address]
physical_address = build_shipping_address(shipping_address)
first_name, last_name = split_names(shipping_address[:name])
shipping = {}
shipping[:first_name] = first_name if first_name
shipping[:last_name] = last_name if last_name
shipping[:phone_number] = shipping_address[:phone_number] if shipping_address[:phone_number]
shipping[:address] = physical_address
post[:order] = { shipping: shipping }
end
def build_shipping_address(shipping_address)
address = {}
address[:city] = shipping_address[:city]
address[:country_code] = shipping_address[:country]
address[:postcode] = shipping_address[:zip]
address[:state] = shipping_address[:state]
address[:street] = shipping_address[:address1]
address
end
def add_stored_credential(post, options)
return unless stored_credential = options[:stored_credential]
external_recurring_data = post[:external_recurring_data] = {}
case stored_credential.dig(:reason_type)
when 'recurring', 'installment'
external_recurring_data[:merchant_trigger_reason] = 'scheduled'
when 'unscheduled'
external_recurring_data[:merchant_trigger_reason] = 'unscheduled'
end
external_recurring_data[:original_transaction_id] = test_mit?(options) ? test_network_transaction_id(post) : stored_credential.dig(:network_transaction_id)
external_recurring_data[:triggered_by] = stored_credential.dig(:initiator) == 'cardholder' ? 'customer' : 'merchant'
end
def test_network_transaction_id(post)
case post['payment_method']['card']['brand']
when 'visa'
TEST_NETWORK_TRANSACTION_IDS[:visa]
when 'master'
TEST_NETWORK_TRANSACTION_IDS[:master]
end
end
def test_mit?(options)
test? && options.dig(:stored_credential, :initiator) == 'merchant'
end
def add_three_ds(post, options)
return unless three_d_secure = options[:three_d_secure]
pm_options = post.dig('payment_method_options', 'card')
external_three_ds = {
version: format_three_ds_version(three_d_secure),
eci: three_d_secure[:eci]
}.merge(three_ds_version_specific_fields(three_d_secure))
pm_options ? pm_options.merge!(external_three_ds: external_three_ds) : post['payment_method_options'] = { card: { external_three_ds: external_three_ds } }
end
def format_three_ds_version(three_d_secure)
version = three_d_secure[:version].split('.')
version.push('0') until version.length == 3
version.join('.')
end
def three_ds_version_specific_fields(three_d_secure)
if three_d_secure[:version].to_f >= 2
{
authentication_value: three_d_secure[:cavv],
ds_transaction_id: three_d_secure[:ds_transaction_id],
three_ds_server_transaction_id: three_d_secure[:three_ds_server_trans_id]
}
else
{
cavv: three_d_secure[:cavv],
xid: three_d_secure[:xid]
}
end
end
def authorization_only?(options = {})
options.include?(:auto_capture) && options[:auto_capture] == false
end
def add_descriptor(post, options)
post[:descriptor] = options[:description] if options[:description]
end
def parse(body)
JSON.parse(body)
end
def commit(action, post, id = nil)
url = build_request_url(action, id)
post_headers = { 'Authorization' => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
response = parse(ssl_post(url, post_data(post), post_headers))
Response.new(
success_from(response),
message_from(response),
response,
authorization: authorization_from(response),
avs_result: AVSResult.new(code: response.dig('latest_payment_attempt', 'authentication_data', 'avs_result')),
cvv_result: CVVResult.new(response.dig('latest_payment_attempt', 'authentication_data', 'cvc_code')),
test: test?,
error_code: error_code_from(response)
)
end
def handle_response(response)
case response.code.to_i
when 200...300, 400, 404
response.body
else
raise ResponseError.new(response)
end
end
def post_data(post)
post.to_json
end
def success_from(response)
%w(REQUIRES_PAYMENT_METHOD SUCCEEDED RECEIVED REQUIRES_CAPTURE CANCELLED).include?(response['status'])
end
def message_from(response)
response.dig('latest_payment_attempt', 'status') || response['status'] || response['message']
end
def authorization_from(response)
response.dig('latest_payment_attempt', 'payment_intent_id')
end
def error_code_from(response)
response['provider_original_response_code'] || response['code'] unless success_from(response)
end
end
end
end