class ActiveMerchant::Billing::CyberSourceRestGateway

Constants

CREDIT_CARD_CODES
NT_PAYMENT_SOLUTION
WALLET_PAYMENT_SOLUTION

Public Class Methods

new(options = {}) click to toggle source
Calls superclass method ActiveMerchant::Billing::Gateway::new
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 45
def initialize(options = {})
  requires!(options, :merchant_id, :public_key, :private_key)
  super
end

Public Instance Methods

authorize(money, payment, options = {}, capture = false) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 54
def authorize(money, payment, options = {}, capture = false)
  post = build_auth_request(money, payment, options)
  post[:processingInformation][:capture] = true if capture

  commit('payments', post, options)
end
capture(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 61
def capture(money, authorization, options = {})
  payment = authorization.split('|').first
  post = build_reference_request(money, options)

  commit("payments/#{payment}/captures", post, options)
end
credit(money, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 74
def credit(money, payment, options = {})
  post = build_credit_request(money, payment, options)
  commit('credits', post)
end
purchase(money, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 50
def purchase(money, payment, options = {})
  authorize(money, payment, options, true)
end
refund(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 68
def refund(money, authorization, options = {})
  payment = authorization.split('|').first
  post = build_reference_request(money, options)
  commit("payments/#{payment}/refunds", post, options)
end
scrub(transcript) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 97
def scrub(transcript)
  transcript.
    gsub(/(\\?"number\\?":\\?")\d+/, '\1[FILTERED]').
    gsub(/(\\?"routingNumber\\?":\\?")\d+/, '\1[FILTERED]').
    gsub(/(\\?"securityCode\\?":\\?")\d+/, '\1[FILTERED]').
    gsub(/(\\?"cryptogram\\?":\\?")[^<]+/, '\1[FILTERED]').
    gsub(/(signature=")[^"]*/, '\1[FILTERED]').
    gsub(/(keyid=")[^"]*/, '\1[FILTERED]').
    gsub(/(Digest: SHA-256=)[\w\/\+=]*/, '\1[FILTERED]')
end
supports_scrubbing?() click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 93
def supports_scrubbing?
  true
end
verify(credit_card, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 85
def verify(credit_card, options = {})
  amount = eligible_for_zero_auth?(credit_card, options) ? 0 : 100
  MultiResponse.run(:use_first_response) do |r|
    r.process { authorize(amount, 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/cyber_source_rest.rb, line 79
def void(authorization, options = {})
  payment, amount = authorization.split('|')
  post = build_void_request(amount)
  commit("payments/#{payment}/reversals", post)
end

Private Instance Methods

add_ach(post, payment) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 223
def add_ach(post, payment)
  post[:paymentInformation][:bank] = {
    account: {
      type: payment.account_type == 'checking' ? 'C' : 'S',
      number: payment.account_number
    },
    routingNumber: payment.routing_number
  }
end
add_address(post, payment_method, address, options, address_type) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 282
def add_address(post, payment_method, address, options, address_type)
  return unless address.present?

  first_name, last_name = address_names(address[:name], payment_method)

  post[:orderInformation][address_type] = {
    firstName:             first_name,
    lastName:              last_name,
    address1:              address[:address1],
    address2:              address[:address2],
    locality:              address[:city],
    administrativeArea:    address[:state],
    postalCode:            address[:zip],
    country:               lookup_country_code(address[:country])&.value,
    email:                 options[:email].presence || 'null@cybersource.com',
    phoneNumber:           address[:phone]
    # merchantTaxID:         ship_to ? options[:merchant_tax_id] : nil,
    # company:               address[:company],
    # companyTaxID:          address[:companyTaxID],
    # ipAddress:             options[:ip],
    # driversLicenseNumber:  options[:drivers_license_number],
    # driversLicenseState:   options[:drivers_license_state],
  }.compact
end
add_amount(post, amount, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 215
def add_amount(post, amount, options)
  currency = options[:currency] || currency(amount)
  post[:orderInformation][:amountDetails] = {
    totalAmount: localized_amount(amount, currency),
    currency: currency
  }
end
add_authorization_options(post, payment, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 334
def add_authorization_options(post, payment, options)
  initiator = options.dig(:stored_credential, :initiator) == 'cardholder' ? 'customer' : 'merchant'
  authorization_options = {
    authorizationOptions: {
      initiator: {
        type: initiator
      }
    }
  }.compact

  authorization_options[:authorizationOptions][:initiator][:storedCredentialUsed] = true if initiator == 'merchant'
  authorization_options[:authorizationOptions][:initiator][:credentialStoredOnFile] = true if options.dig(:stored_credential, :initial_transaction)
  authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction] ||= {}
  unless options.dig(:stored_credential, :initial_transaction)
    network_transaction_id = options[:network_transaction_id] || options.dig(:stored_credential, :network_transaction_id) || ''
    authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:previousTransactionID] = network_transaction_id
    authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:originalAuthorizedAmount] = post.dig(:orderInformation, :amountDetails, :totalAmount) if card_brand(payment) == 'discover'
  end
  authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:reason] = options[:reason_code] if options[:reason_code]
  post[:processingInformation].merge!(authorization_options)
end
add_business_rules_data(post, payment, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 457
def add_business_rules_data(post, payment, options)
  post[:processingInformation][:authorizationOptions] = {}
  post[:processingInformation][:authorizationOptions][:ignoreAvsResult] = 'true' if options[:ignore_avs].to_s == 'true'
  post[:processingInformation][:authorizationOptions][:ignoreCvResult] = 'true' if options[:ignore_cvv].to_s == 'true'
end
add_code(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 195
def add_code(post, options)
  return unless options[:order_id].present?

  post[:clientReferenceInformation][:code] = options[:order_id]
end
add_credit_card(post, creditcard) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 272
def add_credit_card(post, creditcard)
  post[:paymentInformation][:card] = {
    number: creditcard.number,
    expirationMonth: format(creditcard.month, :two_digits),
    expirationYear: format(creditcard.year, :four_digits),
    securityCode: creditcard.verification_value,
    type: CREDIT_CARD_CODES[card_brand(creditcard).to_sym]
  }
end
add_customer_id(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 201
def add_customer_id(post, options)
  return unless options[:customer_id].present?

  post[:paymentInformation][:customer] = { customerId: options[:customer_id] }
end
add_invoice_number(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 484
def add_invoice_number(post, options)
  return unless options[:invoice_number].present?

  post[:orderInformation][:invoiceDetails] ||= {}
  post[:orderInformation][:invoiceDetails][:invoiceNumber] = options[:invoice_number]
end
add_level_2_data(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 110
def add_level_2_data(post, options)
  return unless options[:purchase_order_number]

  post[:orderInformation][:invoiceDetails] ||= {}
  post[:orderInformation][:invoiceDetails][:purchaseOrderNumber] = options[:purchase_order_number]
end
add_level_3_data(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 117
def add_level_3_data(post, options)
  return unless options[:line_items]

  post[:orderInformation][:lineItems] = options[:line_items]
  post[:processingInformation][:purchaseLevel] = '3'
  post[:orderInformation][:shipping_details] = { shipFromPostalCode: options[:ships_from_postal_code] }
  post[:orderInformation][:amountDetails] ||= {}
  post[:orderInformation][:amountDetails][:discountAmount] = options[:discount_amount]
end
add_mdd_fields(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 463
def add_mdd_fields(post, options)
  mdd_fields = options.select { |k, v| k.to_s.start_with?('mdd_field') && v.present? }
  return unless mdd_fields.present?

  post[:merchantDefinedInformation] = mdd_fields.map do |key, value|
    { key: key, value: value }
  end
end
add_merchant_description(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 307
def add_merchant_description(post, options)
  return unless options[:merchant_descriptor_name] || options[:merchant_descriptor_address1] || options[:merchant_descriptor_locality]

  merchant = post[:merchantInformation][:merchantDescriptor] = {}
  merchant[:name] = options[:merchant_descriptor_name] if options[:merchant_descriptor_name]
  merchant[:address1] = options[:merchant_descriptor_address1] if options[:merchant_descriptor_address1]
  merchant[:locality] = options[:merchant_descriptor_locality] if options[:merchant_descriptor_locality]
end
add_network_tokenization_card(post, payment, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 244
def add_network_tokenization_card(post, payment, options)
  post[:processingInformation][:commerceIndicator] = 'internet' unless options[:stored_credential] || card_brand(payment) == 'jcb'

  post[:paymentInformation][:tokenizedCard] = {
    number: payment.number,
    expirationMonth: payment.month,
    expirationYear: payment.year,
    cryptogram: payment.payment_cryptogram,
    type:  CREDIT_CARD_CODES[card_brand(payment).to_sym],
    transactionType: payment.source == :network_token ? '3' : '1'
  }

  if payment.source == :network_token && NT_PAYMENT_SOLUTION[payment.brand]
    post[:processingInformation][:paymentSolution] = NT_PAYMENT_SOLUTION[payment.brand]
  else
    # Apple Pay / Google Pay
    post[:processingInformation][:paymentSolution] = WALLET_PAYMENT_SOLUTION[payment.source]
    if card_brand(payment) == 'master'
      post[:consumerAuthenticationInformation] = {
        ucafAuthenticationData: payment.payment_cryptogram,
        ucafCollectionIndicator: '2'
      }
    else
      post[:consumerAuthenticationInformation] = { cavv: payment.payment_cryptogram }
    end
  end
end
add_partner_solution_id(post) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 491
def add_partner_solution_id(post)
  return unless application_id

  post[:clientReferenceInformation][:partner] = { solutionId: application_id }
end
add_payment(post, payment, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 233
def add_payment(post, payment, options)
  post[:processingInformation] = {}
  if payment.is_a?(NetworkTokenizationCreditCard)
    add_network_tokenization_card(post, payment, options)
  elsif payment.is_a?(Check)
    add_ach(post, payment)
  else
    add_credit_card(post, payment)
  end
end
add_reconciliation_id(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 472
def add_reconciliation_id(post, options)
  return unless options[:reconciliation_id].present?

  post[:clientReferenceInformation][:reconciliationId] = options[:reconciliation_id]
end
add_reversal_amount(post, amount) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 207
def add_reversal_amount(post, amount)
  currency = options[:currency] || currency(amount)

  post[:reversalInformation][:amountDetails] = {
    totalAmount: localized_amount(amount, currency)
  }
end
add_sec_code(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 478
def add_sec_code(post, options)
  return unless options[:sec_code].present?

  post[:processingInformation][:bankTransferOptions] = { secCode: options[:sec_code] }
end
add_stored_credentials(post, payment, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 316
def add_stored_credentials(post, payment, options)
  return unless options[:stored_credential]

  post[:processingInformation][:commerceIndicator] = commerce_indicator(options.dig(:stored_credential, :reason_type))
  add_authorization_options(post, payment, options)
end
add_three_ds(post, payment_method, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 127
def add_three_ds(post, payment_method, options)
  return unless three_d_secure = options[:three_d_secure]

  post[:consumerAuthenticationInformation] ||= {}
  if payment_method.brand == 'master'
    post[:consumerAuthenticationInformation][:ucafAuthenticationData] = three_d_secure[:cavv]
    post[:consumerAuthenticationInformation][:ucafCollectionIndicator] = '2'
  else
    post[:consumerAuthenticationInformation][:cavv] = three_d_secure[:cavv]
  end
  post[:consumerAuthenticationInformation][:cavvAlgorithm] = three_d_secure[:cavv_algorithm] if three_d_secure[:cavv_algorithm]
  post[:consumerAuthenticationInformation][:paSpecificationVersion] = three_d_secure[:version] if three_d_secure[:version]
  post[:consumerAuthenticationInformation][:directoryServerTransactionID] = three_d_secure[:ds_transaction_id] if three_d_secure[:ds_transaction_id]
  post[:consumerAuthenticationInformation][:eciRaw] = three_d_secure[:eci] if three_d_secure[:eci]
  if three_d_secure[:xid].present?
    post[:consumerAuthenticationInformation][:xid] = three_d_secure[:xid]
  else
    post[:consumerAuthenticationInformation][:xid] = three_d_secure[:cavv]
  end
  post[:consumerAuthenticationInformation][:veresEnrolled] = three_d_secure[:enrolled] if three_d_secure[:enrolled]
  post[:consumerAuthenticationInformation][:paresStatus] = three_d_secure[:authentication_response_status] if three_d_secure[:authentication_response_status]
  post
end
auth_headers(action, options, post, http_method = 'post') click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 442
def auth_headers(action, options, post, http_method = 'post')
  digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present?
  date = Time.now.httpdate

  {
    'Accept' => 'application/hal+json;charset=utf-8',
    'Content-Type' => 'application/json;charset=utf-8',
    'V-C-Merchant-Id' => options[:merchant_id] || @options[:merchant_id],
    'Date' => date,
    'Host' => host,
    'Signature' => get_http_signature(action, digest, http_method, date),
    'Digest' => digest
  }
end
authorization_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 404
def authorization_from(response)
  id = response['id']
  has_amount = response['orderInformation'] && response['orderInformation']['amountDetails'] && response['orderInformation']['amountDetails']['authorizedAmount']
  amount = response['orderInformation']['amountDetails']['authorizedAmount'].delete('.') if has_amount

  return id if amount.blank?

  [id, amount].join('|')
end
build_auth_request(amount, payment, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 157
def build_auth_request(amount, payment, options)
  { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post|
    add_customer_id(post, options)
    add_code(post, options)
    add_payment(post, payment, options)
    add_mdd_fields(post, options)
    add_amount(post, amount, options)
    add_address(post, payment, options[:billing_address], options, :billTo)
    add_address(post, payment, options[:shipping_address], options, :shipTo)
    add_business_rules_data(post, payment, options)
    add_partner_solution_id(post)
    add_stored_credentials(post, payment, options)
    add_three_ds(post, payment, options)
    add_level_2_data(post, options)
    add_level_3_data(post, options)
  end.compact
end
build_credit_request(amount, payment, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 184
def build_credit_request(amount, payment, options)
  { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post|
    add_code(post, options)
    add_credit_card(post, payment)
    add_mdd_fields(post, options)
    add_amount(post, amount, options)
    add_address(post, payment, options[:billing_address], options, :billTo)
    add_merchant_description(post, options)
  end.compact
end
build_reference_request(amount, options) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 175
def build_reference_request(amount, options)
  { clientReferenceInformation: {}, orderInformation: {} }.tap do |post|
    add_code(post, options)
    add_mdd_fields(post, options)
    add_amount(post, amount, options)
    add_partner_solution_id(post)
  end.compact
end
build_void_request(amount = nil) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 151
def build_void_request(amount = nil)
  { reversalInformation: { amountDetails: { totalAmount: nil } } }.tap do |post|
    add_reversal_amount(post, amount.to_i) if amount.present?
  end.compact
end
commerce_indicator(reason_type) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 323
def commerce_indicator(reason_type)
  case reason_type
  when 'recurring'
    'recurring'
  when 'installment'
    'install'
  else
    'internet'
  end
end
commit(action, post, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 372
def commit(action, post, options = {})
  add_reconciliation_id(post, options)
  add_sec_code(post, options)
  add_invoice_number(post, options)
  response = parse(ssl_post(url(action), post.to_json, auth_headers(action, options, post)))
  Response.new(
    success_from(response),
    message_from(response),
    response,
    authorization: authorization_from(response),
    avs_result: AVSResult.new(code: response.dig('processorInformation', 'avs', 'code')),
    # cvv_result: CVVResult.new(response['some_cvv_response_key']),
    network_transaction_id: network_transaction_id_from(response),
    test: test?,
    error_code: error_code_from(response)
  )
rescue ActiveMerchant::ResponseError => e
  response = e.response.body.present? ? parse(e.response.body) : { 'response' => { 'rmsg' => e.response.msg } }
  message = response.dig('response', 'rmsg') || response.dig('message')
  Response.new(false, message, response, test: test?)
end
error_code_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 414
def error_code_from(response)
  response['errorInformation']['reason'] unless success_from(response)
end
get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Time.now.httpdate) click to toggle source

This implementation follows the Cybersource guide on how create the request signature, see: developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/GenerateHeader/httpSignatureAuthentication.html

# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 420
def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Time.now.httpdate)
  string_to_sign = {
    host: host,
    date: gmtdatetime,
    "request-target": "#{http_method} /pts/v2/#{resource}",
    digest: digest,
    "v-c-merchant-id": @options[:merchant_id]
  }.map { |k, v| "#{k}: #{v}" }.join("\n").force_encoding(Encoding::UTF_8)

  {
    keyid: @options[:public_key],
    algorithm: 'HmacSHA256',
    headers: "host date request-target#{digest.present? ? ' digest' : ''} v-c-merchant-id",
    signature: sign_payload(string_to_sign)
  }.map { |k, v| %{#{k}="#{v}"} }.join(', ')
end
host() click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 364
def host
  URI.parse(url('')).host
end
message_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 398
def message_from(response)
  return response['status'] if success_from(response)

  response['errorInformation']['message'] || response['message']
end
network_transaction_id_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 356
def network_transaction_id_from(response)
  response.dig('processorInformation', 'networkTransactionId')
end
parse(body) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 368
def parse(body)
  JSON.parse(body)
end
sign_payload(payload) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 437
def sign_payload(payload)
  decoded_key = Base64.decode64(@options[:private_key])
  Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload))
end
success_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 394
def success_from(response)
  %w(AUTHORIZED PENDING REVERSED).include?(response['status'])
end
url(action) click to toggle source
# File lib/active_merchant/billing/gateways/cyber_source_rest.rb, line 360
def url(action)
  "#{test? ? test_url : live_url}/pts/v2/#{action}"
end