diff --git a/lib/call_attempt.rb b/lib/call_attempt.rb index 34b12805fe32eab0132292197e4c0fbc0abb6040..d63738b37a2a8e34f0219c32c29fb124b3d3ab3c 100644 --- a/lib/call_attempt.rb +++ b/lib/call_attempt.rb @@ -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) diff --git a/lib/call_attempt_repo.rb b/lib/call_attempt_repo.rb index fd7e8d7801d065f9a263ae35e0cf35be9015c59a..c0a28aaa7ccf16fe7d51106960403c2650cfe071 100644 --- a/lib/call_attempt_repo.rb +++ b/lib/call_attempt_repo.rb @@ -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 diff --git a/lib/trust_level.rb b/lib/trust_level.rb new file mode 100644 index 0000000000000000000000000000000000000000..e08036fb9c16cacdb3b7017f0961cd2e1843f669 --- /dev/null +++ b/lib/trust_level.rb @@ -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 diff --git a/lib/trust_level_repo.rb b/lib/trust_level_repo.rb new file mode 100644 index 0000000000000000000000000000000000000000..3e064d85b5b5b44ff39fc3bde5c7e264fdb9eb47 --- /dev/null +++ b/lib/trust_level_repo.rb @@ -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 diff --git a/test/test_helper.rb b/test/test_helper.rb index 981decc44f4161de852b4cf79386c510f1100a92..ce7746d9c67b5c6ca9827c1269c66741bf8a06b4 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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 diff --git a/test/test_trust_level_repo.rb b/test/test_trust_level_repo.rb new file mode 100644 index 0000000000000000000000000000000000000000..a6b8add0ef06db2cb9dad503746e06afe925d4a9 --- /dev/null +++ b/test/test_trust_level_repo.rb @@ -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 diff --git a/test/test_web.rb b/test/test_web.rb index e83ba2aad9f423f4d89db24fe0e03a0a65e518ea..aacabc5ea1dadecdc642e1102ac86548de3efcd3 100644 --- a/test/test_web.rb +++ b/test/test_web.rb @@ -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