Block repeated declines for 24 hours

Stephen Paul Weber created

Change summary

lib/registration.rb       |  6 ++--
lib/transaction.rb        | 42 ++++++++++++++++++++++++++++++++--------
sgx_jmp.rb                |  6 ++--
test/test_registration.rb | 24 +++++++++++-----------
test/test_transaction.rb  | 37 ++++++++++++++++++++++++++++-------
5 files changed, 80 insertions(+), 35 deletions(-)

Detailed changes

lib/registration.rb 🔗

@@ -219,9 +219,9 @@ class Registration
 
 				def write
 					Transaction.sale(
-						@customer.merchant_account,
-						@payment_method,
-						CONFIG[:activation_amount]
+						@customer,
+						CONFIG[:activation_amount],
+						@payment_method
 					).then(
 						method(:sold),
 						->(_) { declined }

lib/transaction.rb 🔗

@@ -1,18 +1,42 @@
 # frozen_string_literal: true
 
 class Transaction
-	def self.sale(merchant_account, payment_method, amount)
-		BRAINTREE.transaction.sale(
-			amount: amount,
-			payment_method_token: payment_method.token,
-			merchant_account_id: merchant_account,
-			options: { submit_for_settlement: true }
-		).then do |response|
-			raise response.message unless response.success?
-			new(response.transaction)
+	def self.sale(customer, amount, payment_method=nil)
+		REDIS.get("jmp_pay_decline-#{customer.customer_id}").then do |declines|
+			raise "too many declines" if declines.to_i >= 2
+
+			BRAINTREE.transaction.sale(
+				amount: amount,
+				**sale_args_for(customer, payment_method)
+			).then do |response|
+				decline_guard(customer, response)
+				new(response.transaction)
+			end
 		end
 	end
 
+	def self.decline_guard(customer, response)
+		return if response.success?
+
+		REDIS.incr("jmp_pay_decline-#{customer.customer_id}").then do
+			REDIS.expire("jmp_pay_decline-#{customer.customer_id}", 60 * 60 * 24)
+		end
+		raise response.message
+	end
+
+	def self.sale_args_for(customer, payment_method=nil)
+		{
+			merchant_account_id: customer.merchant_account,
+			options: { submit_for_settlement: true }
+		}.merge(
+			if payment_method
+				{ payment_method_token: payment_method.token }
+			else
+				{ customer_id: customer.id }
+			end
+		)
+	end
+
 	attr_reader :amount
 
 	def initialize(braintree_transaction)

sgx_jmp.rb 🔗

@@ -205,17 +205,17 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
 		BuyAccountCreditForm.new(customer).add_to_form(reply.form).then { customer }
 	}.then { |customer|
 		EMPromise.all([
+			customer,
 			customer.payment_methods,
-			customer.merchant_account,
 			COMMAND_MANAGER.write(reply)
 		])
-	}.then { |(payment_methods, merchant_account, iq2)|
+	}.then { |(customer, payment_methods, iq2)|
 		iq = iq2 # This allows the catch to use it also
 		payment_method = payment_methods.fetch(
 			iq.form.field("payment_method")&.value.to_i
 		)
 		amount = iq.form.field("amount").value.to_s
-		Transaction.sale(merchant_account, payment_method, amount)
+		Transaction.sale(customer, amount, payment_method)
 	}.then { |transaction|
 		transaction.insert.then { transaction.amount }
 	}.then { |amount|

test/test_registration.rb 🔗

@@ -214,19 +214,19 @@ class RegistrationTest < Minitest::Test
 					:insert,
 					EMPromise.resolve(nil)
 				)
+				customer = Minitest::Mock.new(
+					Customer.new("test", plan_name: "test_usd")
+				)
 				Registration::Payment::CreditCard::Activate::Transaction.expect(
 					:sale,
 					transaction,
 					[
-						"merchant_usd",
-						:test_default_method,
-						CONFIG[:activation_amount]
+						customer,
+						CONFIG[:activation_amount],
+						:test_default_method
 					]
 				)
 				iq = Blather::Stanza::Iq::Command.new
-				customer = Minitest::Mock.new(
-					Customer.new("test", plan_name: "test_usd")
-				)
 				customer.expect(:bill_plan, nil)
 				Registration::Payment::CreditCard::Activate::Finish.expect(
 					:new,
@@ -247,20 +247,20 @@ class RegistrationTest < Minitest::Test
 			em :test_write
 
 			def test_write_declines
+				customer = Minitest::Mock.new(
+					Customer.new("test", plan_name: "test_usd")
+				)
 				Registration::Payment::CreditCard::Activate::Transaction.expect(
 					:sale,
 					EMPromise.reject("declined"),
 					[
-						"merchant_usd",
-						:test_default_method,
-						CONFIG[:activation_amount]
+						customer,
+						CONFIG[:activation_amount],
+						:test_default_method
 					]
 				)
 				iq = Blather::Stanza::Iq::Command.new
 				iq.from = "test@example.com"
-				customer = Minitest::Mock.new(
-					Customer.new("test", plan_name: "test_usd")
-				)
 				result = Minitest::Mock.new
 				result.expect(:then, nil)
 				Registration::Payment::CreditCard::Activate::COMMAND_MANAGER.expect(

test/test_transaction.rb 🔗

@@ -5,6 +5,7 @@ require "transaction"
 
 Transaction::DB = Minitest::Mock.new
 Transaction::BRAINTREE = Minitest::Mock.new
+Transaction::REDIS = Minitest::Mock.new
 
 class TransactionTest < Minitest::Test
 	FAKE_BRAINTREE_TRANSACTION =
@@ -16,26 +17,46 @@ class TransactionTest < Minitest::Test
 		)
 
 	def test_sale_fails
+		Transaction::REDIS.expect(
+			:get,
+			EMPromise.resolve("1"),
+			["jmp_pay_decline-test"]
+		)
+		Transaction::REDIS.expect(
+			:incr,
+			EMPromise.resolve(nil),
+			["jmp_pay_decline-test"]
+		)
+		Transaction::REDIS.expect(
+			:expire,
+			EMPromise.resolve(nil),
+			["jmp_pay_decline-test", 60 * 60 * 24]
+		)
 		braintree_transaction = Minitest::Mock.new
 		Transaction::BRAINTREE.expect(:transaction, braintree_transaction)
 		braintree_transaction.expect(
 			:sale,
 			EMPromise.resolve(
-				OpenStruct.new(success?: false)
+				OpenStruct.new(success?: false, message: "declined")
 			),
 			[Hash]
 		)
-		assert_raises do
+		assert_raises("declined") do
 			Transaction.sale(
-				"merchant_usd",
-				OpenStruct.new(token: "token"),
-				123
+				Customer.new("test", plan_name: "test_usd"),
+				123,
+				OpenStruct.new(token: "token")
 			).sync
 		end
 	end
 	em :test_sale_fails
 
 	def test_sale
+		Transaction::REDIS.expect(
+			:get,
+			EMPromise.resolve("1"),
+			["jmp_pay_decline-test"]
+		)
 		braintree_transaction = Minitest::Mock.new
 		Transaction::BRAINTREE.expect(:transaction, braintree_transaction)
 		braintree_transaction.expect(
@@ -54,9 +75,9 @@ class TransactionTest < Minitest::Test
 			}]
 		)
 		result = Transaction.sale(
-			"merchant_usd",
-			OpenStruct.new(token: "token"),
-			123
+			Customer.new("test", plan_name: "test_usd"),
+			123,
+			OpenStruct.new(token: "token")
 		).sync
 		assert_kind_of Transaction, result
 	end