Merge branch 'more-admin-info'

Stephen Paul Weber created

* 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

Change summary

forms/admin_info.rb           | 12 +++++++
lib/admin_command.rb          | 25 ++++++++------
lib/call_attempt.rb           | 21 +++++++++++-
lib/customer.rb               |  5 +-
lib/customer_info.rb          | 60 +++++++++++++++++++++++++-----------
lib/promise_hash.rb           | 12 +++++++
lib/trust_level.rb            | 26 +++++++++++++++
lib/trust_level_repo.rb       |  2 
sgx_jmp.rb                    | 12 ++++---
test/test_customer_info.rb    | 50 +++++++++++++++++++++++++++++-
test/test_trust_level_repo.rb | 10 +++---
11 files changed, 188 insertions(+), 47 deletions(-)

Detailed changes

forms/admin_info.rb 🔗

@@ -45,6 +45,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",

lib/admin_command.rb 🔗

@@ -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

lib/call_attempt.rb 🔗

@@ -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

lib/customer.rb 🔗

@@ -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

lib/customer_info.rb 🔗

@@ -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
 

lib/promise_hash.rb 🔗

@@ -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

lib/trust_level.rb 🔗

@@ -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

lib/trust_level_repo.rb 🔗

@@ -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

sgx_jmp.rb 🔗

@@ -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))

test/test_customer_info.rb 🔗

@@ -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
 

test/test_trust_level_repo.rb 🔗

@@ -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