~singpolyma/sgx-jmp

c3bd04f46dcb292a8ab5920a6ef3a9ccc523df92 — Stephen Paul Weber 9 months ago 2b3ebc3 + b16e8df
Merge branch 'trustlevel-repo'

* trustlevel-repo:
  TrustLevel{,Repo}
M lib/call_attempt.rb => lib/call_attempt.rb +5 -10
@@ 6,18 6,13 @@ require_relative "tts_template"
require_relative "low_balance"

class CallAttempt
	EXPENSIVE_ROUTE = {
		"usd_beta_unlimited-v20210223" => 0.9,
		"cad_beta_unlimited-v20210223" => 1.1
	}.freeze

	def self.for(customer, rate, usage, direction:, **kwargs)
	def self.for(customer, rate, usage, trust_level, direction:, **kwargs)
		kwargs.merge!(direction: direction)
		included_credit = [customer.minute_limit.to_d - usage, 0].max
		if !rate || rate >= EXPENSIVE_ROUTE.fetch(customer.plan_name, 0.1)
		if !rate || !trust_level.support_call?(rate)
			Unsupported.new(direction: direction)
		elsif included_credit + customer.balance < rate * 10
			NoBalance.for(customer, rate, usage, **kwargs)
			NoBalance.for(customer, rate, usage, trust_level, **kwargs)
		else
			for_ask_or_go(customer, rate, usage, **kwargs)
		end


@@ 90,12 85,12 @@ class CallAttempt
	end

	class NoBalance
		def self.for(customer, rate, usage, direction:, **kwargs)
		def self.for(customer, rate, usage, trust_level, direction:, **kwargs)
			LowBalance.for(customer).then(&:notify!).then do |amount|
				if amount&.positive?
					CallAttempt.for(
						customer.with_balance(customer.balance + amount),
						rate, usage, direction: direction, **kwargs
						rate, usage, trust_level, direction: direction, **kwargs
					)
				else
					NoBalance.new(balance: customer.balance, direction: direction)

M lib/call_attempt_repo.rb => lib/call_attempt_repo.rb +7 -4
@@ 4,10 4,12 @@ require "value_semantics/monkey_patched"
require "lazy_object"

require_relative "call_attempt"
require_relative "trust_level_repo"

class CallAttemptRepo
	value_semantics do
		db Anything(), default: LazyObject.new { DB }
		db    Anything(), default: LazyObject.new { DB }
		redis Anything(), default: LazyObject.new { REDIS }
	end

	def find_outbound(customer, to, **kwargs)


@@ 37,10 39,11 @@ protected
	def find(customer, other_tel, direction:, **kwargs)
		EMPromise.all([
			find_rate(customer.plan_name, other_tel, direction),
			find_usage(customer.customer_id)
		]).then do |(rate, usage)|
			find_usage(customer.customer_id),
			TrustLevelRepo.new(db: db, redis: redis).find(customer)
		]).then do |(rate, usage, trust_level)|
			CallAttempt.for(
				customer, rate, usage, direction: direction, **kwargs
				customer, rate, usage, trust_level, direction: direction, **kwargs
			)
		end
	end

A lib/trust_level.rb => lib/trust_level.rb +74 -0
@@ 0,0 1,74 @@
# frozen_string_literal: true

module TrustLevel
	def self.for(plan_name:, settled_amount: 0, manual: nil)
		@levels.each do |level|
			tl = level.call(
				plan_name: plan_name,
				settled_amount: settled_amount,
				manual: manual
			)
			return tl if tl
		end

		raise "No TrustLevel matched"
	end

	def self.register(&maybe_mk)
		@levels ||= []
		@levels << maybe_mk
	end

	class Tomb
		TrustLevel.register do |manual:, **|
			new if manual == "Tomb"
		end

		def support_call?(*)
			false
		end
	end

	class Basement
		TrustLevel.register do |manual:, settled_amount:, **|
			new if manual == "Basement" || (!manual && settled_amount < 10)
		end

		def support_call?(rate)
			rate <= 0.02
		end
	end

	class Paragon
		TrustLevel.register do |manual:, settled_amount:, **|
			new if manual == "Paragon" || (!manual && settled_amount > 60)
		end

		def support_call?(*)
			true
		end
	end

	class Customer
		TrustLevel.register do |manual:, plan_name:, **|
			if manual && manual != "Customer"
				Sentry.capture_message("Unknown TrustLevel: #{manual}")
			end

			new(plan_name)
		end

		EXPENSIVE_ROUTE = {
			"usd_beta_unlimited-v20210223" => 0.9,
			"cad_beta_unlimited-v20210223" => 1.1
		}.freeze

		def initialize(plan_name)
			@max_rate = EXPENSIVE_ROUTE.fetch(plan_name, 0.1)
		end

		def support_call?(rate)
			rate <= @max_rate
		end
	end
end

A lib/trust_level_repo.rb => lib/trust_level_repo.rb +34 -0
@@ 0,0 1,34 @@
# frozen_string_literal: true

require "value_semantics/monkey_patched"

require_relative "trust_level"

class TrustLevelRepo
	value_semantics do
		db    Anything(), default: LazyObject.new { DB }
		redis Anything(), default: LazyObject.new { REDIS }
	end

	def find(customer)
		EMPromise.all([
			redis.get("jmp_customer_trust_level-#{customer.customer_id}"),
			fetch_settled_amount(customer.customer_id)
		]).then do |(manual, rows)|
			TrustLevel.for(
				manual: manual,
				plan_name: customer.plan_name,
				**(rows.first&.transform_keys(&:to_sym) || {})
			)
		end
	end

protected

	def fetch_settled_amount(customer_id)
		db.query_defer(<<~SQL, [customer_id])
			SELECT SUM(amount) AS settled_amount FROM transactions
			WHERE customer_id=$1 AND settled_after < LOCALTIMESTAMP AND amount > 0
		SQL
	end
end

M test/test_helper.rb => test/test_helper.rb +11 -1
@@ 211,12 211,22 @@ class FakeRedis
end

class FakeDB
	class MultiResult
		def initialize(*args)
			@results = args
		end

		def to_a
			@results.shift
		end
	end

	def initialize(items={})
		@items = items
	end

	def query_defer(_, args)
		EMPromise.resolve(@items.fetch(args, []))
		EMPromise.resolve(@items.fetch(args, []).to_a)
	end
end


A test/test_trust_level_repo.rb => test/test_trust_level_repo.rb +87 -0
@@ 0,0 1,87 @@
# frozen_string_literal: true

require "trust_level_repo"

class TrustLevelRepoTest < Minitest::Test
	def test_manual_tomb
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new,
			redis: FakeRedis.new(
				"jmp_customer_trust_level-test" => "Tomb"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Tomb, trust_level
	end
	em :test_manual_tomb

	def test_manual_basement
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new,
			redis: FakeRedis.new(
				"jmp_customer_trust_level-test" => "Basement"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Basement, trust_level
	end
	em :test_manual_basement

	def test_manual_customer
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new,
			redis: FakeRedis.new(
				"jmp_customer_trust_level-test" => "Customer"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Customer, trust_level
	end
	em :test_manual_customer

	def test_manual_paragon
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new,
			redis: FakeRedis.new(
				"jmp_customer_trust_level-test" => "Paragon"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Paragon, trust_level
	end
	em :test_manual_paragon

	def test_manual_unknown
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new,
			redis: FakeRedis.new(
				"jmp_customer_trust_level-test" => "UNKNOWN"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Customer, trust_level
	end
	em :test_manual_unknown

	def test_new_customer
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new,
			redis: FakeRedis.new
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Basement, trust_level
	end
	em :test_new_customer

	def test_regular_customer
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new(["test"] => [{ "settled_amount" => 15 }]),
			redis: FakeRedis.new
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Customer, trust_level
	end
	em :test_regular_customer

	def test_settled_customer
		trust_level = TrustLevelRepo.new(
			db: FakeDB.new(["test"] => [{ "settled_amount" => 61 }]),
			redis: FakeRedis.new
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Paragon, trust_level
	end
	em :test_settled_customer
end

M test/test_web.rb => test/test_web.rb +13 -3
@@ 79,12 79,22 @@ class WebTest < Minitest::Test
			)
		)
		Web.opts[:call_attempt_repo] = CallAttemptRepo.new(
			redis: FakeRedis.new,
			db: FakeDB.new(
				["test_usd", "+15557654321", :outbound] => [{ "rate" => 0.01 }],
				["test_usd", "+15557654321", :inbound] => [{ "rate" => 0.01 }],
				["customerid_limit"] => [{ "a" => 1000 }],
				["customerid_low"] => [{ "a" => 1000 }],
				["customerid_topup"] => [{ "a" => 1000 }]
				["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 }]
				)
			)
		)
		Web.opts[:common_logger] = FakeLog.new