lib/active_merchant/billing/gateways/kushki.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class KushkiGateway < Gateway
self.display_name = 'Kushki'
self.homepage_url = 'https://www.kushkipagos.com'
self.test_url = 'https://api-uat.kushkipagos.com/'
self.live_url = 'https://api.kushkipagos.com/'
self.supported_countries = %w[BR CL CO EC MX PE]
self.default_currency = 'USD'
self.money_format = :dollars
self.supported_cardtypes = %i[visa master american_express discover diners_club alia]
def initialize(options = {})
requires!(options, :public_merchant_id, :private_merchant_id)
super
end
def purchase(amount, payment_method, options = {})
MultiResponse.run() do |r|
r.process { tokenize(amount, payment_method, options) }
r.process { charge(amount, r.authorization, options, payment_method) }
end
end
def authorize(amount, payment_method, options = {})
MultiResponse.run() do |r|
r.process { tokenize(amount, payment_method, options) }
r.process { preauthorize(amount, r.authorization, options, payment_method) }
end
end
def capture(amount, authorization, options = {})
action = 'capture'
post = {}
post[:ticketNumber] = authorization
add_invoice(action, post, amount, options)
add_full_response(post, options)
commit(action, post)
end
def refund(amount, authorization, options = {})
action = 'refund'
post = {}
post[:ticketNumber] = authorization
add_full_response(post, options)
add_invoice(action, post, amount, options)
commit(action, post, options)
end
def void(authorization, options = {})
action = 'void'
post = {}
post[:ticketNumber] = authorization
add_full_response(post, options)
commit(action, post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((Private-Merchant-Id: )\d+), '\1[FILTERED]').
gsub(%r((\"card\\\":{\\\"number\\\":\\\")\d+), '\1[FILTERED]').
gsub(%r((\"cvv\\\":\\\")\d+), '\1[FILTERED]')
end
private
def tokenize(amount, payment_method, options)
action = 'tokenize'
post = {}
add_invoice(action, post, amount, options)
add_payment_method(post, payment_method, options)
add_full_response(post, options)
add_metadata(post, options)
add_months(post, options)
add_deferred(post, options)
commit(action, post)
end
def charge(amount, authorization, options, payment_method = {})
action = 'charge'
post = {}
add_reference(post, authorization, options)
add_invoice(action, post, amount, options)
add_contact_details(post, options[:contact_details]) if options[:contact_details]
add_full_response(post, options)
add_metadata(post, options)
add_months(post, options)
add_deferred(post, options)
add_three_d_secure(post, payment_method, options)
add_product_details(post, options)
commit(action, post)
end
def preauthorize(amount, authorization, options, payment_method = {})
action = 'preAuthorization'
post = {}
add_reference(post, authorization, options)
add_invoice(action, post, amount, options)
add_full_response(post, options)
add_metadata(post, options)
add_months(post, options)
add_deferred(post, options)
add_three_d_secure(post, payment_method, options)
commit(action, post)
end
def add_invoice(action, post, money, options)
if action == 'tokenize'
post[:totalAmount] = amount(money).to_f
post[:currency] = options[:currency] || currency(money)
post[:isDeferred] = false
else
sum = {}
sum[:currency] = options[:currency] || currency(money)
add_amount_defaults(sum, money, options)
add_amount_by_country(sum, options)
post[:amount] = sum
end
end
def add_amount_defaults(sum, money, options)
sum[:subtotalIva] = 0
sum[:iva] = 0
sum[:subtotalIva0] = amount(money).to_f
sum[:ice] = 0 if sum[:currency] != 'COP'
end
def add_amount_by_country(sum, options)
if amount = options[:amount]
sum[:subtotalIva] = amount[:subtotal_iva].to_f if amount[:subtotal_iva]
sum[:iva] = amount[:iva].to_f if amount[:iva]
sum[:subtotalIva0] = amount[:subtotal_iva_0].to_f if amount[:subtotal_iva_0]
sum[:ice] = amount[:ice].to_f if amount[:ice]
if (extra_taxes = amount[:extra_taxes])
sum[:extraTaxes] ||= Hash.new
sum[:extraTaxes][:propina] = extra_taxes[:propina].to_f if extra_taxes[:propina]
sum[:extraTaxes][:tasaAeroportuaria] = extra_taxes[:tasa_aeroportuaria].to_f if extra_taxes[:tasa_aeroportuaria]
sum[:extraTaxes][:agenciaDeViaje] = extra_taxes[:agencia_de_viaje].to_f if extra_taxes[:agencia_de_viaje]
sum[:extraTaxes][:iac] = extra_taxes[:iac].to_f if extra_taxes[:iac]
end
end
end
def add_payment_method(post, payment_method, options)
card = {}
card[:number] = payment_method.number
card[:cvv] = payment_method.verification_value
card[:expiryMonth] = format(payment_method.month, :two_digits)
card[:expiryYear] = format(payment_method.year, :two_digits)
card[:name] = payment_method.name
post[:card] = card
end
def add_reference(post, authorization, options)
post[:token] = authorization
end
def add_contact_details(post, contact_details_options)
contact_details = {}
contact_details[:documentType] = contact_details_options[:document_type] if contact_details_options[:document_type]
contact_details[:documentNumber] = contact_details_options[:document_number] if contact_details_options[:document_number]
contact_details[:email] = contact_details_options[:email] if contact_details_options[:email]
contact_details[:firstName] = contact_details_options[:first_name] if contact_details_options[:first_name]
contact_details[:lastName] = contact_details_options[:last_name] if contact_details_options[:last_name]
contact_details[:secondLastName] = contact_details_options[:second_last_name] if contact_details_options[:second_last_name]
contact_details[:phoneNumber] = contact_details_options[:phone_number] if contact_details_options[:phone_number]
post[:contactDetails] = contact_details
end
def add_full_response(post, options)
# this is the only currently accepted value for this field, previously it was 'true'
post[:fullResponse] = 'v2' unless options[:full_response] == 'false' || options[:full_response].blank?
end
def add_metadata(post, options)
post[:metadata] = options[:metadata] if options[:metadata]
end
def add_months(post, options)
post[:months] = options[:months] if options[:months]
end
def add_deferred(post, options)
return unless options[:deferred_grace_months] && options[:deferred_credit_type] && options[:deferred_months]
post[:deferred] = {
graceMonths: options[:deferred_grace_months],
creditType: options[:deferred_credit_type],
months: options[:deferred_months]
}
end
def add_product_details(post, options)
return unless options[:product_details]
product_items_array = []
options[:product_details].each do |item|
product_items_obj = {}
product_items_obj[:id] = item[:id] if item[:id]
product_items_obj[:title] = item[:title] if item[:title]
product_items_obj[:price] = item[:price].to_i if item[:price]
product_items_obj[:sku] = item[:sku] if item[:sku]
product_items_obj[:quantity] = item[:quantity].to_i if item[:quantity]
product_items_array << product_items_obj
end
product_items = {
product: product_items_array
}
post[:productDetails] = product_items
end
def add_three_d_secure(post, payment_method, options)
three_d_secure = options[:three_d_secure]
return unless three_d_secure.present?
post[:threeDomainSecure] = {
eci: three_d_secure[:eci],
specificationVersion: three_d_secure[:version]
}
if payment_method.brand == 'master'
post[:threeDomainSecure][:acceptRisk] = three_d_secure[:eci] == '00'
post[:threeDomainSecure][:ucaf] = three_d_secure[:cavv]
post[:threeDomainSecure][:directoryServerTransactionID] = three_d_secure[:ds_transaction_id]
case three_d_secure[:eci]
when '07'
post[:threeDomainSecure][:collectionIndicator] = '0'
when '06'
post[:threeDomainSecure][:collectionIndicator] = '1'
else
post[:threeDomainSecure][:collectionIndicator] = '2'
end
elsif payment_method.brand == 'visa'
post[:threeDomainSecure][:acceptRisk] = three_d_secure[:eci] == '07'
post[:threeDomainSecure][:cavv] = three_d_secure[:cavv]
post[:threeDomainSecure][:xid] = three_d_secure[:xid] if three_d_secure[:xid].present?
else
raise ArgumentError.new 'Kushki supports 3ds2 authentication for only Visa and Mastercard brands.'
end
end
ENDPOINT = {
'tokenize' => 'tokens',
'charge' => 'charges',
'void' => 'charges',
'refund' => 'refund',
'preAuthorization' => 'preAuthorization',
'capture' => 'capture'
}
def commit(action, params, options = {})
response =
begin
parse(ssl_invoke(action, params, options))
rescue ResponseError => e
parse(e.response.body)
end
success = success_from(response)
Response.new(
success,
message_from(success, response),
response,
authorization: success ? authorization_from(response) : nil,
error_code: success ? nil : error_from(response),
test: test?
)
end
def ssl_invoke(action, params, options)
if %w[void refund].include?(action)
# removes ticketNumber from request for partial refunds because gateway will reject if included in request body
data = options[:partial_refund] == true ? post_data(params.except(:ticketNumber)) : nil
ssl_request(:delete, url(action, params), data, headers(action))
else
ssl_post(url(action, params), post_data(params), headers(action))
end
end
def headers(action)
hfields = {}
hfields['Public-Merchant-Id'] = @options[:public_merchant_id] if action == 'tokenize'
hfields['Private-Merchant-Id'] = @options[:private_merchant_id] unless action == 'tokenize'
hfields['Content-Type'] = 'application/json'
hfields
end
def post_data(params)
params.to_json
end
def url(action, params)
base_url = test? ? test_url : live_url
if %w[void refund].include?(action)
base_url + 'v1/' + ENDPOINT[action] + '/' + params[:ticketNumber].to_s
else
base_url + 'card/v1/' + ENDPOINT[action]
end
end
def parse(body)
JSON.parse(body)
rescue JSON::ParserError
message = 'Invalid JSON response received from KushkiGateway. Please contact KushkiGateway if you continue to receive this message.'
message += " (The raw response returned by the API was #{body.inspect})"
{
'message' => message
}
end
def success_from(response)
return true if response['token'] || response['ticketNumber'] || response['code'] == 'K000'
end
def message_from(succeeded, response)
if succeeded
'Succeeded'
else
response['message']
end
end
def authorization_from(response)
response['token'] || response['ticketNumber']
end
def error_from(response)
response['code']
end
end
end
end