When an auto top up fails, don't retry that card soon

Stephen Paul Weber created

for any auto top up

Change summary

lib/low_balance.rb       | 33 ++++++++++++++++++--
test/test_helper.rb      |  6 +++
test/test_low_balance.rb | 66 ++++++++++++++++++++++++++++++++++++++++++
test/test_web.rb         | 21 +++++++++++++
4 files changed, 122 insertions(+), 4 deletions(-)

Detailed changes

lib/low_balance.rb 🔗

@@ -15,9 +15,9 @@ class LowBalance
 		end
 	end
 
-	def self.for_no_lock(customer)
-		if customer.auto_top_up_amount.positive?
-			AutoTopUp.new(customer)
+	def self.for_no_lock(customer, auto: true)
+		if auto && customer.auto_top_up_amount.positive?
+			AutoTopUp.for(customer)
 		else
 			customer.btc_addresses.then do |btc_addresses|
 				new(customer, btc_addresses)
@@ -48,8 +48,28 @@ class LowBalance
 	end
 
 	class AutoTopUp
-		def initialize(customer)
+		def self.for(customer)
+			customer.payment_methods.then(&:default_payment_method).then do |method|
+				blocked?(method).then do |block|
+					next AutoTopUp.new(customer, method) if block.zero?
+
+					log.info("#{customer.customer_id} auto top up blocked")
+					LowBalance.for_no_lock(customer, auto: false)
+				end
+			end
+		end
+
+		def self.blocked?(method)
+			return EMPromise.resolve(1) if method.nil?
+
+			REDIS.exists(
+				"jmp_auto_top_up_block-#{method&.unique_number_identifier}"
+			)
+		end
+
+		def initialize(customer, method=nil)
 			@customer = customer
+			@method = method
 			@message = Blather::Stanza::Message.new
 			@message.from = CONFIG[:notify_from]
 		end
@@ -64,6 +84,11 @@ class LowBalance
 		end
 
 		def failed(e)
+			@method && REDIS.setex(
+				"jmp_auto_top_up_block-#{@method.unique_number_identifier}",
+				60 * 60 * 24 * 7,
+				Time.now
+			)
 			@message.body =
 				"Automatic top-up transaction for " \
 				"$#{@customer.auto_top_up_amount} failed: #{e.message}"

test/test_helper.rb 🔗

@@ -119,8 +119,14 @@ LOG = Class.new {
 	def child(*)
 		Minitest::Mock.new
 	end
+
+	def info(*); end
 }.new.freeze
 
+def log
+	LOG
+end
+
 BLATHER = Class.new {
 	def <<(*); end
 }.new.freeze

test/test_low_balance.rb 🔗

@@ -6,6 +6,7 @@ require "low_balance"
 ExpiringLock::REDIS = Minitest::Mock.new
 CustomerPlan::REDIS = Minitest::Mock.new
 CustomerFinancials::REDIS = Minitest::Mock.new
+LowBalance::AutoTopUp::REDIS = Minitest::Mock.new
 
 class LowBalanceTest < Minitest::Test
 	def test_for_locked
@@ -43,14 +44,79 @@ class LowBalanceTest < Minitest::Test
 			EMPromise.resolve("OK"),
 			["jmp_customer_low_balance-test", Time, "EX", 604800, "NX"]
 		)
+		CustomerFinancials::REDIS.expect(
+			:smembers,
+			EMPromise.resolve([]),
+			["block_credit_cards"]
+		)
+		LowBalance::AutoTopUp::REDIS.expect(
+			:exists,
+			0,
+			["jmp_auto_top_up_block-abcd"]
+		)
+		braintree_customer = Minitest::Mock.new
+		CustomerFinancials::BRAINTREE.expect(:customer, braintree_customer)
+		payment_methods = OpenStruct.new(payment_methods: [
+			OpenStruct.new(default?: true, unique_number_identifier: "abcd")
+		])
+		braintree_customer.expect(
+			:find,
+			EMPromise.resolve(payment_methods),
+			["test"]
+		)
 		assert_kind_of(
 			LowBalance::AutoTopUp,
 			LowBalance.for(customer(auto_top_up_amount: 15)).sync
 		)
 		assert_mock ExpiringLock::REDIS
+		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerFinancials::BRAINTREE
+		assert_mock braintree_customer
 	end
 	em :test_for_auto_top_up
 
+	def test_for_auto_top_up_blocked
+		ExpiringLock::REDIS.expect(
+			:set,
+			EMPromise.resolve("OK"),
+			["jmp_customer_low_balance-test", Time, "EX", 604800, "NX"]
+		)
+		CustomerFinancials::REDIS.expect(
+			:smembers,
+			EMPromise.resolve([]),
+			["block_credit_cards"]
+		)
+		CustomerFinancials::REDIS.expect(
+			:smembers,
+			EMPromise.resolve([]),
+			["jmp_customer_btc_addresses-test"]
+		)
+		LowBalance::AutoTopUp::REDIS.expect(
+			:exists,
+			1,
+			["jmp_auto_top_up_block-blocked"]
+		)
+		braintree_customer = Minitest::Mock.new
+		CustomerFinancials::BRAINTREE.expect(:customer, braintree_customer)
+		payment_methods = OpenStruct.new(payment_methods: [
+			OpenStruct.new(default?: true, unique_number_identifier: "blocked")
+		])
+		braintree_customer.expect(
+			:find,
+			EMPromise.resolve(payment_methods),
+			["test"]
+		)
+		assert_kind_of(
+			LowBalance,
+			LowBalance.for(customer(auto_top_up_amount: 15)).sync
+		)
+		assert_mock ExpiringLock::REDIS
+		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerFinancials::BRAINTREE
+		assert_mock braintree_customer
+	end
+	em :test_for_auto_top_up_blocked
+
 	class AutoTopUpTest < Minitest::Test
 		LowBalance::AutoTopUp::Transaction = Minitest::Mock.new
 

test/test_web.rb 🔗

@@ -170,6 +170,27 @@ class WebTest < Minitest::Test
 			["jmp_customer_low_balance-customerid_topup", Time, "EX", 604800, "NX"]
 		)
 
+		CustomerFinancials::REDIS.expect(
+			:smembers,
+			EMPromise.resolve([]),
+			["block_credit_cards"]
+		)
+		LowBalance::AutoTopUp::REDIS.expect(
+			:exists,
+			0,
+			["jmp_auto_top_up_block-abcd"]
+		)
+		braintree_customer = Minitest::Mock.new
+		CustomerFinancials::BRAINTREE.expect(:customer, braintree_customer)
+		payment_methods = OpenStruct.new(payment_methods: [
+			OpenStruct.new(default?: true, unique_number_identifier: "abcd")
+		])
+		braintree_customer.expect(
+			:find,
+			EMPromise.resolve(payment_methods),
+			["customerid_topup"]
+		)
+
 		Customer::BLATHER.expect(
 			:<<,
 			nil,