SetTrustLevel Command

Christopher Vollick created

Here we have a form for the extra information we need. They say they
want to set the trust level, we ask which one they'd like, and then we
make the command that does that.

This involves adding a new method to the TrustLevel to get just the
manual level, so I can tell the difference between being set to Customer
manually (in the form) or being automatically determined to be Customer
(which means the form should be set to automatic).

I also obviously need the method to set a new trust level too.

Change summary

forms/admin_menu.rb                  |   3 
forms/admin_set_trust_level.rb       |  11 ++
lib/admin_actions/set_trust_level.rb | 143 ++++++++++++++++++++++++++++++
lib/admin_command.rb                 |   4 
lib/trust_level_repo.rb              |  15 ++
5 files changed, 173 insertions(+), 3 deletions(-)

Detailed changes

forms/admin_menu.rb 🔗

@@ -13,6 +13,7 @@ field(
 		{ value: "bill_plan", label: "Bill Customer" },
 		{ value: "cancel_account", label: "Cancel Customer" },
 		{ value: "undo", label: "Undo" },
-		{ value: "reset_declines", label: "Reset Declines" }
+		{ value: "reset_declines", label: "Reset Declines" },
+		{ value: "set_trust_level", label: "Set Trust Level" }
 	]
 )

forms/admin_set_trust_level.rb 🔗

@@ -0,0 +1,11 @@
+form!
+instructions "Set Trust Level"
+
+field(
+	var: "new_trust_level",
+	type: "list-single",
+	label: "Trust Level",
+	value: @manual || "automatic",
+	options: @levels.map { |lvl| { label: lvl, value: lvl } } +
+		[{ label: "Automatic", value: "automatic" }]
+)

lib/admin_actions/set_trust_level.rb 🔗

@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require "value_semantics/monkey_patched"
+require_relative "../admin_action"
+require_relative "../form_to_h"
+require_relative "../trust_level_repo"
+
+class AdminAction
+	class SetTrustLevel < AdminAction
+		include Isomorphic
+
+		class Command
+			using FormToH
+
+			def self.for(target_customer, reply:)
+				TrustLevelRepo.new.find_manual(target_customer.customer_id).then { |man|
+					new(
+						man,
+						customer_id: target_customer.customer_id
+					)
+				}.then { |x|
+					reply.call(x.form).then(&x.method(:create))
+				}
+			end
+
+			def initialize(manual, **bag)
+				@manual = manual
+				@bag = bag.compact
+			end
+
+			def form
+				FormTemplate.render(
+					"admin_set_trust_level",
+					manual: @manual,
+					levels: TrustLevel.constants.map(&:to_s).reject { |x| x == "Manual" }
+				)
+			end
+
+			def create(result)
+				AdminAction::SetTrustLevel.for(
+					previous_trust_level: @manual,
+					**@bag,
+					**result.form.to_h
+						.reject { |_k, v| v == "automatic" }.transform_keys(&:to_sym)
+				)
+			end
+		end
+
+		InvalidLevel = Struct.new(:level, :levels) {
+			def to_s
+				"Trust level invalid: expected #{levels.join(', ')}, got #{level}"
+			end
+		}
+
+		NoMatch = Struct.new(:expected, :actual) {
+			def to_s
+				"Trust level doesn't match: expected #{expected}, got #{actual}"
+			end
+		}
+
+		def initialize(previous_trust_level: nil, new_trust_level: nil, **kwargs)
+			super(
+				previous_trust_level: previous_trust_level.presence,
+				new_trust_level: new_trust_level.presence,
+				**kwargs
+			)
+		end
+
+		def customer_id
+			@attributes[:customer_id]
+		end
+
+		def previous_trust_level
+			@attributes[:previous_trust_level]
+		end
+
+		def new_trust_level
+			@attributes[:new_trust_level]
+		end
+
+		# If I don't check previous_trust_level here I could get into this
+		# situation:
+		# 1. Set from automatic to Customer
+		# 2. Undo
+		# 3. Set from automatic to Paragon
+		# 4. Undo the undo (redo set from automatic to customer)
+		# Now if I don't check previous_trust_level we'll enqueue a thing that says
+		# we've set from manual to customer, but that's not actually what we did! We
+		# set from Paragon to customer. If I undo that now I won't end up back a
+		# paragon, I'll end up at automatic again, which isn't the state I was in a
+		# second ago
+		def check_forward
+			EMPromise.all([
+				check_noop,
+				check_valid,
+				check_consistent
+			])
+		end
+
+		def forward
+			TrustLevelRepo.new.put(customer_id, new_trust_level).then { self }
+		end
+
+		def to_reverse
+			with(
+				previous_trust_level: new_trust_level,
+				new_trust_level: previous_trust_level
+			)
+		end
+
+		def to_s
+			"set_trust_level(#{customer_id}): "\
+			"#{pretty(previous_trust_level)} -> #{pretty(new_trust_level)}"
+		end
+
+	protected
+
+		def check_noop
+			EMPromise.reject(NoOp.new) if new_trust_level == previous_trust_level
+		end
+
+		def check_valid
+			options = TrustLevel.constants.map(&:to_s)
+			return unless new_trust_level && !options.include?(new_trust_level)
+
+			EMPromise.reject(InvalidLevel.new(new_trust_level, options))
+		end
+
+		def check_consistent
+			TrustLevelRepo.new.find_manual(customer_id).then { |trust|
+				unless previous_trust_level == trust
+					EMPromise.reject(
+						NoMatch.new(pretty(previous_trust_level), pretty(trust))
+					)
+				end
+			}
+		end
+
+		def pretty(level)
+			level || "automatic"
+		end
+	end
+end

lib/admin_command.rb 🔗

@@ -4,6 +4,7 @@ require_relative "admin_action_repo"
 require_relative "admin_actions/cancel"
 require_relative "admin_actions/financial"
 require_relative "admin_actions/reset_declines"
+require_relative "admin_actions/set_trust_level"
 require_relative "bill_plan_command"
 require_relative "customer_info_form"
 require_relative "financial_info"
@@ -176,7 +177,8 @@ class AdminCommand
 		[:cancel_account, Simple.new(AdminAction::CancelCustomer)],
 		[:financial, Simple.new(AdminAction::Financial)],
 		[:undo, Undoable.new(Undo)],
-		[:reset_declines, Undoable.new(AdminAction::ResetDeclines::Command)]
+		[:reset_declines, Undoable.new(AdminAction::ResetDeclines::Command)],
+		[:set_trust_level, Undoable.new(AdminAction::SetTrustLevel::Command)]
 	].each do |action, handler|
 		define_method("action_#{action}") do
 			handler.call(

lib/trust_level_repo.rb 🔗

@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+require "lazy_object"
 require "value_semantics/monkey_patched"
 
 require_relative "trust_level"
@@ -12,7 +13,7 @@ class TrustLevelRepo
 
 	def find(customer)
 		EMPromise.all([
-			redis.get("jmp_customer_trust_level-#{customer.customer_id}"),
+			find_manual(customer.customer_id),
 			fetch_settled_amount(customer.customer_id)
 		]).then do |(manual, row)|
 			TrustLevel.for(
@@ -23,6 +24,18 @@ class TrustLevelRepo
 		end
 	end
 
+	def find_manual(customer_id)
+		redis.get("jmp_customer_trust_level-#{customer_id}")
+	end
+
+	def put(customer_id, trust_level)
+		if trust_level
+			redis.set("jmp_customer_trust_level-#{customer_id}", trust_level)
+		else
+			redis.del("jmp_customer_trust_level-#{customer_id}")
+		end
+	end
+
 protected
 
 	def fetch_settled_amount(customer_id)