Skip to content

Commit

Permalink
Adding deepstack gateway
Browse files Browse the repository at this point in the history
Baseline for future store feature

Remove some comments

Updating urls

Added shipping

Remove some comments

Updating urls

Added shipping

Remove some comments

Updating urls

Added shipping

Remove some comments

Updating urls

Added shipping

Rubocop offense fixes

Fixing PR comments

Remove unused credentials

Added missing tests. Removed :sandbox for test?

Removing puts statements

Move set capture true to separate function + remove code comments

Update changelog
  • Loading branch information
khoinguyendeepstack committed Aug 8, 2023
1 parent 21d987d commit 82d632b
Show file tree
Hide file tree
Showing 5 changed files with 902 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Kushki: Fix add amount default method for subtotalIva and subtotalIva0 [yunnydang] #4845
* Rapyd: Add customer object to requests [aenand] #4838
* CyberSource: Add merchant_id [almalee24] #4844
* Deepstack: Add Deepstack Gateway [khoinguyendeepstack] #4830

== Version 1.134.0 (July 25, 2023)
* Update required Ruby version [almalee24] #4823
Expand Down
382 changes: 382 additions & 0 deletions lib/active_merchant/billing/gateways/deepstack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class DeepstackGateway < Gateway
self.test_url = 'https://api.sandbox.deepstack.io'
self.live_url = 'https://api.deepstack.io'

self.supported_countries = ['US']
self.default_currency = 'USD'
self.supported_cardtypes = %i[visa master american_express discover]
self.money_format = :cents

self.homepage_url = 'https://deepstack.io/'
self.display_name = 'Deepstack Gateway'

STANDARD_ERROR_CODE_MAPPING = {}

def initialize(options = {})
requires!(options, :publishable_api_key, :app_id, :shared_secret)
@publishable_api_key, @app_id, @shared_secret = options.values_at(:publishable_api_key, :app_id, :shared_secret)
super
end

def purchase(money, payment, options = {})
post = {}
add_payment(post, payment, options)
add_order(post, money, options)
add_purchase_capture(post)
add_address(post, payment, options)
add_customer_data(post, options)
commit('sale', post)
end

def authorize(money, payment, options = {})
post = {}
add_payment(post, payment, options)
add_order(post, money, options)
add_address(post, payment, options)
add_customer_data(post, options)

commit('auth', post)
end

def capture(money, authorization, options = {})
post = {}
add_invoice(post, money, authorization, options)

commit('capture', post)
end

def refund(money, authorization, options = {})
post = {}
add_invoice(post, money, authorization, options)
commit('refund', post)
end

def void(money, authorization, options = {})
post = {}
add_invoice(post, money, authorization, options)
commit('void', post)
end

def verify(credit_card, options = {})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(100, credit_card, options) }
r.process(:ignore_result) { void(0, r.authorization, options) }
end
end

def get_token(credit_card, options = {})
post = {}
add_payment_instrument(post, credit_card, options)
add_address_payment_instrument(post, credit_card, options)
commit('gettoken', post)
end

def supports_scrubbing?
true
end

def scrub(transcript)
transcript.
gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]').
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r((Hmac: )[\w=]+), '\1[FILTERED]').
gsub(%r((\\"account_number\\":\\")[\w*]+), '\1[FILTERED]').
gsub(%r((\\"cvv\\":\\")\w+), '\1[FILTERED]').
gsub(%r((\\"expiration\\":\\")\w+), '\1[FILTERED]')
end

private

def add_customer_data(post, options)
post[:meta] ||= {}

add_shipping(post, options) if options.key?(:shipping_address)
post[:meta][:client_customer_id] = options[:customer] if options[:customer]
post[:meta][:client_transaction_id] = options[:order_id] if options[:order_id]
post[:meta][:client_transaction_description] = options[:description] if options[:description]
post[:meta][:client_invoice_id] = options[:invoice] if options[:invoice]
post[:meta][:card_holder_ip_address] = options[:ip] if options[:ip]
end

def add_address(post, creditcard, options)
return post unless options.key?(:address) || options.key?(:billing_address)

billing_address = options[:address] || options[:billing_address]
post[:source] ||= {}

post[:source][:billing_contact] = {}
post[:source][:billing_contact][:first_name] = billing_address[:first_name] if billing_address[:first_name]
post[:source][:billing_contact][:last_name] = billing_address[:last_name] if billing_address[:last_name]
post[:source][:billing_contact][:phone] = billing_address[:phone] if billing_address[:phone]
post[:source][:billing_contact][:email] = options[:email] if options[:email]
post[:source][:billing_contact][:address] = {}
post[:source][:billing_contact][:address][:line_1] = billing_address[:address1] if billing_address[:address1]
post[:source][:billing_contact][:address][:line_2] = billing_address[:address2] if billing_address[:address2]
post[:source][:billing_contact][:address][:city] = billing_address[:city] if billing_address[:city]
post[:source][:billing_contact][:address][:state] = billing_address[:state] if billing_address[:state]
post[:source][:billing_contact][:address][:postal_code] = billing_address[:zip] if billing_address[:zip]
post[:source][:billing_contact][:address][:country_code] = billing_address[:country] if billing_address[:country]
end

def add_address_payment_instrument(post, creditcard, options)
return post unless options.key?(:address) || options.key?(:billing_address)

billing_address = options[:address] || options[:billing_address]
post[:source] = {} unless post.key?(:payment_instrument)

post[:payment_instrument][:billing_contact] = {}
post[:payment_instrument][:billing_contact][:first_name] = billing_address[:first_name] if billing_address[:first_name]
post[:payment_instrument][:billing_contact][:last_name] = billing_address[:last_name] if billing_address[:last_name]
post[:payment_instrument][:billing_contact][:phone] = billing_address[:phone] if billing_address[:phone]
post[:payment_instrument][:billing_contact][:email] = billing_address[:email] if billing_address[:email]
post[:payment_instrument][:billing_contact][:address] = {}
post[:payment_instrument][:billing_contact][:address][:line_1] = billing_address[:address1] if billing_address[:address1]
post[:payment_instrument][:billing_contact][:address][:line_2] = billing_address[:address2] if billing_address[:address2]
post[:payment_instrument][:billing_contact][:address][:city] = billing_address[:city] if billing_address[:city]
post[:payment_instrument][:billing_contact][:address][:state] = billing_address[:state] if billing_address[:state]
post[:payment_instrument][:billing_contact][:address][:postal_code] = billing_address[:zip] if billing_address[:zip]
post[:payment_instrument][:billing_contact][:address][:country_code] = billing_address[:country] if billing_address[:country]
end

def add_shipping(post, options = {})
return post unless options.key?(:shipping_address)

shipping = options[:shipping_address]
post[:meta][:shipping_info] = {}
post[:meta][:shipping_info][:first_name] = shipping[:first_name] if shipping[:first_name]
post[:meta][:shipping_info][:last_name] = shipping[:last_name] if shipping[:last_name]
post[:meta][:shipping_info][:phone] = shipping[:phone] if shipping[:phone]
post[:meta][:shipping_info][:email] = shipping[:email] if shipping[:email]
post[:meta][:shipping_info][:address] = {}
post[:meta][:shipping_info][:address][:line_1] = shipping[:address1] if shipping[:address1]
post[:meta][:shipping_info][:address][:line_2] = shipping[:address2] if shipping[:address2]
post[:meta][:shipping_info][:address][:city] = shipping[:city] if shipping[:city]
post[:meta][:shipping_info][:address][:state] = shipping[:state] if shipping[:state]
post[:meta][:shipping_info][:address][:postal_code] = shipping[:zip] if shipping[:zip]
post[:meta][:shipping_info][:address][:country_code] = shipping[:country] if shipping[:country]
end

def add_invoice(post, money, authorization, options)
post[:amount] = amount(money)
post[:charge] = authorization
end

def add_payment(post, payment, options)
if payment.kind_of?(String)
post[:source] = {}
post[:source][:type] = 'card_on_file'
post[:source][:card_on_file] = {}
post[:source][:card_on_file][:id] = payment
post[:source][:card_on_file][:cvv] = options[:verification_value] || ''
post[:source][:card_on_file][:customer_id] = options[:customer_id] || ''
# credit card object
elsif payment.respond_to?(:number)
post[:source] = {}
post[:source][:type] = 'credit_card'
post[:source][:credit_card] = {}
post[:source][:credit_card][:account_number] = payment.number
post[:source][:credit_card][:cvv] = payment.verification_value || ''
post[:source][:credit_card][:expiration] = '%02d%02d' % [payment.month, payment.year % 100]
post[:source][:credit_card][:customer_id] = options[:customer_id] || ''
end
end

def add_payment_instrument(post, creditcard, options)
if creditcard.kind_of?(String)
post[:source] = creditcard
return post
end
return post unless creditcard.respond_to?(:number)

post[:payment_instrument] = {}
post[:payment_instrument][:type] = 'credit_card'
post[:payment_instrument][:credit_card] = {}
post[:payment_instrument][:credit_card][:account_number] = creditcard.number
post[:payment_instrument][:credit_card][:expiration] = '%02d%02d' % [creditcard.month, creditcard.year % 100]
post[:payment_instrument][:credit_card][:cvv] = creditcard.verification_value
end

def add_order(post, amount, options)
post[:transaction] ||= {}

post[:transaction][:amount] = amount
post[:transaction][:cof_type] = options.key?(:cof_type) ? options[:cof_type].upcase : 'UNSCHEDULED_CARDHOLDER'
post[:transaction][:capture] = false # Change this in the request (auth/charge)
post[:transaction][:currency_code] = (options[:currency] || currency(amount).upcase)
post[:transaction][:avs] = options[:avs] || true # default avs to true unless told otherwise
post[:transaction][:save_payment_instrument] = options[:save_payment_instrument] || false
end

def add_purchase_capture(post)
post[:transaction] ||= {}
post[:transaction][:capture] = true
end

def parse(body)
return {} if !body || body.empty?

JSON.parse(body)
end

def commit(action, parameters, method = 'POST')
url = (test? ? test_url : live_url)
if no_hmac(action)
request_headers = headers.merge(create_basic(parameters, action))
else
request_headers = headers.merge(create_hmac(parameters, method))
end
request_url = url + get_url(action)
begin
response = parse(ssl_post(request_url, post_data(action, parameters), request_headers))
Response.new(
success_from(response),
message_from(response),
response,
authorization: authorization_from(response),
avs_result: AVSResult.new(code: response['avs_result']),
cvv_result: CVVResult.new(response['cvv_result']),
test: test?,
error_code: error_code_from(response)
)
rescue ResponseError => e
Response.new(
false,
message_from_error(e.response.body),
response_error(e.response.body)
)
rescue JSON::ParserError
Response.new(
false,
message_from(response),
json_error(response)
)
end
end

def headers
{
'Accept' => 'text/plain',
'Content-Type' => 'application/json'
}
end

def response_error(response)
parse(response)
rescue JSON::ParserError
json_error(response)
end

def json_error(response)
msg = 'Invalid response received from the Conekta API.'
msg += " (The raw response returned by the API was #{response.inspect})"
{
'message' => msg
}
end

def success_from(response)
success = false
if response.key?('response_code')
success = response['response_code'] == '00'
# Hack because token/payment instrument methods do not return a response_code
elsif response.key?('id')
success = true if response['id'].start_with?('tok', 'card')
end

return success
end

def message_from(response)
response = JSON.parse(response) if response.is_a?(String)
if response.key?('message')
return response['message']
elsif response.key?('detail')
return response['detail']
end
end

def message_from_error(response)
if response.is_a?(String)
response.gsub!('\\"', '"')
response = JSON.parse(response)
end

if response.key?('detail')
return response['detail']
elsif response.key?('message')
return response['message']
end
end

def authorization_from(response)
response['id']
end

def post_data(action, parameters = {})
return JSON.generate(parameters)
end

def error_code_from(response)
error_code = nil
error_code = response['response_code'] unless success_from(response)
if error = response.dig('detail')
error_code = error
elsif error = response.dig('error')
error_code = error.dig('reason', 'id')
end
error_code
end

def get_url(action)
base = '/api/v1/'
case action
when 'sale'
return base + 'payments/charge'
when 'auth'
return base + 'payments/charge'
when 'capture'
return base + 'payments/capture'
when 'void'
return base + 'payments/refund'
when 'refund'
return base + 'payments/refund'
when 'gettoken'
return base + 'vault/token'
when 'vault'
return base + 'vault/payment-instrument/token'
else
return base + 'noaction'
end
end

def no_hmac(action)
case action
when 'gettoken'
return true
else
return false
end
end

def create_basic(post, method)
return { 'Authorization' => "Bearer #{@publishable_api_key}" }
end

def create_hmac(post, method)
# Need requestDate, requestMethod, Nonce, AppIDKey
app_id_key = @app_id
request_method = method.upcase
uuid = SecureRandom.uuid
request_time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')

string_to_hash = "#{app_id_key}|#{request_method}|#{request_time}|#{uuid}|#{JSON.generate(post)}"
signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), Base64.strict_decode64(@shared_secret), string_to_hash)
base64_signature = Base64.strict_encode64(signature)
hmac_header = Base64.strict_encode64("#{app_id_key}|#{request_method}|#{request_time}|#{uuid}|#{base64_signature}")
return { 'hmac' => hmac_header }
end
end
end
end
Loading

0 comments on commit 82d632b

Please sign in to comment.