diff --git a/forms/admin_menu.rb b/forms/admin_menu.rb index dc65dca7201cb80281809cf6ee60ccbcc60bba01..c4f50c16ddc94f2b5e1c994bdd28a309434b9296 100644 --- a/forms/admin_menu.rb +++ b/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" } ] ) diff --git a/forms/admin_set_trust_level.rb b/forms/admin_set_trust_level.rb new file mode 100644 index 0000000000000000000000000000000000000000..3f8a74a52a2408d795e91020e7afe5a491e00bfc --- /dev/null +++ b/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" }] +) diff --git a/lib/admin_actions/set_trust_level.rb b/lib/admin_actions/set_trust_level.rb new file mode 100644 index 0000000000000000000000000000000000000000..3ffb20f688195e9fbf026ea8b695ed95be4e205a --- /dev/null +++ b/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 diff --git a/lib/admin_command.rb b/lib/admin_command.rb index 8e721c61787d7a2ed77eb2f324a535c051f5d4a4..9c82e05f30a7c6d8a91a8cc711447917035ce40f 100644 --- a/lib/admin_command.rb +++ b/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( diff --git a/lib/trust_level_repo.rb b/lib/trust_level_repo.rb index 74fbecbc8df820f3f34e4e97529ceff49647b264..6c52e27bc6e8c01a8296b2bf5d058b1073046265 100644 --- a/lib/trust_level_repo.rb +++ b/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)