~singpolyma/sgx-jmp

9440485fcd93b619a22e729cf591264b6569dcd4 — Stephen Paul Weber 4 months ago c3f3220 + 84c77d8
Merge branch 'more-admin-info'

* more-admin-info:
  Test Admin Info with Numbers
  Refetch Customer on Repeated Customer Info Calls
  Trust Level in Customer Info
  Show Callability State in Customer Info
  PromiseHash
  No Settled Transactions is 0, not Null
M forms/admin_info.rb => forms/admin_info.rb +12 -0
@@ 46,6 46,18 @@ if @admin_info.fwd.uri
end

field(
	var: "call_info",
	label: "Call Status",
	value: @admin_info.call_info
)

field(
	var: "trust_level",
	label: "Trust Level",
	value: @admin_info.trust_level
)

field(
	var: "api",
	label: "API",
	value: @admin_info.api.to_s

M lib/admin_command.rb => lib/admin_command.rb +14 -11
@@ 6,12 6,15 @@ require_relative "financial_info"
require_relative "form_template"

class AdminCommand
	def initialize(target_customer)
	def initialize(target_customer, customer_repo)
		@target_customer = target_customer
		@customer_repo = customer_repo
	end

	def start
		action_info.then { menu_or_done }
		@target_customer.admin_info.then { |info|
			reply(info.form)
		}.then { menu_or_done }
	end

	def reply(form)


@@ 40,19 43,19 @@ class AdminCommand
	end

	def new_context(q)
		CustomerInfoForm.new.parse_something(q).then do |new_customer|
			if new_customer.respond_to?(:customer_id)
				AdminCommand.new(new_customer).start
			else
				reply(new_customer.form)
		CustomerInfoForm.new(@customer_repo)
			.parse_something(q).then do |new_customer|
				if new_customer.respond_to?(:customer_id)
					AdminCommand.new(new_customer, @customer_repo).start
				else
					reply(new_customer.form)
				end
			end
		end
	end

	def action_info
		@target_customer.admin_info.then do |info|
			reply(info.form)
		end
		# Refresh the data
		new_context(@target_customer.customer_id)
	end

	def action_financial

M lib/call_attempt.rb => lib/call_attempt.rb +19 -2
@@ 48,6 48,10 @@ class CallAttempt
		["#{direction}/connect", { locals: to_h }]
	end

	def to_s
		"Allowed(max_minutes: #{max_minutes}, limit_remaining: #{limit_remaining})"
	end

	def create_call(fwd, *args, &block)
		fwd.create_call(*args, &block)
	end


@@ 119,6 123,10 @@ class CallAttempt
			[view]
		end

		def to_s
			"Unsupported"
		end

		def create_call(*); end

		def as_json(*)


@@ 135,8 143,8 @@ class CallAttempt
			self.for(rate: rate, **kwargs) if credit < rate * 10
		end

		def self.for(customer:, direction:, **kwargs)
			LowBalance.for(customer).then(&:notify!).then do |amount|
		def self.for(customer:, direction:, low_balance: LowBalance, **kwargs)
			low_balance.for(customer).then(&:notify!).then do |amount|
				if amount&.positive?
					CallAttempt.for(
						customer: customer.with_balance(customer.balance + amount),


@@ 165,6 173,10 @@ class CallAttempt
			[view, { locals: to_h }]
		end

		def to_s
			"NoBalance"
		end

		def create_call(*); end

		def as_json(*)


@@ 211,6 223,11 @@ class CallAttempt
			[view, { locals: to_h }]
		end

		def to_s
			"AtLimit(max_minutes: #{max_minutes}, "\
			"limit_remaining: #{limit_remaining})"
		end

		def create_call(fwd, *args, &block)
			fwd.create_call(*args, &block)
		end

M lib/customer.rb => lib/customer.rb +3 -2
@@ 111,8 111,9 @@ class Customer
		API.for(self)
	end

	def admin_info
		AdminInfo.for(self, @plan)
	# kwargs are passed through for dependency injection from tests
	def admin_info(**kwargs)
		AdminInfo.for(self, @plan, **kwargs)
	end

	def info

M lib/customer_info.rb => lib/customer_info.rb +42 -18
@@ 7,6 7,7 @@ require "value_semantics/monkey_patched"
require_relative "proxied_jid"
require_relative "customer_plan"
require_relative "form_template"
require_relative "promise_hash"

class PlanInfo
	extend Forwardable


@@ 78,14 79,12 @@ class CustomerInfo
	end

	def self.for(customer, plan)
		PlanInfo.for(plan).then do |plan_info|
			new(
				plan_info: plan_info,
				tel: customer.registered? ? customer.registered?.phone : nil,
				balance: customer.balance,
				cnam: customer.tndetails.dig(:features, :lidb, :subscriber_information)
			)
		end
		PromiseHash.all(
			plan_info: PlanInfo.for(plan),
			tel: customer.registered? ? customer.registered?.phone : nil,
			balance: customer.balance,
			cnam: customer.tndetails.dig(:features, :lidb, :subscriber_information)
		).then(&method(:new))
	end

	def form


@@ 100,18 99,43 @@ class AdminInfo
		fwd Either(CustomerFwd, nil)
		info CustomerInfo
		api API
		call_info String
		trust_level String
	end

	def self.for(
		customer, plan,
		trust_level_repo: TrustLevelRepo.new,
		call_attempt_repo: CallAttemptRepo.new
	)
		PromiseHash.all(
			jid: customer.jid,
			customer_id: customer.customer_id,
			fwd: customer.fwd,
			info: CustomerInfo.for(customer, plan),
			api: customer.api,
			call_info: call_info(customer, call_attempt_repo),
			trust_level: trust_level_repo.find(customer).then(&:to_s)
		).then(&method(:new))
	end

	class FakeLowBalance
		def self.for(_)
			self
		end

		def self.notify!
			EMPromise.resolve(0)
		end
	end

	def self.for(customer, plan)
		EMPromise.all([
			CustomerInfo.for(customer, plan),
			customer.api
		]).then do |info, api_value|
			new(
				jid: customer.jid,
				customer_id: customer.customer_id,
				fwd: customer.fwd, info: info, api: api_value
			)
	def self.call_info(customer, call_attempt_repo)
		if customer.registered?
			call_attempt_repo
				.find_outbound(customer, "+1", call_id: "dry_run")
				.then(&:to_s)
		else
			EMPromise.resolve("No calling")
		end
	end


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

require "em_promise"

module PromiseHash
	def self.all(**kwargs)
		keys = kwargs.keys
		EMPromise.all(kwargs.values).then { |results|
			Hash[keys.zip(results)]
		}
	end
end

M lib/trust_level.rb => lib/trust_level.rb +25 -1
@@ 1,5 1,7 @@
# frozen_string_literal: true

require "delegate"

module TrustLevel
	def self.for(plan_name:, settled_amount: 0, manual: nil)
		@levels.each do |level|


@@ 8,7 10,7 @@ module TrustLevel
				settled_amount: settled_amount,
				manual: manual
			)
			return tl if tl
			return manual ? Manual.new(tl) : tl if tl
		end

		raise "No TrustLevel matched"


@@ 19,6 21,12 @@ module TrustLevel
		@levels << maybe_mk
	end

	class Manual < SimpleDelegator
		def to_s
			"Manual(#{super})"
		end
	end

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


@@ 31,6 39,10 @@ module TrustLevel
		def send_message?(*)
			false
		end

		def to_s
			"Tomb"
		end
	end

	class Basement


@@ 45,6 57,10 @@ module TrustLevel
		def send_message?(messages_today)
			messages_today < 200
		end

		def to_s
			"Basement"
		end
	end

	class Paragon


@@ 59,6 75,10 @@ module TrustLevel
		def send_message?(messages_today)
			messages_today < 700
		end

		def to_s
			"Paragon"
		end
	end

	class Customer


@@ 86,5 106,9 @@ module TrustLevel
		def send_message?(messages_today)
			messages_today < 500
		end

		def to_s
			"Customer"
		end
	end
end

M lib/trust_level_repo.rb => lib/trust_level_repo.rb +1 -1
@@ 27,7 27,7 @@ protected

	def fetch_settled_amount(customer_id)
		db.query_one(<<~SQL, customer_id, default: {})
			SELECT SUM(amount) AS settled_amount FROM transactions
			SELECT COALESCE(SUM(amount), 0) AS settled_amount FROM transactions
			WHERE customer_id=$1 AND settled_after < LOCALTIMESTAMP AND amount > 0
		SQL
	end

M sgx_jmp.rb => sgx_jmp.rb +7 -5
@@ 755,16 755,18 @@ Command.new(
	Command.customer.then do |customer|
		raise AuthError, "You are not an admin" unless customer&.admin?

		customer_repo = CustomerRepo.new(
			sgx_repo: Bwmsgsv2Repo.new,
			bandwidth_tn_repo: EmptyRepo.new # No CNAM in admin
		)

		Command.reply { |reply|
			reply.allowed_actions = [:next]
			reply.command << FormTemplate.render("customer_picker")
		}.then { |response|
			CustomerInfoForm.new(CustomerRepo.new(
				sgx_repo: Bwmsgsv2Repo.new,
				bandwidth_tn_repo: EmptyRepo.new # No CNAM in admin
			)).find_customer(response)
			CustomerInfoForm.new(customer_repo).find_customer(response)
		}.then do |target_customer|
			AdminCommand.new(target_customer).start
			AdminCommand.new(target_customer, customer_repo).start
		end
	end
}.register(self).then(&CommandList.method(:register))

M test/test_customer_info.rb => test/test_customer_info.rb +48 -2
@@ 2,6 2,8 @@

require "test_helper"
require "customer_info"
require "trust_level_repo"
require "trust_level"

API::REDIS = FakeRedis.new
CustomerPlan::REDIS = Minitest::Mock.new


@@ 37,6 39,7 @@ class CustomerInfoTest < Minitest::Test
		sgx.expect(:registered?, false)
		fwd = CustomerFwd.for(uri: "tel:+12223334444", timeout: 15)
		sgx.expect(:fwd, fwd)
		sgx.expect(:registered?, false)

		CustomerPlan::DB.expect(
			:query_one,


@@ 45,11 48,49 @@ class CustomerInfoTest < Minitest::Test
		)

		cust = customer(sgx: sgx, plan_name: "test_usd")
		assert cust.admin_info.sync.form

		trust_repo = Minitest::Mock.new
		trust_repo.expect(:find, TrustLevel::Basement, [cust])

		assert cust.admin_info(trust_level_repo: trust_repo).sync.form
		assert_mock sgx
		assert_mock trust_repo
	end
	em :test_admin_info_does_not_crash

	def test_admin_info_with_tel_does_not_crash
		registered = Struct.new(:phone).new("+12223334444")
		fwd = CustomerFwd.for(uri: "tel:+12223334444", timeout: 15)
		sgx = Struct.new(:registered?, :fwd).new(registered, fwd)

		CustomerPlan::DB.expect(
			:query_one,
			EMPromise.resolve({ start_date: Time.now }),
			[String, "test"]
		)

		cust = customer(sgx: sgx, plan_name: "test_usd")

		call_attempt_repo = Minitest::Mock.new
		call_attempt_repo.expect(
			:find_outbound,
			CallAttempt::Unsupported.new(direction: :outbound),
			[cust, "+1", { call_id: "dry_run" }]
		)

		trust_repo = Minitest::Mock.new
		trust_repo.expect(:find, TrustLevel::Basement, [cust])

		assert cust
			.admin_info(
				trust_level_repo: trust_repo,
				call_attempt_repo: call_attempt_repo
			).sync.form
		assert_mock call_attempt_repo
		assert_mock trust_repo
	end
	em :test_admin_info_with_tel_does_not_crash

	def test_inactive_info_does_not_crash
		sgx = Minitest::Mock.new
		sgx.expect(:registered?, false)


@@ 69,6 110,7 @@ class CustomerInfoTest < Minitest::Test
	def test_inactive_admin_info_does_not_crash
		sgx = Minitest::Mock.new
		sgx.expect(:registered?, false)
		sgx.expect(:registered?, false)
		sgx.expect(:fwd, CustomerFwd::None.new(uri: nil, timeout: nil))

		plan = CustomerPlan.new("test", plan: nil, expires_at: nil)


@@ 79,8 121,12 @@ class CustomerInfoTest < Minitest::Test
			sgx: sgx
		)

		assert cust.admin_info.sync.form
		trust_repo = Minitest::Mock.new
		trust_repo.expect(:find, TrustLevel::Basement, [cust])

		assert cust.admin_info(trust_level_repo: trust_repo).sync.form
		assert_mock sgx
		assert_mock trust_repo
	end
	em :test_inactive_admin_info_does_not_crash


M test/test_trust_level_repo.rb => test/test_trust_level_repo.rb +5 -5
@@ 10,7 10,7 @@ class TrustLevelRepoTest < Minitest::Test
				"jmp_customer_trust_level-test" => "Tomb"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Tomb, trust_level
		assert_equal "Manual(Tomb)", trust_level.to_s
	end
	em :test_manual_tomb



@@ 21,7 21,7 @@ class TrustLevelRepoTest < Minitest::Test
				"jmp_customer_trust_level-test" => "Basement"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Basement, trust_level
		assert_equal "Manual(Basement)", trust_level.to_s
	end
	em :test_manual_basement



@@ 32,7 32,7 @@ class TrustLevelRepoTest < Minitest::Test
				"jmp_customer_trust_level-test" => "Customer"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Customer, trust_level
		assert_equal "Manual(Customer)", trust_level.to_s
	end
	em :test_manual_customer



@@ 43,7 43,7 @@ class TrustLevelRepoTest < Minitest::Test
				"jmp_customer_trust_level-test" => "Paragon"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Paragon, trust_level
		assert_equal "Manual(Paragon)", trust_level.to_s
	end
	em :test_manual_paragon



@@ 54,7 54,7 @@ class TrustLevelRepoTest < Minitest::Test
				"jmp_customer_trust_level-test" => "UNKNOWN"
			)
		).find(OpenStruct.new(customer_id: "test", plan_name: "usd")).sync
		assert_kind_of TrustLevel::Customer, trust_level
		assert_equal "Manual(Customer)", trust_level.to_s
	end
	em :test_manual_unknown