Merge branch 'activate-with-balance'

Stephen Paul Weber created

* activate-with-balance:
  If customer already has enough balance, just bill them and finish
  New BillPay registration step
  Save plan to DB as soon as it is selected
  Move credit_to lookup into relevant class

Change summary

config-schema.dhall       |  1 
config.dhall.sample       |  1 
lib/customer.rb           |  2 
lib/customer_plan.rb      | 26 ++++++++++++++++++++--
lib/registration.rb       | 47 ++++++++++++++++++++++++++--------------
test/test_customer.rb     |  5 ++++
test/test_helper.rb       |  1 
test/test_registration.rb | 19 +++++++++++-----
8 files changed, 75 insertions(+), 27 deletions(-)

Detailed changes

config-schema.dhall 🔗

@@ -1,4 +1,5 @@
 { activation_amount : Natural
+, activation_amount_accept : Natural
 , admins : List Text
 , adr : Text
 , approved_domains : List { mapKey : Text, mapValue : Optional Text }

config.dhall.sample 🔗

@@ -66,6 +66,7 @@ in
 	},
 	oxr_app_id = "",
 	activation_amount = 15,
+	activation_amount_accept = 15,
 	credit_card_url = \(jid: Text) -> \(customer_id: Text) ->
 		"https://pay.jmp.chat/${jid}/credit_cards?customer_id=${customer_id}",
 	electrum_notify_url = \(address: Text) -> \(customer_id: Text) ->

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 🔗

@@ -38,9 +38,10 @@ class Registration
 			jid = ProxiedJID.new(customer.jid).unproxied
 			if customer.active?
 				Finish.new(customer, tel)
+			elsif customer.balance >= CONFIG[:activation_amount_accept]
+				BillPlan.new(customer, tel)
 			elsif CONFIG[:approved_domains].key?(jid.domain.to_sym)
-				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
-				Allow.new(customer, tel, credit_to)
+				Allow.for(customer, tel, jid)
 			else
 				new(customer, tel)
 			end
@@ -86,6 +87,11 @@ class Registration
 		end
 
 		class Allow < Activation
+			def self.for(customer, tel, jid)
+				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
+				new(customer, tel, credit_to)
+			end
+
 			def initialize(customer, tel, credit_to)
 				super(customer, tel)
 				@credit_to = credit_to
@@ -132,9 +138,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
@@ -152,14 +160,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)
@@ -254,10 +255,8 @@ class Registration
 			protected
 
 				def sold(tx)
-					tx.insert.then {
-						@customer.bill_plan
-					}.then do
-						@finish.new(@customer, @tel).write
+					tx.insert.then do
+						BillPlan.new(@customer, @tel, finish: @finish).write
 					end
 				end
 
@@ -408,6 +407,20 @@ class Registration
 		end
 	end
 
+	class BillPlan
+		def initialize(customer, tel, finish: Finish)
+			@customer = customer
+			@tel = tel
+			@finish = finish
+		end
+
+		def write
+			@customer.bill_plan.then do
+				@finish.new(@customer, @tel).write
+			end
+		end
+	end
+
 	class Finish
 		def initialize(customer, tel)
 			@customer = customer

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_helper.rb 🔗

@@ -68,6 +68,7 @@ CONFIG = {
 	},
 	notify_from: "notify_from@example.org",
 	activation_amount: 1,
+	activation_amount_accept: 1,
 	plans: [
 		{
 			name: "test_usd",

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