lib/active_merchant/billing/gateways/payu_latam.rb
require 'digest/md5'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class PayuLatamGateway < Gateway
self.display_name = 'PayU Latam'
self.homepage_url = 'http://www.payulatam.com'
self.test_url = 'https://sandbox.api.payulatam.com/payments-api/4.0/service.cgi'
self.live_url = 'https://api.payulatam.com/payments-api/4.0/service.cgi'
self.supported_countries = %w[AR BR CL CO MX PA PE]
self.default_currency = 'USD'
self.money_format = :dollars
self.supported_cardtypes = %i[visa master american_express diners_club naranja cabal]
BRAND_MAP = {
'visa' => 'VISA',
'master' => 'MASTERCARD',
'maestro' => 'MASTERCARD',
'american_express' => 'AMEX',
'diners_club' => 'DINERS',
'naranja' => 'NARANJA',
'cabal' => 'CABAL'
}
MINIMUMS = {
'ARS' => 1700,
'BRL' => 600,
'MXN' => 3900,
'PEN' => 500
}
def initialize(options = {})
requires!(options, :merchant_id, :account_id, :api_login, :api_key, :payment_country)
super
end
def purchase(amount, payment_method, options = {})
post = {}
auth_or_sale(post, 'AUTHORIZATION_AND_CAPTURE', amount, payment_method, options)
commit('purchase', post)
end
def authorize(amount, payment_method, options = {})
post = {}
auth_or_sale(post, 'AUTHORIZATION', amount, payment_method, options)
commit('auth', post)
end
def capture(amount, authorization, options = {})
post = {}
add_credentials(post, 'SUBMIT_TRANSACTION', options)
add_transaction_elements(post, 'CAPTURE', options)
add_reference(post, authorization)
if !amount.nil? && amount.to_f != 0.0
post[:transaction][:additionalValues] ||= {}
post[:transaction][:additionalValues][:TX_VALUE] = invoice_for(amount, options)[:TX_VALUE]
end
commit('capture', post)
end
def void(authorization, options = {})
post = {}
add_credentials(post, 'SUBMIT_TRANSACTION', options)
add_transaction_elements(post, 'VOID', options)
add_reference(post, authorization)
commit('void', post)
end
def refund(amount, authorization, options = {})
post = {}
add_credentials(post, 'SUBMIT_TRANSACTION', options)
if options[:partial_refund]
add_transaction_elements(post, 'PARTIAL_REFUND', options)
post[:transaction][:additionalValues] ||= {}
post[:transaction][:additionalValues][:TX_VALUE] = invoice_for(amount, options)[:TX_VALUE]
else
add_transaction_elements(post, 'REFUND', options)
end
add_reference(post, authorization)
commit('refund', post)
end
def verify(credit_card, options = {})
minimum = MINIMUMS[options[:currency].upcase] if options[:currency]
amount = options[:verify_amount] || minimum || 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
def store(payment_method, options = {})
post = {}
add_credentials(post, 'CREATE_TOKEN')
add_payment_method_to_be_tokenized(post, payment_method, options)
commit('store', post)
end
def verify_credentials
post = {}
add_credentials(post, 'GET_PAYMENT_METHODS')
response = commit('verify_credentials', post)
response.success?
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((\"creditCard\\\":{\\\"number\\\":\\\")\d+), '\1[FILTERED]').
gsub(%r((\"securityCode\\\":\\\")\d+), '\1[FILTERED]').
gsub(%r((\"apiKey\\\":\\\")\w+), '\1[FILTERED]')
end
private
def auth_or_sale(post, transaction_type, amount, payment_method, options)
add_credentials(post, 'SUBMIT_TRANSACTION', options)
add_transaction_elements(post, transaction_type, options)
add_order(post, options)
add_buyer(post, payment_method, options)
add_invoice(post, amount, options)
add_signature(post)
add_payment_method(post, payment_method, options)
add_payer(post, payment_method, options)
add_extra_parameters(post, options)
end
def add_credentials(post, command, options = {})
post[:test] = test? unless command == 'CREATE_TOKEN'
post[:language] = options[:language] || 'en'
post[:command] = command
merchant = {}
merchant[:apiLogin] = @options[:api_login]
merchant[:apiKey] = @options[:api_key]
post[:merchant] = merchant
end
def add_transaction_elements(post, type, options)
transaction = {}
transaction[:paymentCountry] = @options[:payment_country]
transaction[:type] = type
transaction[:ipAddress] = options[:ip] || ''
transaction[:userAgent] = options[:user_agent] if options[:user_agent]
transaction[:cookie] = options[:cookie] if options[:cookie]
transaction[:deviceSessionId] = options[:device_session_id] if options[:device_session_id]
post[:transaction] = transaction
end
def add_order(post, options)
order = {}
order[:accountId] = @options[:account_id]
order[:partnerId] = options[:partner_id] if options[:partner_id]
order[:referenceCode] = options[:order_id] || generate_unique_id
order[:description] = options[:description] || 'Compra en ' + @options[:merchant_id]
order[:language] = options[:language] || 'en'
order[:shippingAddress] = shipping_address_fields(options) if options[:shipping_address]
post[:transaction][:order] = order
end
def add_payer(post, payment_method, options)
address = options[:billing_address]
payer = {}
payer[:fullName] = payment_method.name.strip
payer[:contactPhone] = address[:phone] if address && address[:phone]
payer[:dniNumber] = options[:dni_number] if options[:dni_number]
payer[:dniType] = options[:dni_type] if options[:dni_type]
payer[:emailAddress] = options[:email] if options[:email]
payer[:birthdate] = options[:birth_date] if options[:birth_date] && @options[:payment_country] == 'MX'
payer[:billingAddress] = billing_address_fields(options)
post[:transaction][:payer] = payer
end
def billing_address_fields(options)
return unless address = options[:billing_address]
billing_address = {}
billing_address[:street1] = address[:address1]
billing_address[:street2] = address[:address2]
billing_address[:city] = address[:city]
billing_address[:state] = address[:state]
billing_address[:country] = address[:country] unless address[:country].blank?
billing_address[:postalCode] = address[:zip] if @options[:payment_country] == 'MX'
billing_address[:phone] = address[:phone]
billing_address
end
def add_buyer(post, payment_method, options)
buyer = {}
if buyer_hash = options[:buyer]
buyer[:fullName] = buyer_hash[:name]
buyer[:dniNumber] = buyer_hash[:dni_number]
buyer[:dniType] = buyer_hash[:dni_type]
buyer[:merchantBuyerId] = buyer_hash[:merchant_buyer_id]
buyer[:cnpj] = buyer_hash[:cnpj] if @options[:payment_country] == 'BR'
buyer[:emailAddress] = buyer_hash[:email]
buyer[:contactPhone] = (options[:billing_address][:phone] if options[:billing_address]) || (options[:shipping_address][:phone_number] if options[:shipping_address]) || ''
buyer[:shippingAddress] = shipping_address_fields(options) if options[:shipping_address]
else
buyer[:fullName] = payment_method.name.strip
buyer[:dniNumber] = options[:dni_number]
buyer[:dniType] = options[:dni_type]
buyer[:merchantBuyerId] = options[:merchant_buyer_id]
buyer[:cnpj] = options[:cnpj] if @options[:payment_country] == 'BR'
buyer[:emailAddress] = options[:email]
buyer[:contactPhone] = (options[:billing_address][:phone] if options[:billing_address]) || (options[:shipping_address][:phone_number] if options[:shipping_address]) || ''
buyer[:shippingAddress] = shipping_address_fields(options) if options[:shipping_address]
end
post[:transaction][:order][:buyer] = buyer
end
def shipping_address_fields(options)
return unless address = options[:shipping_address]
shipping_address = {}
shipping_address[:street1] = address[:address1]
shipping_address[:street2] = address[:address2]
shipping_address[:city] = address[:city]
shipping_address[:state] = address[:state]
shipping_address[:country] = address[:country]
shipping_address[:postalCode] = address[:zip]
shipping_address[:phone] = address[:phone_number]
shipping_address
end
def add_invoice(post, money, options)
post[:transaction][:order][:additionalValues] = invoice_for(money, options)
end
def invoice_for(money, options)
tx_value = {}
tx_value[:value] = amount(money)
tx_value[:currency] = options[:currency] || currency(money)
tx_tax = {}
tx_tax[:value] = options[:tax] || '0'
tx_tax[:currency] = options[:currency] || currency(money)
tx_tax_return_base = {}
tx_tax_return_base[:value] = options[:tax_return_base] || '0'
tx_tax_return_base[:currency] = options[:currency] || currency(money)
additional_values = {}
additional_values[:TX_VALUE] = tx_value
additional_values[:TX_TAX] = tx_tax if @options[:payment_country] == 'CO'
additional_values[:TX_TAX_RETURN_BASE] = tx_tax_return_base if @options[:payment_country] == 'CO'
additional_values
end
def add_signature(post)
post[:transaction][:order][:signature] = signature_from(post)
end
def signature_from(post)
signature_string = [
@options[:api_key],
@options[:merchant_id],
post[:transaction][:order][:referenceCode],
post[:transaction][:order][:additionalValues][:TX_VALUE][:value],
post[:transaction][:order][:additionalValues][:TX_VALUE][:currency]
].compact.join('~')
Digest::MD5.hexdigest(signature_string)
end
def codensa_bin?(number)
number.start_with?('590712')
end
def add_payment_method(post, payment_method, options)
if payment_method.is_a?(String)
brand, token = split_authorization(payment_method)
credit_card = {}
credit_card[:securityCode] = options[:cvv] if options[:cvv]
credit_card[:processWithoutCvv2] = true if options[:cvv].blank?
post[:transaction][:creditCard] = credit_card
post[:transaction][:creditCardTokenId] = token
post[:transaction][:paymentMethod] = brand.upcase
else
credit_card = {}
credit_card[:number] = payment_method.number
credit_card[:securityCode] = payment_method.verification_value || options[:cvv]
credit_card[:expirationDate] = format(payment_method.year, :four_digits).to_s + '/' + format(payment_method.month, :two_digits).to_s
credit_card[:name] = payment_method.name.strip
credit_card[:processWithoutCvv2] = true if add_process_without_cvv2(payment_method, options)
post[:transaction][:creditCard] = credit_card
post[:transaction][:paymentMethod] = codensa_bin?(payment_method.number) ? 'CODENSA' : BRAND_MAP[payment_method.brand.to_s]
end
end
def add_process_without_cvv2(payment_method, options)
return true if payment_method.verification_value.blank? && options[:cvv].blank?
false
end
def add_extra_parameters(post, options)
extra_parameters = {}
extra_parameters[:INSTALLMENTS_NUMBER] = options[:installments_number] || 1
extra_parameters[:EXTRA1] = options[:extra_1] if options[:extra_1]
extra_parameters[:EXTRA2] = options[:extra_2] if options[:extra_2]
extra_parameters[:EXTRA3] = options[:extra_3] if options[:extra_3]
post[:transaction][:extraParameters] = extra_parameters
end
def add_reference(post, authorization)
order_id, transaction_id = split_authorization(authorization)
order = {}
order[:id] = order_id
post[:transaction][:order] = order
post[:transaction][:parentTransactionId] = transaction_id
post[:transaction][:reason] = 'n/a'
end
def add_payment_method_to_be_tokenized(post, payment_method, options)
credit_card_token = {}
credit_card_token[:payerId] = options[:payer_id] || generate_unique_id
credit_card_token[:name] = payment_method.name.strip
credit_card_token[:identificationNumber] = options[:dni_number]
credit_card_token[:paymentMethod] = BRAND_MAP[payment_method.brand.to_s]
credit_card_token[:number] = payment_method.number
credit_card_token[:expirationDate] = format(payment_method.year, :four_digits).to_s + '/' + format(payment_method.month, :two_digits).to_s
post[:creditCardToken] = credit_card_token
end
def commit(action, params)
raw_response = ssl_post(url, post_data(params), headers)
response = parse(raw_response)
rescue ResponseError => e
raw_response = e.response.body
response_error(raw_response)
rescue JSON::ParserError
unparsable_response(raw_response)
else
success = success_from(action, response)
Response.new(
success,
message_from(action, success, response),
response,
authorization: success ? authorization_from(action, response) : nil,
error_code: success ? nil : error_from(action, response),
test: test?
)
end
def headers
{
'Content-Type' => 'application/json',
'Accept' => 'application/json'
}
end
def post_data(params)
params.merge(test: test?)
params.to_json
end
def url
test? ? test_url : live_url
end
def parse(body)
JSON.parse(body)
end
def success_from(action, response)
case action
when 'store'
response['code'] == 'SUCCESS' && response['creditCardToken'] && response['creditCardToken']['creditCardTokenId'].present?
when 'verify_credentials'
response['code'] == 'SUCCESS'
when 'refund', 'void'
response['code'] == 'SUCCESS' && response['transactionResponse'] && (response['transactionResponse']['state'] == 'PENDING' || response['transactionResponse']['state'] == 'APPROVED')
else
response['code'] == 'SUCCESS' && response['transactionResponse'] && (response['transactionResponse']['state'] == 'APPROVED')
end
end
def message_from(action, success, response)
case action
when 'store'
message_from_store(success, response)
when 'verify_credentials'
message_from_verify_credentials(success)
else
message_from_transaction_response(success, response)
end
end
def message_from_store(success, response)
return response['code'] if success
error_description = response['creditCardToken']['errorDescription'] if response['creditCardToken']
response['error'] || error_description || 'FAILED'
end
def message_from_verify_credentials(success)
return 'VERIFIED' if success
'FAILED'
end
def message_from_transaction_response(success, response)
response_code = response.dig('transactionResponse', 'responseCode') || response.dig('transactionResponse', 'pendingReason')
return response_code if success
return response_code + ' | ' + response.dig('transactionResponse', 'paymentNetworkResponseErrorMessage') if response.dig('transactionResponse', 'paymentNetworkResponseErrorMessage')
return response.dig('transactionResponse', 'responseMessage') if response.dig('transactionResponse', 'responseMessage')
return response['error'] if response['error']
return response_code if response_code
'FAILED'
end
def authorization_from(action, response)
case action
when 'store'
[
response['creditCardToken']['paymentMethod'],
response['creditCardToken']['creditCardTokenId']
].compact.join('|')
when 'verify_credentials'
nil
else
[
response['transactionResponse']['orderId'],
response['transactionResponse']['transactionId']
].compact.join('|')
end
end
def split_authorization(authorization)
authorization.split('|')
end
def error_from(action, response)
case action
when 'store'
response['creditCardToken']['errorDescription'] if response['creditCardToken']
when 'verify_credentials'
response['error'] || 'FAILED'
else
response['transactionResponse']['paymentNetworkResponseCode'] || response['transactionResponse']['errorCode'] if response['transactionResponse']
end
end
def response_error(raw_response)
response = parse(raw_response)
rescue JSON::ParserError
unparsable_response(raw_response)
else
return Response.new(
false,
message_from('', false, response),
response,
test: test?
)
end
def unparsable_response(raw_response)
message = 'Invalid JSON response received from PayuLatamGateway. Please contact PayuLatamGateway if you continue to receive this message.'
message += " (The raw response returned by the API was #{raw_response.inspect})"
return Response.new(false, message)
end
end
end
end