~singpolyma/sgx-jmp

aab558d05d3d7a412f81ff1e44739cb0c5015d11 — Stephen Paul Weber 6 months ago d1899ac
Allow setting parent during signup using a special referral code

Lookup any referral code to see if it is one for setting a parent, if so set the
parent when we set the plan.  In the invite code flow, reload customer and check
balance and if there is enough then we can bill customer and proceed, no need to
add more credit or another code.

Verify parent when setting to make sure it has the same currency as the child
plan at creation time (note that updating the parent plan in the future can
violate this, so be very careful if/when we allow for that!)
M lib/customer.rb => lib/customer.rb +2 -2
@@ 81,10 81,10 @@ class Customer
		)
	end

	def with_plan(plan_name)
	def with_plan(plan_name, **kwargs)
		self.class.new(
			@customer_id, @jid,
			plan: @plan.with_plan_name(plan_name),
			plan: @plan.with_plan_name(plan_name, **kwargs),
			balance: @balance, tndetails: @tndetails, sgx: @sgx
		)
	end

M lib/customer_plan.rb => lib/customer_plan.rb +17 -2
@@ 70,15 70,29 @@ class CustomerPlan
		:expired
	end

	def with_plan_name(plan_name)
	def with_plan_name(plan_name, **kwargs)
		self.class.new(
			@customer_id,
			plan: Plan.for(plan_name),
			expires_at: @expires_at
			expires_at: @expires_at, **kwargs
		)
	end

	def verify_parent!
		return unless @parent_customer_id

		result = DB.query(<<~SQL, [@parent_customer_id])
			SELECT plan_name FROM customer_plans WHERE customer_id=$1
		SQL

		raise "Invalid parent account" if !result || !result.first

		plan = Plan.for(result.first["plan_name"])
		raise "Parent currency mismatch" unless plan.currency == currency
	end

	def save_plan!
		verify_parent!
		DB.exec_defer(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
			INSERT INTO plan_log
				(customer_id, plan_name, parent_customer_id, date_range)


@@ 107,6 121,7 @@ class CustomerPlan
	end

	def activate_plan_starting_now
		verify_parent!
		activated = DB.exec(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
			INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)

M lib/registration.rb => lib/registration.rb +40 -11
@@ 11,6 11,7 @@ require_relative "./command"
require_relative "./em"
require_relative "./invites_repo"
require_relative "./oob"
require_relative "./parent_code_repo"
require_relative "./proxied_jid"
require_relative "./tel_selections"
require_relative "./welcome_message"


@@ 105,7 106,7 @@ class Registration

		def next_step(iq)
			code = iq.form.field("code")&.value&.to_s
			save_customer_plan(iq).then {
			save_customer_plan(iq, code).then {
				finish_if_valid_invite(code)
			}.catch_only(InvitesRepo::Invalid) do
				@invites.stash_code(customer.customer_id, code).then do


@@ 124,10 125,12 @@ class Registration
			end
		end

		def save_customer_plan(iq)
			plan_name = iq.form.field("plan_name").value.to_s
			@customer = @customer.with_plan(plan_name)
			@customer.save_plan!
		def save_customer_plan(iq, code)
			ParentCodeRepo.new(REDIS).find(code).then do |parent|
				plan_name = iq.form.field("plan_name").value.to_s
				@customer = @customer.with_plan(plan_name, parent_customer_id: parent)
				@customer.save_plan!
			end
		end

		class GooglePlay


@@ 136,6 139,7 @@ class Registration
				@google_play_userid = google_play_userid
				@tel = tel
				@invites = InvitesRepo.new(DB, REDIS)
				@parent_code_repo = ParentCodeRepo.new(REDIS)
			end

			def used


@@ 163,17 167,25 @@ class Registration
			end

			def activate(iq)
				REDIS.sadd("google_play_userids", @google_play_userid).then {
					plan_name = iq.form.field("plan_name").value.to_s
					@customer = @customer.with_plan(plan_name)
					@customer.activate_plan_starting_now
				plan_name = iq.form.field("plan_name").value
				code = iq.form.field("code")&.value
				EMPromise.all([
					@parent_code_repo.find(code),
					REDIS.sadd("google_play_userids", @google_play_userid)
				]).then { |(parent, _)|
					save_active_plan(plan_name, parent)
				}.then do
					use_referral_code(iq.form.field("code")&.value&.to_s)
					use_referral_code(code)
				end
			end

		protected

			def save_active_plan(plan_name, parent)
				@customer = @customer.with_plan(plan_name, parent_customer_id: parent)
				@customer.activate_plan_starting_now
			end

			def use_referral_code(code)
				EMPromise.resolve(nil).then {
					@invites.claim_code(@customer.customer_id, code) {


@@ 406,7 418,24 @@ class Registration
		end

		class InviteCode
			Payment.kinds[:code] = method(:new)
			Payment.kinds[:code] = ->(*args, **kw) { self.for(*args, **kw) }

			def self.for(in_customer, tel, finish: Finish, **)
				reload_customer(in_customer).then do |customer|
					if customer.balance >= CONFIG[:activation_amount_accept]
						next BillPlan.new(customer, tel, finish: finish)
					end

					msg = if customer.balance.positive?
						"Account balance not enough to cover the activation"
					end
					new(customer, tel, error: msg)
				end
			end

			def self.reload_customer(customer)
				Command.execution.customer_repo.find(customer.customer_id)
			end

			FIELDS = [{
				var: "code",

M test/test_customer.rb => test/test_customer.rb +5 -0
@@ 49,6 49,11 @@ class CustomerTest < Minitest::Test
	em :test_bill_plan_activate

	def test_bill_plan_reactivate_child
		CustomerPlan::DB.expect(
			:query,
			[{ "plan_name" => "test_usd" }],
			[String, ["parent"]]
		)
		CustomerPlan::DB.expect(:transaction, nil) do |&block|
			block.call
			true

M test/test_customer_repo.rb => test/test_customer_repo.rb +5 -0
@@ 195,6 195,11 @@ class CustomerRepoTest < Minitest::Test
			["jmp_customer_feature_flags-testp"]
		)
		CustomerPlan::DB.expect(
			:query,
			[{ "plan_name" => "test_usd" }],
			[String, ["1234"]]
		)
		CustomerPlan::DB.expect(
			:exec_defer,
			EMPromise.resolve(nil),
			[String, ["testp", "test_usd", "1234"]]

M test/test_registration.rb => test/test_registration.rb +66 -9
@@ 112,7 112,9 @@ class RegistrationTest < Minitest::Test

	class ActivationTest < Minitest::Test
		Registration::Activation::DB = Minitest::Mock.new
		Registration::Activation::REDIS = FakeRedis.new
		Registration::Activation::REDIS = FakeRedis.new(
			"jmp_parent_codes" => { "PARENT_CODE" => 1 }
		)
		Registration::Activation::Payment = Minitest::Mock.new
		Registration::Activation::Finish = Minitest::Mock.new
		Command::COMMAND_MANAGER = Minitest::Mock.new


@@ 136,7 138,9 @@ class RegistrationTest < Minitest::Test
					)
				end]
			)
			@customer.expect(:with_plan, @customer, ["test_usd"])
			@customer.expect(:with_plan, @customer) do |*args, **|
				assert_equal ["test_usd"], args
			end
			@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
			Registration::Activation::Payment.expect(
				:for,


@@ 170,7 174,9 @@ class RegistrationTest < Minitest::Test
					)
				end]
			)
			@customer.expect(:with_plan, @customer, ["test_usd"])
			@customer.expect(:with_plan, @customer) do |*args, **|
				assert_equal ["test_usd"], args
			end
			@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
			@customer.expect(:activate_plan_starting_now, EMPromise.resolve(nil), [])
			Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call }


@@ 212,7 218,9 @@ class RegistrationTest < Minitest::Test
					)
				end]
			)
			@customer.expect(:with_plan, @customer, ["test_usd"])
			@customer.expect(:with_plan, @customer) do |*args, **|
				assert_equal ["test_usd"], args
			end
			@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
			Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call }
			Registration::Activation::DB.expect(


@@ 241,6 249,50 @@ class RegistrationTest < Minitest::Test
			assert_mock Registration::Activation::DB
		end
		em :test_write_with_group_code

		def test_write_with_parent_code
			Command::COMMAND_MANAGER.expect(
				:write,
				EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
					iq.form.fields = [
						{ var: "plan_name", value: "test_usd" },
						{ var: "code", value: "PARENT_CODE" }
					]
				}),
				[Matching.new do |iq|
					assert_equal :form, iq.form.type
					assert_equal(
						"You've selected +15555550000 as your JMP number.",
						iq.form.instructions.lines.first.chomp
					)
				end]
			)
			@customer.expect(:with_plan, @customer) do |*args, **kwargs|
				assert_equal ["test_usd"], args
				assert_equal({ parent_customer_id: 1 }, kwargs)
			end
			@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
			Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call }
			Registration::Activation::DB.expect(
				:exec,
				OpenStruct.new(cmd_tuples: 0),
				[String, ["test", "PARENT_CODE"]]
			)
			Registration::Activation::Payment.expect(
				:for,
				EMPromise.reject(:test_result),
				[Blather::Stanza::Iq, @customer, "+15555550000"]
			)
			assert_equal(
				:test_result,
				execute_command { @activation.write.catch { |e| e } }
			)
			assert_mock Command::COMMAND_MANAGER
			assert_mock @customer
			assert_mock Registration::Activation::Payment
			assert_mock Registration::Activation::DB
		end
		em :test_write_with_parent_code
	end

	class AllowTest < Minitest::Test


@@ 426,13 478,18 @@ class RegistrationTest < Minitest::Test
				{ var: "activation_method", value: "code" },
				{ var: "plan_name", value: "test_usd" }
			]
			result = Registration::Payment.for(
				iq,
				customer,
				"+15555550000"
			)
			cust = customer
			result = execute_command do
				Command.execution.customer_repo.expect(:find, cust, ["test"])
				Registration::Payment.for(
					iq,
					cust,
					"+15555550000"
				)
			end
			assert_kind_of Registration::Payment::InviteCode, result
		end
		em :test_for_code

		class BitcoinTest < Minitest::Test
			Registration::Payment::Bitcoin::BTC_SELL_PRICES = Minitest::Mock.new