Save plan to DB as soon as it is selected

Stephen Paul Weber created

Customer will appear as instantly expired (having been active for 1 second in
the immediate past).

This can replace the pending_plan key for BTC activations. It also serves as a
record for mail-in activation of what plan they had selected.

Once the account is really activated the tiny row is removed.

Change summary

lib/customer.rb           |  2 +-
lib/customer_plan.rb      | 26 +++++++++++++++++++++++---
lib/registration.rb       | 17 ++++++-----------
test/test_customer.rb     |  5 +++++
test/test_registration.rb | 19 +++++++++++++------
5 files changed, 48 insertions(+), 21 deletions(-)

Detailed changes

lib/customer.rb 🔗

@@ -25,7 +25,7 @@ class Customer
 	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
 	               :currency, :merchant_account, :plan_name, :minute_limit,
 	               :message_limit, :auto_top_up_amount, :monthly_overage_limit,
-	               :monthly_price
+	               :monthly_price, :save_plan!
 	def_delegators :@sgx, :register!, :registered?, :set_ogm_url,
 	               :fwd, :transcription_enabled
 	def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage

lib/customer_plan.rb 🔗

@@ -44,6 +44,21 @@ class CustomerPlan
 		)
 	end
 
+	def save_plan!
+		DB.exec_defer(<<~SQL, [@customer_id, plan_name])
+			INSERT INTO plan_log
+				(customer_id, plan_name, date_range)
+			VALUES (
+				$1,
+				$2,
+				tsrange(
+					LOCALTIMESTAMP - '2 seconds'::interval,
+					LOCALTIMESTAMP - '1 second'::interval
+				)
+			)
+		SQL
+	end
+
 	def bill_plan
 		EM.promise_fiber do
 			DB.transaction do
@@ -54,12 +69,17 @@ class CustomerPlan
 	end
 
 	def activate_plan_starting_now
-		DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive?
-			INSERT INTO plan_log
-				(customer_id, plan_name, date_range)
+		activated = DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive?
+			INSERT INTO plan_log (customer_id, plan_name, date_range)
 			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
 			ON CONFLICT DO NOTHING
 		SQL
+		return false unless activated
+
+		DB.exec(<<~SQL, [@customer_id])
+			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
+				AND upper(date_range) - lower(date_range) < '2 seconds'
+		SQL
 	end
 
 	def activation_date

lib/registration.rb 🔗

@@ -136,9 +136,11 @@ class Registration
 		def self.for(iq, customer, tel, final_message: nil, finish: Finish)
 			plan_name = iq.form.field("plan_name").value.to_s
 			customer = customer.with_plan(plan_name)
-			kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
-				raise "Invalid activation method"
-			}.call(customer, tel, final_message: final_message, finish: finish)
+			customer.save_plan!.then do
+				kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
+					raise "Invalid activation method"
+				}.call(customer, tel, final_message: final_message, finish: finish)
+			end
 		end
 
 		class Bitcoin
@@ -156,14 +158,7 @@ class Registration
 			attr_reader :customer_id, :tel
 
 			def save
-				EMPromise.all([
-					REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel),
-					REDIS.setex(
-						"pending_plan_for-#{customer_id}",
-						THIRTY_DAYS,
-						@customer.plan_name
-					)
-				])
+				REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel)
 			end
 
 			def note_text(amount, addr)

test/test_customer.rb 🔗

@@ -38,6 +38,11 @@ class CustomerTest < Minitest::Test
 			OpenStruct.new(cmd_tuples: 1),
 			[String, ["test", "test_usd"]]
 		)
+		CustomerPlan::DB.expect(
+			:exec,
+			OpenStruct.new(cmd_tuples: 0),
+			[String, ["test"]]
+		)
 		customer(plan_name: "test_usd").bill_plan.sync
 		CustomerPlan::DB.verify
 	end

test/test_registration.rb 🔗

@@ -245,10 +245,8 @@ class RegistrationTest < Minitest::Test
 
 		def test_for_bitcoin
 			cust = Minitest::Mock.new(customer)
-			cust.expect(
-				:add_btc_address,
-				EMPromise.resolve("testaddr")
-			)
+			cust.expect(:with_plan, cust, ["test_usd"])
+			cust.expect(:save_plan!, nil)
 			iq = Blather::Stanza::Iq::Command.new
 			iq.form.fields = [
 				{ var: "activation_method", value: "bitcoin" },
@@ -256,9 +254,13 @@ class RegistrationTest < Minitest::Test
 			]
 			result = Registration::Payment.for(iq, cust, "+15555550000")
 			assert_kind_of Registration::Payment::Bitcoin, result
+			assert_mock cust
 		end
 
 		def test_for_credit_card
+			cust = Minitest::Mock.new(customer)
+			cust.expect(:with_plan, cust, ["test_usd"])
+			cust.expect(:save_plan!, nil)
 			braintree_customer = Minitest::Mock.new
 			CustomerFinancials::BRAINTREE.expect(
 				:customer,
@@ -277,14 +279,18 @@ class RegistrationTest < Minitest::Test
 			]
 			result = Registration::Payment.for(
 				iq,
-				customer,
+				cust,
 				"+15555550000"
 			).sync
 			assert_kind_of Registration::Payment::CreditCard, result
+			assert_mock cust
 		end
 		em :test_for_credit_card
 
 		def test_for_code
+			cust = Minitest::Mock.new(customer)
+			cust.expect(:with_plan, cust, ["test_usd"])
+			cust.expect(:save_plan!, nil)
 			iq = Blather::Stanza::Iq::Command.new
 			iq.form.fields = [
 				{ var: "activation_method", value: "code" },
@@ -292,10 +298,11 @@ class RegistrationTest < Minitest::Test
 			]
 			result = Registration::Payment.for(
 				iq,
-				customer,
+				cust,
 				"+15555550000"
 			)
 			assert_kind_of Registration::Payment::InviteCode, result
+			assert_mock cust
 		end
 
 		class BitcoinTest < Minitest::Test