lib/active_merchant/billing/gateways/vpos.rb
require 'digest'
require 'jwe'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class VposGateway < Gateway
self.test_url = 'https://vpos.infonet.com.py:8888'
self.live_url = 'https://vpos.infonet.com.py'
self.supported_countries = ['PY']
self.default_currency = 'PYG'
self.supported_cardtypes = %i[visa master panal]
self.homepage_url = 'https://comercios.bancard.com.py'
self.display_name = 'vPOS'
self.money_format = :dollars
ENDPOINTS = {
pci_encryption_key: '/vpos/api/0.3/application/encryption-key',
pay_pci_buy_encrypted: '/vpos/api/0.3/pci/encrypted',
pci_buy_rollback: '/vpos/api/0.3/pci_buy/rollback',
refund: '/vpos/api/0.3/refunds'
}
def initialize(options = {})
requires!(options, :private_key, :public_key)
@private_key = options[:private_key]
@public_key = options[:public_key]
@encryption_key = OpenSSL::PKey::RSA.new(options[:encryption_key]) if options[:encryption_key]
@shop_process_id = options[:shop_process_id] || SecureRandom.random_number(10**15)
super
end
def purchase(money, payment, options = {})
commerce = options[:commerce] || @options[:commerce]
commerce_branch = options[:commerce_branch] || @options[:commerce_branch]
shop_process_id = options[:shop_process_id] || @shop_process_id
token = generate_token(shop_process_id, 'pay_pci', commerce, commerce_branch, amount(money), currency(money))
post = {}
post[:token] = token
post[:commerce] = commerce.to_s
post[:commerce_branch] = commerce_branch.to_s
post[:shop_process_id] = shop_process_id
post[:number_of_payments] = options[:number_of_payments] || 1
post[:recursive] = options[:recursive] || false
add_invoice(post, money, options)
add_card_data(post, payment)
add_customer_data(post, options)
commit(:pay_pci_buy_encrypted, post)
end
def void(authorization, options = {})
_, shop_process_id = authorization.to_s.split('#')
token = generate_token(shop_process_id, 'rollback', '0.00')
post = {
token: token,
shop_process_id: shop_process_id
}
commit(:pci_buy_rollback, post)
end
def credit(money, payment, options = {})
# Not permitted for foreign cards.
commerce = options[:commerce] || @options[:commerce]
commerce_branch = options[:commerce_branch] || @options[:commerce_branch]
token = generate_token(@shop_process_id, 'refund', commerce, commerce_branch, amount(money), currency(money))
post = {}
post[:token] = token
post[:commerce] = commerce.to_i
post[:commerce_branch] = commerce_branch.to_i
post[:shop_process_id] = @shop_process_id
add_invoice(post, money, options)
add_card_data(post, payment)
add_customer_data(post, options)
post[:origin_shop_process_id] = options[:original_shop_process_id] if options[:original_shop_process_id]
commit(:refund, post)
end
def refund(money, authorization, options = {})
commerce = options[:commerce] || @options[:commerce]
commerce_branch = options[:commerce_branch] || @options[:commerce_branch]
shop_process_id = options[:shop_process_id] || @shop_process_id
_, original_shop_process_id = authorization.to_s.split('#')
token = generate_token(shop_process_id, 'refund', commerce, commerce_branch, amount(money), currency(money))
post = {}
post[:token] = token
post[:commerce] = commerce.to_i
post[:commerce_branch] = commerce_branch.to_i
post[:shop_process_id] = shop_process_id
add_invoice(post, money, options)
add_customer_data(post, options)
post[:origin_shop_process_id] = original_shop_process_id || options[:original_shop_process_id]
commit(:refund, post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
clean_transcript = remove_invalid_utf_8_byte_sequences(transcript)
clean_transcript.
gsub(/(token\\":\\")[.\-\w]+/, '\1[FILTERED]').
gsub(/(card_encrypted_data\\":\\")[.\-\w]+/, '\1[FILTERED]')
end
def remove_invalid_utf_8_byte_sequences(transcript)
transcript.encode('UTF-8', 'binary', undef: :replace, replace: '')
end
# Required to encrypt PAN data.
def one_time_public_key
token = generate_token('get_encription_public_key', @public_key)
response = commit(:pci_encryption_key, token: token)
response.params['encryption_key']
end
private
def generate_token(*elements)
Digest::MD5.hexdigest(@private_key + elements.join)
end
def add_invoice(post, money, options)
post[:amount] = amount(money)
post[:currency] = options[:currency] || currency(money)
end
def add_card_data(post, payment)
card_number = payment.number
cvv = payment.verification_value
payload = { card_number: card_number, cvv: cvv }.to_json
encryption_key = @encryption_key || OpenSSL::PKey::RSA.new(one_time_public_key)
post[:card_encrypted_data] = JWE.encrypt(payload, encryption_key)
post[:card_month_expiration] = format(payment.month, :two_digits)
post[:card_year_expiration] = format(payment.year, :two_digits)
end
def add_customer_data(post, options)
post[:additional_data] = options[:additional_data] || '' # must be passed even if empty
end
def parse(body)
JSON.parse(body)
end
def commit(action, parameters)
url = build_request_url(action)
begin
response = parse(ssl_post(url, post_data(parameters)))
rescue ResponseError => e
# Errors are returned with helpful data,
# but get filtered out by `ssl_post` because of their HTTP status.
response = parse(e.response.body)
end
Response.new(
success_from(response),
message_from(response),
response,
authorization: authorization_from(response),
avs_result: nil,
cvv_result: nil,
test: test?,
error_code: error_code_from(response)
)
end
def success_from(response)
if code = response.dig('confirmation', 'response_code')
code == '00'
else
response['status'] == 'success'
end
end
def message_from(response)
%w(confirmation refund).each do |m|
message =
response.dig(m, 'extended_response_description') ||
response.dig(m, 'response_description') ||
response.dig(m, 'response_details')
return message if message
end
[response.dig('messages', 0, 'key'), response.dig('messages', 0, 'dsc')].join(':')
end
def authorization_from(response)
response_body = response.dig('confirmation') || response.dig('refund')
return unless response_body
authorization_number = response_body.dig('authorization_number') || response_body.dig('authorization_code')
shop_process_id = response_body.dig('shop_process_id')
"#{authorization_number}##{shop_process_id}"
end
def error_code_from(response)
response.dig('confirmation', 'response_code') unless success_from(response)
end
def build_request_url(action)
base_url = (test? ? test_url : live_url)
base_url + ENDPOINTS[action]
end
def post_data(data)
{ public_key: @public_key,
operation: data }.compact.to_json
end
end
end
end