lib/active_merchant/billing/gateways/qvalent.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class QvalentGateway < Gateway
self.display_name = 'Qvalent'
self.homepage_url = 'https://www.qvalent.com/'
self.test_url = 'https://ccapi.client.support.qvalent.com/post/CreditCardAPIReceiver'
self.live_url = 'https://ccapi.client.qvalent.com/post/CreditCardAPIReceiver'
self.supported_countries = ['AU']
self.default_currency = 'AUD'
self.money_format = :cents
self.supported_cardtypes = %i[visa master american_express discover jcb diners]
CVV_CODE_MAPPING = {
'S' => 'D'
}
def initialize(options = {})
requires!(options, :username, :password, :merchant, :pem)
super
end
def purchase(amount, payment_method, options = {})
post = {}
add_invoice(post, amount, options)
add_order_number(post, options)
add_payment_method(post, payment_method)
add_verification_value(post, payment_method)
add_stored_credential_data(post, payment_method, options)
add_customer_data(post, options)
add_soft_descriptors(post, options)
add_customer_reference(post, options)
commit('capture', post)
end
def authorize(amount, payment_method, options = {})
post = {}
add_invoice(post, amount, options)
add_order_number(post, options)
add_payment_method(post, payment_method)
add_verification_value(post, payment_method)
add_stored_credential_data(post, payment_method, options)
add_customer_data(post, options)
add_soft_descriptors(post, options)
add_customer_reference(post, options)
commit('preauth', post)
end
def capture(amount, authorization, options = {})
post = {}
add_invoice(post, amount, options)
add_reference(post, authorization, options)
add_customer_data(post, options)
add_soft_descriptors(post, options)
add_customer_reference(post, options)
commit('captureWithoutAuth', post)
end
def refund(amount, authorization, options = {})
post = {}
add_invoice(post, amount, options)
add_reference(post, authorization, options)
add_customer_data(post, options)
add_soft_descriptors(post, options)
post['order.ECI'] = options[:eci] || 'SSL'
add_customer_reference(post, options)
commit('refund', post)
end
# Credit requires the merchant account to be enabled for "Adhoc Refunds"
def credit(amount, payment_method, options = {})
post = {}
add_invoice(post, amount, options)
add_order_number(post, options)
add_payment_method(post, payment_method)
add_customer_data(post, options)
add_soft_descriptors(post, options)
add_customer_reference(post, options)
commit('refund', post)
end
def void(authorization, options = {})
post = {}
add_reference(post, authorization, options)
add_customer_data(post, options)
add_soft_descriptors(post, options)
add_customer_reference(post, options)
commit('reversal', post)
end
def store(payment_method, options = {})
post = {}
add_payment_method(post, payment_method)
add_card_reference(post, options)
commit('registerAccount', post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((&?customer.password=)[^&]*), '\1[FILTERED]').
gsub(%r((&?card.PAN=)[^&]*), '\1[FILTERED]').
gsub(%r((&?card.CVN=)[^&]*), '\1[FILTERED]')
end
private
CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") }
CURRENCY_CODES['AUD'] = 'AUD'
CURRENCY_CODES['INR'] = 'INR'
def add_soft_descriptors(post, options)
post['customer.merchantName'] = options[:customer_merchant_name] if options[:customer_merchant_name]
post['customer.merchantStreetAddress'] = options[:customer_merchant_street_address] if options[:customer_merchant_street_address]
post['customer.merchantLocation'] = options[:customer_merchant_location] if options[:customer_merchant_location]
post['customer.merchantState'] = options[:customer_merchant_state] if options[:customer_merchant_state]
post['customer.merchantCountry'] = options[:customer_merchant_country] if options[:customer_merchant_country]
post['customer.merchantPostCode'] = options[:customer_merchant_post_code] if options[:customer_merchant_post_code]
post['customer.subMerchantId'] = options[:customer_sub_merchant_id] if options[:customer_sub_merchant_id]
end
def add_invoice(post, money, options)
post['order.amount'] = amount(money)
post['card.currency'] = CURRENCY_CODES[options[:currency] || currency(money)]
end
def add_payment_method(post, payment_method)
post['card.cardHolderName'] = payment_method.name
post['card.PAN'] = payment_method.number
post['card.expiryYear'] = format(payment_method.year, :two_digits)
post['card.expiryMonth'] = format(payment_method.month, :two_digits)
end
def add_stored_credential_data(post, payment_method, options)
post['order.ECI'] = options[:eci] || eci(options)
if (stored_credential = options[:stored_credential]) && %w(visa master).include?(payment_method.brand)
post['card.posEntryMode'] = stored_credential[:initial_transaction] ? 'MANUAL' : 'STORED_CREDENTIAL'
stored_credential_usage(post, payment_method, options) unless stored_credential[:initiator] && stored_credential[:initiator] == 'cardholder'
post['order.authTraceId'] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id]
end
end
def stored_credential_usage(post, payment_method, options)
return unless payment_method.brand == 'visa'
stored_credential = options[:stored_credential]
if stored_credential[:reason_type] == 'unscheduled'
if stored_credential[:initiator] == 'merchant'
post['card.storedCredentialUsage'] = 'UNSCHEDULED_MIT'
elsif stored_credential[:initiator] == 'customer'
post['card.storedCredentialUsage'] = 'UNSCHEDULED_CIT'
end
elsif stored_credential[:reason_type] == 'recurring'
post['card.storedCredentialUsage'] = 'RECURRING'
elsif stored_credential[:reason_type] == 'installment'
post['card.storedCredentialUsage'] = 'INSTALLMENT'
end
end
def eci(options)
if options.dig(:stored_credential, :initial_transaction)
'SSL'
elsif options.dig(:stored_credential, :initiator) && options[:stored_credential][:initiator] == 'cardholder'
'MTO'
elsif options.dig(:stored_credential, :reason_type)
case options[:stored_credential][:reason_type]
when 'recurring'
'REC'
when 'installment'
'INS'
when 'unscheduled'
'MTO'
end
else
'SSL'
end
end
def add_verification_value(post, payment_method)
post['card.CVN'] = payment_method.verification_value
end
def add_card_reference(post, options)
post['customer.customerReferenceNumber'] = options[:customer_reference_number] || options[:order_id]
end
def add_customer_reference(post, options)
post['customer.customerReferenceNumber'] = options[:customer_reference_number] if options[:customer_reference_number]
end
def add_reference(post, authorization, options)
post['customer.originalOrderNumber'] = authorization
add_order_number(post, options)
end
def add_order_number(post, options)
post['customer.orderNumber'] = options[:order_id] || SecureRandom.uuid
end
def add_customer_data(post, options)
post['order.ipAddress'] = options[:ip] || '127.0.0.1'
post['order.xid'] = options[:xid] if options[:xid]
post['order.cavv'] = options[:cavv] if options[:cavv]
end
def commit(action, post)
post['customer.username'] = @options[:username]
post['customer.password'] = @options[:password]
post['customer.merchant'] = @options[:merchant]
post['order.type'] = action
data = build_request(post)
raw = parse(ssl_post(url(action), data, headers))
succeeded = success_from(raw['response.responseCode'])
Response.new(
succeeded,
message_from(succeeded, raw),
raw,
authorization: raw['response.orderNumber'] || raw['response.customerReferenceNumber'],
cvv_result: cvv_result(succeeded, raw),
error_code: error_code_from(succeeded, raw),
test: test?
)
end
def cvv_result(succeeded, raw)
return unless succeeded
code = CVV_CODE_MAPPING[raw['response.cvnResponse']] || raw['response.cvnResponse']
CVVResult.new(code)
end
def headers
{
'Content-Type' => 'application/x-www-form-urlencoded'
}
end
def build_request(post)
post.to_query + '&message.end'
end
def url(action)
(test? ? test_url : live_url)
end
def parse(body)
result = {}
body.to_s.each_line do |pair|
result[$1] = $2 if pair.strip =~ /\A([^=]+)=(.+)\Z/im
end
result
end
def parse_element(response, node)
if node.has_elements?
node.elements.each { |element| parse_element(response, element) }
else
response[node.name.underscore.to_sym] = node.text
end
end
SUCCESS_CODES = %w(00 08 10 11 16 QS QZ)
def success_from(response)
SUCCESS_CODES.include?(response)
end
def message_from(succeeded, response)
if succeeded
'Succeeded'
else
response['response.text'] || 'Unable to read error message'
end
end
STANDARD_ERROR_CODE_MAPPING = {
'14' => STANDARD_ERROR_CODE[:invalid_number],
'QQ' => STANDARD_ERROR_CODE[:invalid_cvc],
'33' => STANDARD_ERROR_CODE[:expired_card],
'NT' => STANDARD_ERROR_CODE[:incorrect_address],
'12' => STANDARD_ERROR_CODE[:card_declined],
'06' => STANDARD_ERROR_CODE[:processing_error],
'01' => STANDARD_ERROR_CODE[:call_issuer],
'04' => STANDARD_ERROR_CODE[:pickup_card]
}
def error_code_from(succeeded, response)
succeeded ? nil : STANDARD_ERROR_CODE_MAPPING[response['response.responseCode']]
end
end
end
end