dont allow tombed customers to transact

Phillip Davis created

Change summary

config.ru                                 | 81 -----------------------
lib/credit_card_customer_gateway.rb       | 86 +++++++++++++++++++++++++
lib/credit_card_gateway.rb                | 79 ++++++++++++++++++++++
lib/customer.rb                           |  8 ++
test/test_credit_card_customer_gateway.rb | 21 ++++++
test/test_helper.rb                       |  7 ++
6 files changed, 202 insertions(+), 80 deletions(-)

Detailed changes

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(

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

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

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

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

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)