diff --git a/config.ru b/config.ru index e5ba3c07281483b9ca2818e1a007c63cd9400707..6c96ddafb46aa4af10ffc09f15a7c0b43a320b8a 100644 --- a/config.ru +++ b/config.ru @@ -23,6 +23,7 @@ require_relative "lib/customer" require_relative "lib/three_d_secure_repo" require_relative "lib/electrum" require_relative "lib/transaction" +require_relative "lib/credit_card_customer_gateway" require "sentry-ruby" Sentry.init do |config| @@ -119,86 +120,6 @@ class CreditCardGateway end end -class CreditCardCustomerGateway - def initialize(jid, customer_id, antifraud) - @jid = jid - @customer_id = customer_id - @gateway = CreditCardGateway.new( - customer_plan&.dig(:currency), - antifraud - ) - end - - def merchant_account - @gateway.merchant_account - end - - def check_customer_id(cid) - return cid unless ENV["RACK_ENV"] == "production" - - raise "customer_id does not match" unless @customer_id == cid - - cid - end - - def customer_id - customer_id = Customer.new(nil, @jid).customer_id - return customer_id if check_customer_id(customer_id) - - result = @gateway.customer.create - raise "Braintree customer create failed" unless result.success? - - @customer_id = result.customer.id - Customer.new(@customer_id, @jid).save! - @customer_id - end - - def customer_plan - name = DB.exec_params(<<~SQL, [@customer_id]).first&.[]("plan_name") - SELECT plan_name FROM customer_plans WHERE customer_id=$1 LIMIT 1 - SQL - PLANS.find { |plan| plan[:name].to_s == name } - end - - def client_token - @gateway.client_token(customer_id: customer_id) - end - - def payment_methods? - !@gateway.customer.find(customer_id).payment_methods.empty? - end - - def antifraud - return if REDIS.exists?("jmp_antifraud_bypass-#{customer_id}") - - @gateway.antifraud - end - - def with_antifraud(&blk) - @gateway.with_antifraud(&blk) - end - - def sale(nonce, amount) - @gateway.sale(nonce, amount, customer_id: customer_id) - end - - def default_method(nonce) - with_antifraud do - @gateway.payment_method.create( - customer_id: customer_id, payment_method_nonce: nonce, - options: { - verify_card: true, make_default: true, - verification_merchant_account_id: merchant_account.to_s - } - ) - end - end - - def remove_method(token) - @gateway.payment_method.delete(token) - end -end - class UnknownTransactions def self.from(currency, customer_id, address, tx_hashes) self.for( diff --git a/lib/credit_card_customer_gateway.rb b/lib/credit_card_customer_gateway.rb new file mode 100644 index 0000000000000000000000000000000000000000..a1f0fe9937809696f65c609d2caa0132e445438d --- /dev/null +++ b/lib/credit_card_customer_gateway.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require_relative "credit_card_gateway" + +class CreditCardCustomerGateway + def initialize(jid, customer_id, antifraud, currency: nil) + @jid = jid + @customer_id = customer_id + @gateway = CreditCardGateway.new( + currency || customer_plan&.dig(:currency), + antifraud + ) + end + + def merchant_account + @gateway.merchant_account + end + + def check_customer_id(cid) + return cid unless ENV["RACK_ENV"] == "production" + + raise "customer_id does not match" unless @customer_id == cid + + cid + end + + def customer_id + customer_id = Customer.new(nil, @jid).customer_id + return customer_id if check_customer_id(customer_id) + + result = @gateway.customer.create + raise "Braintree customer create failed" unless result.success? + + @customer_id = result.customer.id + Customer.new(@customer_id, @jid).save! + @customer_id + end + + def customer_plan + name = DB.exec_params(<<~SQL, [@customer_id]).first&.[]("plan_name") + SELECT plan_name FROM customer_plans WHERE customer_id=$1 LIMIT 1 + SQL + PLANS.find { |plan| plan[:name].to_s == name } + end + + def client_token + @gateway.client_token(customer_id: customer_id) + end + + def payment_methods? + !@gateway.customer.find(customer_id).payment_methods.empty? + end + + def antifraud + return if REDIS.exists?("jmp_antifraud_bypass-#{customer_id}") + + @gateway.antifraud + end + + def with_antifraud(&blk) + @gateway.with_antifraud(&blk) + end + + def sale(nonce, amount) + customer = Customer.new(@customer_id, @jid) + raise "Please contact JMP support." if customer.trust_level == "Tombed" + + @gateway.sale(nonce, amount, customer_id: customer_id) + end + + def default_method(nonce) + with_antifraud do + @gateway.payment_method.create( + customer_id: customer_id, payment_method_nonce: nonce, + options: { + verify_card: true, make_default: true, + verification_merchant_account_id: merchant_account.to_s + } + ) + end + end + + def remove_method(token) + @gateway.payment_method.delete(token) + end +end diff --git a/lib/credit_card_gateway.rb b/lib/credit_card_gateway.rb new file mode 100644 index 0000000000000000000000000000000000000000..842e8e7cae7f0fc32a27a3335948603e0b986eec --- /dev/null +++ b/lib/credit_card_gateway.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "braintree" +require "customer" + +class CreditCardGateway + class ErrorResult < StandardError + def self.for(result) + if result.verification&.status == "gateway_rejected" && + result.verification&.gateway_rejection_reason == "cvv" + new("fieldInvalidForCvv") + else + new(result.message) + end + end + end + + def initialize(currency, antifraud) + @currency = currency + @antifraud = antifraud + + @gateway = Braintree::Gateway.new( + environment: BRAINTREE_CONFIG[:environment].to_s, + merchant_id: BRAINTREE_CONFIG[:merchant_id].to_s, + public_key: BRAINTREE_CONFIG[:public_key].to_s, + private_key: BRAINTREE_CONFIG[:private_key].to_s + ) + end + + def merchant_account + BRAINTREE_CONFIG[:merchant_accounts][@currency] + end + + def client_token(**kwargs) + kwargs[:merchant_account_id] = merchant_account.to_s if merchant_account + @gateway.client_token.generate(**kwargs) + end + + def antifraud + REDIS.mget(@antifraud.map { |k| "jmp_antifraud-#{k}" }).find do |anti| + anti.to_i > 2 + end && + Braintree::ErrorResult.new( + @gateway, errors: {}, message: "Please contact support" + ) + end + + def with_antifraud + result = antifraud || yield + return result if result.success? + + @antifraud.each do |k| + REDIS.incr("jmp_antifraud-#{k}") + REDIS.expire("jmp_antifraud-#{k}", 60 * 60 * 24) + end + + raise ErrorResult.for(result) + end + + def sale(nonce, amount, **kwargs) + with_antifraud do + @gateway.transaction.sale( + payment_method_nonce: nonce, + amount: amount, merchant_account_id: merchant_account.to_s, + options: { + store_in_vault_on_success: true, submit_for_settlement: true + }, **kwargs + ) + end + end + + def customer + @gateway.customer + end + + def payment_method + @gateway.payment_method + end +end diff --git a/lib/customer.rb b/lib/customer.rb index 01843622f2408bca14a07a90fc45b4eca2eeb388..0b8e60026e0cd82e67a433581ebe52838a989072 100644 --- a/lib/customer.rb +++ b/lib/customer.rb @@ -20,8 +20,16 @@ class Customer raise "Saving new customer,jid to redis failed" end + def trust_level + REDIS.get(redis_trust_level) + end + protected + def redis_trust_level + "jmp_customer_trust_level-#{@customer_id}" + end + def redis_key_jid "jmp_customer_id-#{@jid}" end diff --git a/test/test_credit_card_customer_gateway.rb b/test/test_credit_card_customer_gateway.rb new file mode 100644 index 0000000000000000000000000000000000000000..ce95ec72acb523102133a760e1af5d1fe655a5f2 --- /dev/null +++ b/test/test_credit_card_customer_gateway.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "test_helper" +require "credit_card_customer_gateway" + +Customer::REDIS = Minitest::Mock.new +CreditCardCustomerGateway::DB = Minitest::Mock.new + +class CreditCardCustomerGatewayTest < Minitest::Test + def setup + @gateway = CreditCardCustomerGateway.new("test@test.net", "0001", true, currency: "CAD") + end + + def test_no_cc_for_tombed + Customer::REDIS.expect(:get, "Tombed", ["jmp_customer_trust_level-0001"]) + CreditCardCustomerGateway::DB.expect(:exec_params, [OpenStruct.new(name: "plan_name")]) + assert_raises RuntimeError do + @gateway.sale("nonce", 1000) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index f1c79e6221eb554a4b96a8b5b3ad7329667d81f8..9850c4bca361c411fab0bb1763fc5d44cac34dae 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,6 +21,13 @@ rescue LoadError nil end +BRAINTREE_CONFIG = { + environment: "sandbox", + merchant_id: "some_merchant_id", + public_key: "some_public_key", + private_key: "some_private_key" +}.freeze + module Minitest class Test def self.property(m, &block)