Restrict new users from outbound call/text

Stephen Paul Weber created

Create a new trust level "Cellar" that cannot call or text or
invite (simlar to Tomb, but semantically different and can pay etc).

This is now the default trust level until an account receives at least
one text "from a person". Accounts before a certain date are
grandfathered in to now get this level. Port ins will get to bypass this
because the reachability SMS reply will count as a text from a person.
Accounts invited in any way except a group code will also bypass Cellar.

The instructions on the payment form and the welcome message are both
updated to warn of this new restriction. Note that the welcome message
is conditional to only include the warning if it applies to the customer
by that point.

Texts from short codes or with links or codes or from support do not
count as "from a person". There is also a block list of numbers in redis
to restrict what numbers are "from a person". The number that got an
account past Cellar is stored for possible addition to the block list.

Change summary

forms/registration/activate.rb       |  2 
lib/invites_repo.rb                  | 26 ++++---
lib/registration.rb                  | 32 +++++++-
lib/trust_level.rb                   | 44 +++++++++++--
lib/trust_level_repo.rb              | 32 +++++++++
lib/welcome_message.rb               | 30 +++++++-
sgx_jmp.rb                           |  7 +
test/test_admin_command.rb           | 12 +++
test/test_buy_account_credit_form.rb | 14 ++++
test/test_credit_card_sale.rb        | 84 +++++++++++++++++++++++++
test/test_helper.rb                  | 10 +++
test/test_registration.rb            | 49 +++++++++++++-
test/test_trust_level_repo.rb        | 66 +++++++++++++++++++
test/test_web.rb                     | 98 +++++++++++++++++++++++++++++
14 files changed, 464 insertions(+), 42 deletions(-)

Detailed changes

forms/registration/activate.rb 🔗

@@ -5,6 +5,8 @@ instructions <<~I
 	You've selected #{@tel} as your JMP number.
 	To activate your account, you can either deposit $#{'%.2f' % (CONFIG[:activation_amount] + @tel.price)} to your balance or enter your referral code if you have one.
 	(If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
+
+	After payment is complete, your number will be activated for inbound calls and texts. Calling out, or sending a text, will often be restricted until you receive at least one text from a person or port in a number.
 I
 
 field(

lib/invites_repo.rb 🔗

@@ -3,19 +3,25 @@
 require "multibases"
 require "securerandom"
 
+require_relative "trust_level_repo"
+
 class InvitesRepo
 	class Invalid < StandardError; end
 
 	def initialize(db=DB, redis=REDIS)
 		@db = db
 		@redis = redis
+		@trust_level_repo = TrustLevelRepo.new(db: db, redis: redis)
 	end
 
-	def unused_invites(customer_id)
-		promise = @db.query_defer(<<~SQL, [customer_id])
-			SELECT code FROM unused_invites WHERE creator_id=$1
-		SQL
-		promise.then { |result| result.map { |row| row["code"] } }
+	def unused_invites(customer)
+		@trust_level_repo.find(customer).then { |tl|
+			next [] unless tl.invite?
+
+			@db.query_defer(<<~SQL, [customer_id])
+				SELECT code FROM unused_invites WHERE creator_id=$1
+			SQL
+		}.then { |result| result.map { |row| row["code"] } }
 	end
 
 	def find_or_create_group_code(customer_id)
@@ -44,7 +50,7 @@ class InvitesRepo
 			]).then do |(_, credit_to)|
 				next false if credit_to.to_s.strip == ""
 
-				create_claimed_code(credit_to, customer_id)
+				create_claimed_code(credit_to, customer_id, trusted: false)
 				credit_to
 			end
 		end
@@ -69,10 +75,10 @@ class InvitesRepo
 		end
 	end
 
-	def create_claimed_code(creator_id, used_by_id)
-		@db.exec(<<~SQL, [creator_id, used_by_id])
-			INSERT INTO invites (creator_id, used_by_id, used_at)
-			VALUES ($1, $2, LOCALTIMESTAMP)
+	def create_claimed_code(creator_id, used_by_id, trusted: true)
+		@db.exec(<<~SQL, [creator_id, used_by_id, trusted])
+			INSERT INTO invites (creator_id, used_by_id, used_at, trusted)
+			VALUES ($1, $2, LOCALTIMESTAMP, $3)
 		SQL
 	end
 

lib/registration.rb 🔗

@@ -718,10 +718,17 @@ class Registration
 	class Finish
 		TN_UNAVAILABLE = "The JMP number %s is no longer available."
 
-		def initialize(customer, tel)
+		def initialize(
+			customer, tel,
+			trust_level_repo: TrustLevelRepo.new(
+				db: LazyObject.new { DB },
+				redis: LazyObject.new { REDIS }
+			)
+		)
 			@customer = customer
 			@tel = tel
 			@invites = InvitesRepo.new(DB, REDIS)
+			@trust_level_repo = trust_level_repo
 		end
 
 		def write
@@ -796,18 +803,27 @@ class Registration
 					@tel.charge(customer)
 				])
 			}.then do
-				FinishOnboarding.for(customer, @tel).then(&:write)
+				FinishOnboarding.for(
+					customer, @tel, trust_level_repo: @trust_level_repo
+				).then(&:write)
 			end
 		end
 	end
 
 	module FinishOnboarding
-		def self.for(customer, tel, db: LazyObject.new { DB })
+		def self.for(
+			customer, tel,
+			db: LazyObject.new { DB },
+			trust_level_repo: TrustLevelRepo.new(
+				db: LazyObject.new { DB },
+				redis: LazyObject.new { REDIS }
+			)
+		)
 			jid = ProxiedJID.new(customer.jid).unproxied
 			if jid.domain == CONFIG[:onboarding_domain]
 				Snikket.for(customer, tel, db: db)
 			else
-				NotOnboarding.new(customer, tel)
+				NotOnboarding.new(customer, tel, trust_level_repo: trust_level_repo)
 			end
 		end
 
@@ -1006,13 +1022,17 @@ class Registration
 		end
 
 		class NotOnboarding
-			def initialize(customer, tel)
+			def initialize(customer, tel, trust_level_repo:)
 				@customer = customer
 				@tel = tel
+				@trust_level_repo = trust_level_repo
 			end
 
 			def write
-				WelcomeMessage.new(@customer, @tel).welcome
+				WelcomeMessage.for(
+					@customer, @tel,
+					trust_level_repo: @trust_level_repo
+				).then(&:welcome)
 				Command.finish("Your JMP account has been activated as #{@tel}")
 			end
 		end

lib/trust_level.rb 🔗

@@ -3,12 +3,15 @@
 require "delegate"
 
 module TrustLevel
-	def self.for(plan_name:, settled_amount: 0, manual: nil)
+	def self.for(
+		customer:, settled_amount: 0, manual: nil,
+		activated: nil, invited: false, activater: nil
+	)
 		@levels.each do |level|
 			tl = level.call(
-				plan_name: plan_name,
-				settled_amount: settled_amount,
-				manual: manual
+				customer: customer,
+				settled_amount: settled_amount, activated: activated,
+				manual: manual, invited: invited, activater: activater
 			)
 			return manual ? Manual.new(tl) : tl if tl
 		end
@@ -32,6 +35,10 @@ module TrustLevel
 			new if manual == "Tomb"
 		end
 
+		def invite?
+			false
+		end
+
 		def write_cdr?
 			false
 		end
@@ -70,8 +77,13 @@ module TrustLevel
 	end
 
 	class Cellar
-		TrustLevel.register do |manual:, **|
-			new if manual == "Cellar"
+		TrustLevel.register do |manual:, activated:, invited:, activater:, **|
+			new if manual == "Cellar" || (!manual && !activater && !invited && \
+				(!activated || activated > Time.parse("2026-02-26")))
+		end
+
+		def invite?
+			false
 		end
 
 		def write_cdr?
@@ -117,6 +129,10 @@ module TrustLevel
 			new if manual == "Basement" || (!manual && settled_amount < 10)
 		end
 
+		def invite?
+			true
+		end
+
 		def write_cdr?
 			true
 		end
@@ -171,6 +187,10 @@ module TrustLevel
 			new if manual == "Paragon" || (!manual && settled_amount > 60)
 		end
 
+		def invite?
+			true
+		end
+
 		def write_cdr?
 			true
 		end
@@ -225,6 +245,10 @@ module TrustLevel
 			new if manual == "Olympias"
 		end
 
+		def invite?
+			true
+		end
+
 		def write_cdr?
 			true
 		end
@@ -253,12 +277,12 @@ module TrustLevel
 	end
 
 	class Customer
-		TrustLevel.register do |manual:, plan_name:, **|
+		TrustLevel.register do |manual:, customer:, **|
 			if manual && manual != "Customer"
 				Sentry.capture_message("Unknown TrustLevel: #{manual}")
 			end
 
-			new(plan_name)
+			new(customer.plan_name)
 		end
 
 		EXPENSIVE_ROUTE = {
@@ -272,6 +296,10 @@ module TrustLevel
 			@max_rate = EXPENSIVE_ROUTE.fetch(plan_name, 0.1)
 		end
 
+		def invite?
+			true
+		end
+
 		def write_cdr?
 			true
 		end

lib/trust_level_repo.rb 🔗

@@ -14,11 +14,17 @@ class TrustLevelRepo
 	def find(customer)
 		EMPromise.all([
 			find_manual(customer.customer_id),
+			redis.get("jmp_customer_activater-#{customer.customer_id}"),
+			customer.activation_date,
+			fetch_was_invited(customer.customer_id),
 			fetch_settled_amount(customer.billing_customer_id)
-		]).then do |(manual, row)|
+		]).then do |(manual, activater, activated, invited, row)|
 			TrustLevel.for(
+				customer: customer,
 				manual: manual,
-				plan_name: customer.plan_name,
+				activated: activated,
+				invited: invited[:used_at],
+				activater: activater,
 				**row
 			)
 		end
@@ -36,6 +42,22 @@ class TrustLevelRepo
 		end
 	end
 
+	def incoming_message(customer, stanza)
+		from = stanza.from.node.to_s
+		return if from =~ /^$|^[^+]/ # don't count short codes
+
+		body = m.body.to_s
+		return if body =~ /^$|http|code/i
+
+		redis.sismember("jmp_blocked_activation_source", from).then do |blocked|
+			next if blocked
+
+			redis.set(
+				"jmp_customer_activater-#{customer.customer_id}", stanza.from.node, "NX"
+			)
+		end
+	end
+
 protected
 
 	def fetch_settled_amount(customer_id)
@@ -44,4 +66,10 @@ protected
 			WHERE customer_id=$1 AND settled_after < LOCALTIMESTAMP AND amount > 0
 		SQL
 	end
+
+	def fetch_was_invited(customer_id)
+		db.query_one(<<~SQL, customer_id, default: {})
+			SELECT used_at FROM invites WHERE used_by_id=$1 AND trusted LIMIT 1
+		SQL
+	end
 end

lib/welcome_message.rb 🔗

@@ -1,9 +1,18 @@
 # frozen_string_literal: true
 
+require_relative "trust_level_repo"
+
 class WelcomeMessage
-	def initialize(customer, tel)
+	def self.for(customer, tel, trust_level_repo: TrustLevelRepo.new)
+		trust_level_repo.find(customer).then do |tl|
+			new(customer, tel, tl)
+		end
+	end
+
+	def initialize(customer, tel, trust_level)
 		@customer = customer
 		@tel = tel
+		@trust_level = trust_level
 	end
 
 	def welcome
@@ -13,15 +22,24 @@ class WelcomeMessage
 		end
 	end
 
+	def warning
+		return if @trust_level.support_call?(0, 0) && @trust_level.send_message?(0)
+
+		"\n\nYour account is activated for inbound calls and texts, but you " \
+		"won't be able to call out or send a text until you receive at least " \
+		"one text from a person at your new number, or port in a number from " \
+		"outside."
+	end
+
 	def message
 		m = Blather::Stanza::Message.new
 		m.from = CONFIG[:notify_from]
 		m.body =
-			"Welcome to JMP!  Your JMP number is #{@tel}. This is an automated " \
-			"message, but anything you send here will go direct to real humans " \
-			"who will be happy to help you.  Such support requests will get a " \
-			"reply within 8 business hours.\n\n" \
-			"FAQ: https://jmp.chat/faq\n\n" \
+			"Welcome to JMP! Your JMP number is #{@tel}.#{warning}\n\nThis is an " \
+			"automated message, but anything you send here will go direct to real " \
+			"humans who will be happy to help you.  Such support requests will " \
+			"get a reply within 8 business hours.\n\n" \
+			"FAQ: https://jmp.chat/faq\n" \
 			"Account Settings: xmpp:cheogram.com?command"
 		m
 	end

sgx_jmp.rb 🔗

@@ -276,6 +276,8 @@ before nil, to: /\Acustomer_/, from: /(\A|@)#{FROM_BACKEND}(\/|\Z)/ do |s|
 	CustomerRepo.new(set_user: Sentry.method(:set_user)).find(
 		s.to.node.delete_prefix("customer_")
 	).then do |customer|
+		# Intentionally called outside filter so ported-in numbers activate here
+		TrustLevelRepo.new.incoming_message(customer, s)
 		ReachabilityRepo::SMS.new
 			.find(customer, s.from.node, stanza: s).then do |reach|
 				reach.filter do
@@ -304,6 +306,7 @@ message(
 	CustomerRepo
 		.new(set_user: Sentry.method(:set_user))
 		.find_by_jid(address["jid"]).then { |customer|
+			TrustLevelRepo.new.incoming_message(customer, s)
 			m.from = m.from.with(domain: CONFIG[:component][:jid])
 			m.to = m.to.with(domain: customer.jid.domain)
 			address["jid"] = customer.jid.to_s
@@ -716,7 +719,7 @@ Command.new(
 	Command.customer.then { |customer|
 		EMPromise.all([
 			repo.find_or_create_group_code(customer.customer_id),
-			repo.unused_invites(customer.customer_id)
+			repo.unused_invites(customer)
 		])
 	}.then do |(group_code, invites)|
 		if invites.empty?
@@ -1092,7 +1095,7 @@ Command.new(
 			jid = ProxiedJID.new(customer.jid).unproxied
 			if jid.domain == CONFIG[:onboarding_domain]
 				CustomerRepo.new.find(customer.customer_id).then do |cust|
-					WelcomeMessage.new(cust, customer.registered?.phone).welcome
+					WelcomeMessage.for(cust, customer.registered?.phone).then(&:welcome)
 				end
 			end
 			Command.finish { |reply|

test/test_admin_command.rb 🔗

@@ -164,6 +164,18 @@ class AdminCommandTest < Minitest::Test
 				["jmp_customer_trust_level-testuser"]
 			)
 
+			TrustLevelRepo::REDIS.expect(
+				:get,
+				EMPromise.resolve(nil),
+				["jmp_customer_activater-testuser"]
+			)
+
+			TrustLevelRepo::DB.expect(
+				:query_one,
+				EMPromise.resolve({}),
+				[String, "testuser"], default: {}
+			)
+
 			TrustLevelRepo::DB.expect(
 				:query_one,
 				EMPromise.resolve({ settled_amount: 0 }),

test/test_buy_account_credit_form.rb 🔗

@@ -36,6 +36,19 @@ class BuyAccountCreditFormTest < Minitest::Test
 			EMPromise.resolve("Customer"),
 			["jmp_customer_trust_level-test"]
 		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve(nil),
+			["jmp_customer_activater-test"]
+		)
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
 		TrustLevelRepo::DB.expect(
 			:query_one,
 			EMPromise.resolve({}),
@@ -47,6 +60,7 @@ class BuyAccountCreditFormTest < Minitest::Test
 			BuyAccountCreditForm.for(customer).sync
 		)
 
+		assert_mock CustomerPlan::DB
 		assert_mock TrustLevelRepo::REDIS
 		assert_mock TrustLevelRepo::DB
 	end

test/test_credit_card_sale.rb 🔗

@@ -32,6 +32,19 @@ class CreditCardSaleTest < Minitest::Test
 			EMPromise.resolve("Customer"),
 			["jmp_customer_trust_level-test"]
 		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve(nil),
+			["jmp_customer_activater-test"]
+		)
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
 		TrustLevelRepo::DB.expect(
 			:query_one,
 			EMPromise.resolve({}),
@@ -72,6 +85,7 @@ class CreditCardSaleTest < Minitest::Test
 			).sale.sync
 		end
 		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerPlan::DB
 		assert_mock CreditCardSale::REDIS
 		assert_mock TrustLevelRepo::REDIS
 		assert_mock TrustLevelRepo::DB
@@ -89,6 +103,19 @@ class CreditCardSaleTest < Minitest::Test
 			EMPromise.resolve("Customer"),
 			["jmp_customer_trust_level-test"]
 		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve(nil),
+			["jmp_customer_activater-test"]
+		)
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
 		TrustLevelRepo::DB.expect(
 			:query_one,
 			EMPromise.resolve({}),
@@ -107,6 +134,7 @@ class CreditCardSaleTest < Minitest::Test
 			).sale.sync
 		end
 		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerPlan::DB
 		assert_mock CreditCardSale::REDIS
 		assert_mock TrustLevelRepo::REDIS
 		assert_mock TrustLevelRepo::DB
@@ -124,6 +152,19 @@ class CreditCardSaleTest < Minitest::Test
 			EMPromise.resolve("Customer"),
 			["jmp_customer_trust_level-test"]
 		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve(nil),
+			["jmp_customer_activater-test"]
+		)
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
 		TrustLevelRepo::DB.expect(
 			:query_one,
 			EMPromise.resolve({}),
@@ -144,6 +185,7 @@ class CreditCardSaleTest < Minitest::Test
 		end
 
 		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerPlan::DB
 		assert_mock CreditCardSale::REDIS
 		assert_mock TrustLevelRepo::REDIS
 		assert_mock TrustLevelRepo::DB
@@ -161,6 +203,19 @@ class CreditCardSaleTest < Minitest::Test
 			EMPromise.resolve("Customer"),
 			["jmp_customer_trust_level-test"]
 		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve("Customer"),
+			["jmp_customer_activater-test"]
+		)
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
 		TrustLevelRepo::DB.expect(
 			:query_one,
 			EMPromise.resolve({}),
@@ -181,6 +236,7 @@ class CreditCardSaleTest < Minitest::Test
 		end
 
 		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerPlan::DB
 		assert_mock CreditCardSale::REDIS
 		assert_mock TrustLevelRepo::REDIS
 		assert_mock TrustLevelRepo::DB
@@ -218,6 +274,19 @@ class CreditCardSaleTest < Minitest::Test
 			EMPromise.resolve("Customer"),
 			["jmp_customer_trust_level-test"]
 		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve(nil),
+			["jmp_customer_activater-test"]
+		)
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
 		TrustLevelRepo::DB.expect(
 			:query_one,
 			EMPromise.resolve({}),
@@ -255,6 +324,7 @@ class CreditCardSaleTest < Minitest::Test
 		).sale.sync
 		assert_equal FAKE_BRAINTREE_TRANSACTION, result
 		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerPlan::DB
 		assert_mock CreditCardSale::REDIS
 		assert_mock TrustLevelRepo::REDIS
 		assert_mock TrustLevelRepo::DB
@@ -309,6 +379,19 @@ class CreditCardSaleTest < Minitest::Test
 			EMPromise.resolve("Customer"),
 			["jmp_customer_trust_level-test"]
 		)
+		TrustLevelRepo::REDIS.expect(
+			:get,
+			EMPromise.resolve(nil),
+			["jmp_customer_activater-test"]
+		)
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+		TrustLevelRepo::DB.expect(
+			:query_one,
+			EMPromise.resolve({}),
+			[String, "test"], default: {}
+		)
 		TrustLevelRepo::DB.expect(
 			:query_one,
 			EMPromise.resolve({}),
@@ -367,6 +450,7 @@ class CreditCardSaleTest < Minitest::Test
 		assert_mock transaction_class
 		assert_mock transaction
 		assert_mock CustomerFinancials::REDIS
+		assert_mock CustomerPlan::DB
 		assert_mock CreditCardSale::REDIS
 		assert_mock TrustLevelRepo::REDIS
 		assert_mock TrustLevelRepo::DB

test/test_helper.rb 🔗

@@ -478,6 +478,16 @@ class FakeIBRRepo
 	end
 end
 
+class FakeTrustLevelRepo
+	def initialize(levels)
+		@levels = levels
+	end
+
+	def find(customer)
+		TrustLevel.for(customer: customer, manual: @levels[customer.customer_id])
+	end
+end
+
 module EventMachine
 	class << self
 		# Patch EM.add_timer to be instant in tests

test/test_registration.rb 🔗

@@ -220,6 +220,7 @@ class RegistrationTest < Minitest::Test
 				"PARENT_TOMB" => "2",
 				"PARENT_MANY_SUBACCOUNTS" => "many_subaccounts"
 			},
+			"jmp_customer_trust_level-1" => "Basement",
 			"jmp_customer_trust_level-2" => "Tomb"
 		)
 		Registration::Activation::Payment = Minitest::Mock.new
@@ -436,6 +437,12 @@ class RegistrationTest < Minitest::Test
 						)
 					end]
 				)
+				CustomerPlan::DB.expect(
+					:query_one, {}, [String, "1"]
+				)
+				Registration::Activation::DB.expect(
+					:query_one, {}, [String, "1"], default: {}
+				)
 				Registration::Activation::DB.expect(
 					:query_one, {}, [String, "1"], default: {}
 				)
@@ -466,6 +473,7 @@ class RegistrationTest < Minitest::Test
 				)
 			end
 			assert_mock Command::COMMAND_MANAGER
+			assert_mock CustomerPlan::DB
 			assert_mock @customer
 			assert_mock Registration::Activation::Payment
 			assert_mock Registration::Activation::DB
@@ -490,6 +498,12 @@ class RegistrationTest < Minitest::Test
 					)
 				end]
 			)
+			CustomerPlan::DB.expect(
+				:query_one, {}, [String, "1"]
+			)
+			Registration::Activation::DB.expect(
+				:query_one, {}, [String, "1"], default: {}
+			)
 			Registration::Activation::DB.expect(
 				:query_one, {}, [String, "1"], default: {}
 			)
@@ -531,6 +545,12 @@ class RegistrationTest < Minitest::Test
 					)
 				end]
 			)
+			CustomerPlan::DB.expect(
+				:query_one, {}, [String, "2"]
+			)
+			Registration::Activation::DB.expect(
+				:query_one, {}, [String, "2"], default: {}
+			)
 			Registration::Activation::DB.expect(
 				:query_one, {}, [String, "2"], default: {}
 			)
@@ -542,6 +562,7 @@ class RegistrationTest < Minitest::Test
 				execute_command { @activation.write.catch(&:to_s) }
 			)
 			assert_mock Command::COMMAND_MANAGER
+			assert_mock CustomerPlan::DB
 			assert_mock @customer
 			assert_mock Registration::Activation::Payment
 			assert_mock Registration::Activation::DB
@@ -565,6 +586,12 @@ class RegistrationTest < Minitest::Test
 					)
 				end]
 			)
+			CustomerPlan::DB.expect(
+				:query_one, {}, [String, "many_subaccounts"]
+			)
+			Registration::Activation::DB.expect(
+				:query_one, {}, [String, "many_subaccounts"], default: {}
+			)
 			Registration::Activation::DB.expect(
 				:query_one, {}, [String, "many_subaccounts"], default: {}
 			)
@@ -576,6 +603,7 @@ class RegistrationTest < Minitest::Test
 				execute_command { @activation.write.catch(&:to_s) }
 			)
 			assert_mock Command::COMMAND_MANAGER
+			assert_mock CustomerPlan::DB
 			assert_mock @customer
 			assert_mock Registration::Activation::Payment
 			assert_mock Registration::Activation::DB
@@ -654,7 +682,7 @@ class RegistrationTest < Minitest::Test
 			Registration::Activation::Allow::DB.expect(
 				:exec,
 				nil,
-				[String, ["refercust", "test"]]
+				[String, ["refercust", "test", true]]
 			)
 			cust.expect(:with_plan, cust, ["test_usd"])
 			cust.expect(:activate_plan_starting_now, nil)
@@ -1039,6 +1067,12 @@ class RegistrationTest < Minitest::Test
 					:new,
 					OpenStruct.new(write: nil)
 				) { |*| true }
+				CustomerPlan::DB.expect(
+					:query_one, {}, [String, "parent_customer"]
+				)
+				Registration::Payment::InviteCode::DB.expect(
+					:query_one, {}, [String, "parent_customer"], default: {}
+				)
 				Registration::Payment::InviteCode::DB.expect(
 					:query_one, {}, [String, "parent_customer"], default: {}
 				)
@@ -1053,9 +1087,14 @@ class RegistrationTest < Minitest::Test
 					)
 					Registration::Payment::InviteCode::REDIS.expect(
 						:get,
-						EMPromise.resolve(nil),
+						EMPromise.resolve("Basement"),
 						["jmp_customer_trust_level-parent_customer"]
 					)
+					Registration::Payment::InviteCode::REDIS.expect(
+						:get,
+						EMPromise.resolve(nil),
+						["jmp_customer_activater-parent_customer"]
+					)
 					CustomerPlan::DB.expect(
 						:query,
 						[{ "plan_name" => "test_usd" }],
@@ -1090,6 +1129,7 @@ class RegistrationTest < Minitest::Test
 					).write
 				end
 				assert_mock Command::COMMAND_MANAGER
+				assert_mock CustomerPlan::DB
 				assert_mock Registration::Payment::InviteCode::DB
 				assert_mock Registration::Payment::InviteCode::REDIS
 				assert_mock Registration::Payment::MaybeBill::BillPlan
@@ -1314,7 +1354,8 @@ class RegistrationTest < Minitest::Test
 			iq.from = "test\\40example.com@cheogram.com"
 			@finish = Registration::Finish.new(
 				customer(sgx: @sgx, plan_name: "test_usd"),
-				TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
+				TelSelections::ChooseTel::Tn.for_pending_value("+15555550000"),
+				trust_level_repo: FakeTrustLevelRepo.new("test" => "Cellar")
 			)
 		end
 
@@ -1468,7 +1509,7 @@ class RegistrationTest < Minitest::Test
 			Registration::Finish::DB.expect(
 				:exec,
 				EMPromise.resolve(nil),
-				[String, ["test-inviter", "test"]]
+				[String, ["test-inviter", "test", false]]
 			)
 			Transaction::DB.expect(:transaction, nil) do |&blk|
 				blk.call

test/test_trust_level_repo.rb 🔗

@@ -3,6 +3,16 @@
 require "trust_level_repo"
 
 class TrustLevelRepoTest < Minitest::Test
+	def setup
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "test"]
+		)
+	end
+
+	def teardown
+		assert_mock CustomerPlan::DB
+	end
+
 	def test_manual_tomb
 		trust_level = TrustLevelRepo.new(
 			db: FakeDB.new,
@@ -14,6 +24,17 @@ class TrustLevelRepoTest < Minitest::Test
 	end
 	em :test_manual_tomb
 
+	def test_manual_cellar
+		trust_level = TrustLevelRepo.new(
+			db: FakeDB.new,
+			redis: FakeRedis.new(
+				"jmp_customer_trust_level-test" => "Cellar"
+			)
+		).find(customer(plan_name: "test_usd")).sync
+		assert_equal "Manual(Cellar)", trust_level.to_s
+	end
+	em :test_manual_cellar
+
 	def test_manual_basement
 		trust_level = TrustLevelRepo.new(
 			db: FakeDB.new,
@@ -63,14 +84,51 @@ class TrustLevelRepoTest < Minitest::Test
 			db: FakeDB.new,
 			redis: FakeRedis.new
 		).find(customer(plan_name: "test_usd")).sync
-		assert_kind_of TrustLevel::Basement, trust_level
+		assert_kind_of TrustLevel::Cellar, trust_level
 	end
 	em :test_new_customer
 
+	def test_new_customer_with_activater
+		trust_level = TrustLevelRepo.new(
+			db: FakeDB.new,
+			redis: FakeRedis.new(
+				"jmp_customer_activater-test" => "+15551234567"
+			)
+		).find(customer(plan_name: "test_usd")).sync
+		assert_kind_of TrustLevel::Basement, trust_level
+	end
+	em :test_new_customer_with_activater
+
+	def test_invited_customer
+		trust_level = TrustLevelRepo.new(
+			db: FakeDB.new(
+				["test"] => FakeDB::MultiResult.new([{ used_at: Time.now }], [])
+			),
+			redis: FakeRedis.new
+		).find(customer(plan_name: "test_usd")).sync
+		assert_kind_of TrustLevel::Basement, trust_level
+	end
+	em :test_invited_customer
+
+	def test_old_customer
+		CustomerPlan::DB.query_one("", "test")
+		CustomerPlan::DB.expect(
+			:query_one, { start_date: Time.parse("2026-02-25") }, [String, "test"]
+		)
+		trust_level = TrustLevelRepo.new(
+			db: FakeDB.new,
+			redis: FakeRedis.new
+		).find(customer(plan_name: "test_usd")).sync
+		assert_kind_of TrustLevel::Basement, trust_level
+	end
+	em :test_old_customer
+
 	def test_regular_customer
 		trust_level = TrustLevelRepo.new(
 			db: FakeDB.new(["test"] => [{ "settled_amount" => 15 }]),
-			redis: FakeRedis.new
+			redis: FakeRedis.new(
+				"jmp_customer_activater-test" => "+15551234567"
+			)
 		).find(customer(plan_name: "test_usd")).sync
 		assert_kind_of TrustLevel::Customer, trust_level
 	end
@@ -79,7 +137,9 @@ class TrustLevelRepoTest < Minitest::Test
 	def test_settled_customer
 		trust_level = TrustLevelRepo.new(
 			db: FakeDB.new(["test"] => [{ "settled_amount" => 61 }]),
-			redis: FakeRedis.new
+			redis: FakeRedis.new(
+				"jmp_customer_activater-test" => "+15551234567"
+			)
 		).find(customer(plan_name: "test_usd")).sync
 		assert_kind_of TrustLevel::Paragon, trust_level
 	end

test/test_web.rb 🔗

@@ -124,7 +124,13 @@ class WebTest < Minitest::Test
 			)
 		)
 		Web.opts[:call_attempt_repo] = CallAttemptRepo.new(
-			redis: FakeRedis.new,
+			redis: FakeRedis.new(
+				"jmp_customer_activater-customerid" => "+15551234567",
+				"jmp_customer_activater-customerid2" => "+15551234567",
+				"jmp_customer_activater-customerid_limit" => "+15551234567",
+				"jmp_customer_activater-customerid_low" => "+15551234567",
+				"jmp_customer_activater-customerid_topup" => "+15551234567"
+			),
 			db: FakeDB.new(
 				["test_usd", "+15557654321", :outbound] => [{ "rate" => 0.01 }],
 				["test_usd", "+1911", :outbound] => [{ "rate" => 0.01 }],
@@ -134,14 +140,17 @@ class WebTest < Minitest::Test
 				["test_usd", "+18001234567", :outbound] => [{ "rate" => 0.00 }],
 				["customerid_limit"] => FakeDB::MultiResult.new(
 					[{ "a" => 1000 }],
+					[],
 					[{ "settled_amount" => 15 }]
 				),
 				["customerid_low"] => FakeDB::MultiResult.new(
 					[{ "a" => 1000 }],
+					[],
 					[{ "settled_amount" => 15 }]
 				),
 				["customerid_topup"] => FakeDB::MultiResult.new(
 					[{ "a" => 1000 }],
+					[],
 					[{ "settled_amount" => 15 }]
 				)
 			)
@@ -163,6 +172,10 @@ class WebTest < Minitest::Test
 	end
 
 	def test_outbound_forwards
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
+
 		post(
 			"/outbound/calls",
 			{
@@ -180,10 +193,15 @@ class WebTest < Minitest::Test
 			"<PhoneNumber>+15557654321</PhoneNumber></Transfer></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_forwards
 
 	def test_outbound_low_balance
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_low"]
+		)
+
 		ExpiringLock::REDIS.expect(
 			:set,
 			EMPromise.resolve(nil),
@@ -208,10 +226,15 @@ class WebTest < Minitest::Test
 			last_response.body
 		)
 		assert_mock ExpiringLock::REDIS
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_low_balance
 
 	def test_outbound_low_balance_top_up
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_topup"]
+		)
+
 		LowBalance::AutoTopUp::CreditCardSale.expect(
 			:create,
 			EMPromise.resolve(
@@ -273,10 +296,15 @@ class WebTest < Minitest::Test
 		assert_mock ExpiringLock::REDIS
 		assert_mock Customer::BLATHER
 		assert_mock LowBalance::AutoTopUp::CreditCardSale
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_low_balance_top_up
 
 	def test_outbound_unsupported
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
+
 		post(
 			"/outbound/calls",
 			{
@@ -294,10 +322,15 @@ class WebTest < Minitest::Test
 			"supported on your account.</SpeakSentence></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_unsupported
 
 	def test_outbound_unsupported_short_numbers_911
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
+
 		post(
 			"/outbound/calls",
 			{
@@ -315,10 +348,15 @@ class WebTest < Minitest::Test
 			"supported on your account.</SpeakSentence></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_unsupported_short_numbers_911
 
 	def test_outbound_supported_9116
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
+
 		post(
 			"/outbound/calls",
 			{
@@ -336,10 +374,15 @@ class WebTest < Minitest::Test
 			"<PhoneNumber>+19116</PhoneNumber></Transfer></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_supported_9116
 
 	def test_outbound_atlimit
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_limit"]
+		)
+
 		post(
 			"/outbound/calls",
 			{
@@ -360,6 +403,7 @@ class WebTest < Minitest::Test
 			"charges. You can hang up to cancel.</SpeakSentence></Gather></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_atlimit
 
@@ -385,6 +429,10 @@ class WebTest < Minitest::Test
 	em :test_outbound_no_customer
 
 	def test_outbound_atlimit_digits
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_limit"]
+		)
+
 		post(
 			"/outbound/calls",
 			{
@@ -403,10 +451,15 @@ class WebTest < Minitest::Test
 			"<PhoneNumber>+15557654321</PhoneNumber></Transfer></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_atlimit_digits
 
 	def test_outbound_toll_free
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
+
 		post(
 			"/outbound/calls",
 			{
@@ -424,10 +477,15 @@ class WebTest < Minitest::Test
 			"<PhoneNumber>+18001234567</PhoneNumber></Transfer></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_toll_free
 
 	def test_outbound_disconnect
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
+
 		post(
 			"/outbound/calls/status",
 			{
@@ -444,10 +502,15 @@ class WebTest < Minitest::Test
 
 		assert last_response.ok?
 		assert_equal("OK", last_response.body)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_disconnect
 
 	def test_outbound_disconnect_tombed
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_tombed"]
+		)
+
 		@cdr_repo.stub(:put, ->(*) { raise "put called" }) do
 			post(
 				"/outbound/calls/status",
@@ -466,10 +529,14 @@ class WebTest < Minitest::Test
 
 		assert last_response.ok?
 		assert_equal("OK", last_response.body)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_outbound_disconnect_tombed
 
 	def test_inbound
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
 		CustomerFwd::BANDWIDTH_VOICE.expect(
 			:create_call,
 			OpenStruct.new(data: OpenStruct.new(call_id: "ocall")),
@@ -500,10 +567,14 @@ class WebTest < Minitest::Test
 			last_response.body
 		)
 		assert_mock CustomerFwd::BANDWIDTH_VOICE
+		assert_mock CustomerPlan::DB
 	end
 	em :test_inbound
 
 	def test_inbound_from_reachability
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
 		CustomerFwd::BANDWIDTH_VOICE.expect(
 			:create_call,
 			OpenStruct.new(data: OpenStruct.new(call_id: "ocall")),
@@ -541,10 +612,14 @@ class WebTest < Minitest::Test
 		)
 		assert_mock CustomerFwd::BANDWIDTH_VOICE
 		assert_mock ReachableRedis
+		assert_mock CustomerPlan::DB
 	end
 	em :test_inbound_from_reachability
 
 	def test_inbound_no_bwmsgsv2
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid2"]
+		)
 		CustomerFwd::BANDWIDTH_VOICE.expect(
 			:create_call,
 			OpenStruct.new(data: OpenStruct.new(call_id: "ocall")),
@@ -575,10 +650,15 @@ class WebTest < Minitest::Test
 			last_response.body
 		)
 		assert_mock CustomerFwd::BANDWIDTH_VOICE
+		assert_mock CustomerPlan::DB
 	end
 	em :test_inbound_no_bwmsgsv2
 
 	def test_inbound_low
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_low"]
+		)
+
 		ExpiringLock::REDIS.expect(
 			:set,
 			EMPromise.resolve(nil),
@@ -604,10 +684,15 @@ class WebTest < Minitest::Test
 		)
 		assert_mock CustomerFwd::BANDWIDTH_VOICE
 		assert_mock ExpiringLock::REDIS
+		assert_mock CustomerPlan::DB
 	end
 	em :test_inbound_low
 
 	def test_inbound_leg2
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid"]
+		)
+
 		post(
 			"/inbound/calls/acall?customer_id=customerid",
 			{
@@ -625,10 +710,15 @@ class WebTest < Minitest::Test
 			"</Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_inbound_leg2
 
 	def test_inbound_limit_leg2
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_limit"]
+		)
+
 		path = "/inbound/calls/acall?customer_id=customerid_limit"
 
 		post(
@@ -652,10 +742,15 @@ class WebTest < Minitest::Test
 			"</SpeakSentence></Gather></Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_inbound_limit_leg2
 
 	def test_inbound_limit_digits_leg2
+		CustomerPlan::DB.expect(
+			:query_one, {}, [String, "customerid_limit"]
+		)
+
 		post(
 			"/inbound/calls/acall?customer_id=customerid_limit",
 			{
@@ -674,6 +769,7 @@ class WebTest < Minitest::Test
 			"</Response>",
 			last_response.body
 		)
+		assert_mock CustomerPlan::DB
 	end
 	em :test_inbound_limit_digits_leg2