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