feat: add upper limit to top-ups based on trust level

Amolith created

Change summary

lib/buy_account_credit_form.rb       | 21 ++++---
lib/credit_card_sale.rb              | 41 ++++++++++++++--
lib/trust_level.rb                   | 58 ++++++++++++++++++----
sgx_jmp.rb                           |  5 +
test/test_buy_account_credit_form.rb | 29 ++++++++---
test/test_credit_card_sale.rb        | 75 ++++++++++++++++++++++++++++++
6 files changed, 194 insertions(+), 35 deletions(-)

Detailed changes

lib/buy_account_credit_form.rb 🔗

@@ -1,21 +1,25 @@
 # frozen_string_literal: true
 
+require_relative "trust_level_repo"
+require_relative "credit_card_sale"
+
 class BuyAccountCreditForm
-	class AmountValidationError < StandardError
-		def initialize(amount)
-			super("amount #{amount} must be more than $15")
-		end
+	def self.trust_level_repo(**kwargs)
+		kwargs[:trust_level_repo] || TrustLevelRepo.new(**kwargs)
 	end
 
 	def self.for(customer)
-		customer.payment_methods.then do |payment_methods|
-			new(customer.balance, payment_methods)
+		trust_level_repo.find(customer).then do |trust_level|
+			customer.payment_methods.then do |payment_methods|
+				new(customer.balance, payment_methods, trust_level.max_top_up_amount)
+			end
 		end
 	end
 
-	def initialize(balance, payment_methods)
+	def initialize(balance, payment_methods, max_top_up_amount)
 		@balance = balance
 		@payment_methods = payment_methods
+		@max_top_up_amount = max_top_up_amount
 	end
 
 	def form
@@ -23,13 +27,12 @@ class BuyAccountCreditForm
 			:top_up,
 			balance: @balance,
 			payment_methods: @payment_methods,
-			range: [15, nil]
+			range: [15, @max_top_up_amount]
 		)
 	end
 
 	def parse(form)
 		amount = form.field("amount")&.value&.to_s
-		raise AmountValidationError, amount unless amount.to_i >= 15
 
 		{
 			payment_method: @payment_methods.fetch(

lib/credit_card_sale.rb 🔗

@@ -6,6 +6,38 @@ require "delegate"
 require_relative "transaction"
 require_relative "trust_level_repo"
 
+class TransactionDeclinedError < StandardError; end
+
+class AmountTooHighError < TransactionDeclinedError
+	attr_reader :amount, :max_amount
+
+	def initialize(amount, max_amount)
+		@amount = amount
+		@max_amount = max_amount
+		super("Amount $#{amount} exceeds maximum allowed amount of $#{max_amount}")
+	end
+end
+
+class AmountTooLowError < TransactionDeclinedError
+	attr_reader :amount, :min_amount
+
+	def initialize(amount, min_amount)
+		@amount = amount
+		@min_amount = min_amount
+		super("Amount $#{amount} is below minimum amount of $#{min_amount}")
+	end
+end
+
+class DeclinedError < TransactionDeclinedError
+	attr_reader :declines, :max_declines
+
+	def initialize(declines, max_declines)
+		@declines = declines
+		@max_declines = max_declines
+		super("Transaction declined")
+	end
+end
+
 class CreditCardSale
 	def self.create(*args, transaction_class: Transaction, **kwargs)
 		new(*args, **kwargs).sale.then do |response|
@@ -62,10 +94,9 @@ protected
 			REDIS.exists("jmp_customer_credit_card_lock-#{@customer.customer_id}"),
 			@trust_repo.find(@customer), @customer.declines
 		]).then do |(lock, tl, declines)|
-			unless tl.credit_card_transaction?(@amount.to_d, declines)
-				raise "Declined"
-			end
-			raise "Too many payments recently" if lock == 1
+			raise TransactionDeclinedError, "Too many payments recently" if lock == 1
+
+			tl.validate_credit_card_transaction!(@amount.to_d, declines)
 		end
 	end
 
@@ -107,7 +138,7 @@ class BraintreeFailure < StandardError
 	attr_reader :response
 
 	def initialize(response)
-		super response.message
+		super(response.message)
 		@response = response
 	end
 end

lib/trust_level.rb 🔗

@@ -32,6 +32,10 @@ module TrustLevel
 			new if manual == "Tomb"
 		end
 
+		def max_top_up_amount
+			0
+		end
+
 		def write_cdr?
 			false
 		end
@@ -44,8 +48,9 @@ module TrustLevel
 			false
 		end
 
-		def credit_card_transaction?(*)
-			false
+		def validate_credit_card_transaction!(_amount, _declines)
+			# Give a more ambiguous error so they don't know they're tombed.
+			raise DeclinedError
 		end
 
 		def create_subaccount?(*)
@@ -62,6 +67,14 @@ module TrustLevel
 			new if manual == "Basement" || (!manual && settled_amount < 10)
 		end
 
+		def max_top_up_amount
+			35
+		end
+
+		def max_declines
+			2
+		end
+
 		def write_cdr?
 			true
 		end
@@ -74,8 +87,11 @@ module TrustLevel
 			messages_today < 40
 		end
 
-		def credit_card_transaction?(amount, declines)
-			amount <= 35 && declines <= 2
+		def validate_credit_card_transaction!(amount, declines)
+			raise DeclinedError.new(declines, max_declines) if declines > max_declines
+			return unless amount > max_top_up_amount
+
+			raise AmountTooHighError.new(amount, max_top_up_amount)
 		end
 
 		def create_subaccount?(already_have)
@@ -92,6 +108,14 @@ module TrustLevel
 			new if manual == "Paragon" || (!manual && settled_amount > 60)
 		end
 
+		def max_top_up_amount
+			500
+		end
+
+		def max_declines
+			3
+		end
+
 		def write_cdr?
 			true
 		end
@@ -104,8 +128,11 @@ module TrustLevel
 			messages_today < 700
 		end
 
-		def credit_card_transaction?(amount, declines)
-			amount <= 500 && declines <= 3
+		def validate_credit_card_transaction!(amount, declines)
+			raise DeclinedError.new(declines, max_declines) if declines > max_declines
+			return unless amount > max_top_up_amount
+
+			raise AmountTooHighError.new(amount, max_top_up_amount)
 		end
 
 		def create_subaccount?(already_have)
@@ -134,9 +161,7 @@ module TrustLevel
 			true
 		end
 
-		def credit_card_transaction?(*)
-			true
-		end
+		def validate_credit_card_transaction!(*) end
 
 		def create_subaccount?(*)
 			true
@@ -167,6 +192,14 @@ module TrustLevel
 			@max_rate = EXPENSIVE_ROUTE.fetch(plan_name, 0.1)
 		end
 
+		def max_top_up_amount
+			130
+		end
+
+		def max_declines
+			2
+		end
+
 		def write_cdr?
 			true
 		end
@@ -179,8 +212,11 @@ module TrustLevel
 			messages_today < 500
 		end
 
-		def credit_card_transaction?(amount, declines)
-			amount <= 130 && declines <= 2
+		def validate_credit_card_transaction!(amount, declines)
+			raise DeclinedError.new(declines, max_declines) if declines > max_declines
+			return unless amount > max_top_up_amount
+
+			raise AmountTooHighError.new(amount, max_top_up_amount)
 		end
 
 		def create_subaccount?(already_have)

sgx_jmp.rb 🔗

@@ -670,7 +670,10 @@ Command.new(
 		end
 	}.then { |transaction|
 		Command.finish("#{transaction} added to your account balance.")
-	}.catch_only(BuyAccountCreditForm::AmountValidationError) do |e|
+	}.catch_only(
+		BuyAccountCreditForm::AmountTooHighError,
+		BuyAccountCreditForm::AmountTooLowError
+	) do |e|
 		Command.finish(e.message, type: :error)
 	end
 }.register(self).then(&CommandList.method(:register))

test/test_buy_account_credit_form.rb 🔗

@@ -3,16 +3,21 @@
 require "test_helper"
 require "buy_account_credit_form"
 require "customer"
+require "credit_card_sale"
 
 CustomerFinancials::BRAINTREE = Minitest::Mock.new
 CustomerFinancials::REDIS = Minitest::Mock.new
+TrustLevelRepo::REDIS = Minitest::Mock.new
+TrustLevelRepo::DB = Minitest::Mock.new
 
 class BuyAccountCreditFormTest < Minitest::Test
 	def setup
 		@payment_method = OpenStruct.new(card_type: "Test", last_4: "1234")
+		@max_top_up_amount = 130
 		@form = BuyAccountCreditForm.new(
 			BigDecimal("15.1234"),
-			PaymentMethods.new([@payment_method])
+			PaymentMethods.new([@payment_method]),
+			@max_top_up_amount
 		)
 	end
 
@@ -26,10 +31,24 @@ class BuyAccountCreditFormTest < Minitest::Test
 			["test"]
 		)
 
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve("Customer"),
+			["jmp_customer_trust_level-test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
+
 		assert_kind_of(
 			BuyAccountCreditForm,
 			BuyAccountCreditForm.for(customer).sync
 		)
+
+		assert_mock TrustLevelRepo::REDIS
+		assert_mock TrustLevelRepo::DB
 	end
 	em :test_for
 
@@ -67,14 +86,6 @@ class BuyAccountCreditFormTest < Minitest::Test
 		assert_equal "123", @form.parse(iq_form)[:amount]
 	end
 
-	def test_parse_bad_amount
-		iq_form = Blather::Stanza::X.new
-		iq_form.fields = [{ var: "amount", value: "1" }]
-		assert_raises(BuyAccountCreditForm::AmountValidationError) do
-			@form.parse(iq_form)[:amount]
-		end
-	end
-
 	def test_parse_payment_method
 		iq_form = Blather::Stanza::X.new
 		iq_form.fields = [

test/test_credit_card_sale.rb 🔗

@@ -9,6 +9,7 @@ CreditCardSale::BRAINTREE = Minitest::Mock.new
 CreditCardSale::REDIS = Minitest::Mock.new
 TrustLevelRepo::REDIS = Minitest::Mock.new
 TrustLevelRepo::DB = Minitest::Mock.new
+CustomerFinancials::REDIS = Minitest::Mock.new
 
 class CreditCardSaleTest < Minitest::Test
 	FAKE_BRAINTREE_TRANSACTION =
@@ -111,6 +112,80 @@ class CreditCardSaleTest < Minitest::Test
 	end
 	em :test_sale_locked
 
+	def test_sale_amount_too_high
+		CreditCardSale::REDIS.expect(
+			:exists,
+			EMPromise.resolve(0),
+			["jmp_customer_credit_card_lock-test"]
+		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve("Customer"),
+			["jmp_customer_trust_level-test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
+		CustomerFinancials::REDIS.expect(
+			:get,
+			EMPromise.resolve("0"),
+			["jmp_pay_decline-test"]
+		)
+
+		assert_raises(AmountTooHighError) do
+			CreditCardSale.new(
+				customer(plan_name: "test_usd"),
+				amount: 131,
+				payment_method: OpenStruct.new(token: "token")
+			).sale.sync
+		end
+
+		assert_mock CustomerFinancials::REDIS
+		assert_mock CreditCardSale::REDIS
+		assert_mock TrustLevelRepo::REDIS
+		assert_mock TrustLevelRepo::DB
+	end
+	em :test_sale_amount_too_high
+
+	def test_sale_too_many_declines
+		CreditCardSale::REDIS.expect(
+			:exists,
+			EMPromise.resolve(0),
+			["jmp_customer_credit_card_lock-test"]
+		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve("Customer"),
+			["jmp_customer_trust_level-test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
+		CustomerFinancials::REDIS.expect(
+			:get,
+			EMPromise.resolve("3"),
+			["jmp_pay_decline-test"]
+		)
+
+		assert_raises(DeclinedError) do
+			CreditCardSale.new(
+				customer(plan_name: "test_usd"),
+				amount: 50,
+				payment_method: OpenStruct.new(token: "token")
+			).sale.sync
+		end
+
+		assert_mock CustomerFinancials::REDIS
+		assert_mock CreditCardSale::REDIS
+		assert_mock TrustLevelRepo::REDIS
+		assert_mock TrustLevelRepo::DB
+	end
+	em :test_sale_too_many_declines
+
 	def test_sale
 		req = stub_request(
 			:post,