class ActiveMerchant::Billing::AirwallexGateway

Constants

ENDPOINTS
TEST_NETWORK_TRANSACTION_IDS

Provided by Airwallex for testing purposes

Public Class Methods

new(options = {}) click to toggle source
Calls superclass method ActiveMerchant::Billing::Gateway::new
# File lib/active_merchant/billing/gateways/airwallex.rb, line 30
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

Public Instance Methods

authorize(money, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 54
def authorize(money, payment, options = {})
  # authorize is just a purchase w/o an auto capture
  purchase(money, payment, options.merge({ auto_capture: false }))
end
capture(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 59
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
purchase(money, card, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 38
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
refund(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 72
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
scrub(transcript) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 106
def scrub(transcript)
  transcript.
    gsub(/(\\\"number\\\":\\\")\d+/, '\1[REDACTED]').
    gsub(/(\\\"cvc\\\":\\\")\d+/, '\1[REDACTED]')
end
supports_scrubbing?() click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 102
def supports_scrubbing?
  true
end
verify(credit_card, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 95
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
void(authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 84
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

Private Instance Methods

add_billing(post, card, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 179
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
add_card(post, card, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 222
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
add_descriptor(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 328
def add_descriptor(post, options)
  post[:descriptor] = options[:description] if options[:description]
end
add_invoice(post, money, options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 217
def add_invoice(post, money, options)
  post[:amount] = amount(money)
  post[:currency] = (options[:currency] || currency(money))
end
add_order(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 237
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
add_referrer_data(post) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 159
def add_referrer_data(post)
  post[:referrer_data] = { type: 'spreedly' }
end
add_return_url(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 122
def add_return_url(post, options)
  post[:return_url] = options[:return_url] if options[:return_url]
end
add_stored_credential(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 260
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
add_three_ds(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 289
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
authorization_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 375
def authorization_from(response)
  response.dig('latest_payment_attempt', 'payment_intent_id')
end
authorization_only?(options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 324
def authorization_only?(options = {})
  options.include?(:auto_capture) && options[:auto_capture] == false
end
build_address(address) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 205
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
build_request_url(action, id = nil) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 152
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
build_shipping_address(shipping_address) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 250
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
commit(action, post, id = nil) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 336
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
create_payment_intent(money, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 163
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
error_code_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 379
def error_code_from(response)
  response['provider_original_response_code'] || response['code'] unless success_from(response)
end
format_three_ds_version(three_d_secure) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 302
def format_three_ds_version(three_d_secure)
  version = three_d_secure[:version].split('.')

  version.push('0') until version.length == 3
  version.join('.')
end
generate_uuid() click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 126
def generate_uuid
  SecureRandom.uuid
end
handle_response(response) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 354
def handle_response(response)
  case response.code.to_i
  when 200...300, 400, 404
    response.body
  else
    raise ResponseError.new(response)
  end
end
has_name_info?(card) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 193
def has_name_info?(card)
  # These fields are required if billing data is sent.
  card.first_name && card.last_name
end
has_required_address_info?(address) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 198
def has_required_address_info?(address)
  # These fields are required if address data is sent.
  return unless address

  address[:address1] && address[:country]
end
merchant_order_id(options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 118
def merchant_order_id(options)
  options[:merchant_order_id] || options[:order_id] || generate_uuid
end
message_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 371
def message_from(response)
  response.dig('latest_payment_attempt', 'status') || response['status'] || response['message']
end
parse(body) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 332
def parse(body)
  JSON.parse(body)
end
post_data(post) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 363
def post_data(post)
  post.to_json
end
request_id(options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 114
def request_id(options)
  options[:request_id] || generate_uuid
end
setup_access_token() click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 130
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
success_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 367
def success_from(response)
  %w(REQUIRES_PAYMENT_METHOD SUCCEEDED RECEIVED REQUIRES_CAPTURE CANCELLED).include?(response['status'])
end
test_mit?(options) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 285
def test_mit?(options)
  test? && options.dig(:stored_credential, :initiator) == 'merchant'
end
test_network_transaction_id(post) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 276
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
three_ds_version_specific_fields(three_d_secure) click to toggle source
# File lib/active_merchant/billing/gateways/airwallex.rb, line 309
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