From 54189f8b387fe0e4a44a763c14300ac174698bdc Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Tue, 10 May 2022 13:45:23 -0400 Subject: [PATCH 1/9] Cleanup Admin Form It used to handle the initial failure differently than internal failure. Now I've moved the outside bits inside, so it can run again when it encounters an unknown character. --- lib/admin_command.rb | 35 ++++++++++++++++++++++++++++----- lib/customer_info_form.rb | 19 ++---------------- sgx_jmp.rb | 9 +-------- test/test_customer_info.rb | 6 ------ test/test_customer_info_form.rb | 15 +++----------- 5 files changed, 36 insertions(+), 48 deletions(-) diff --git a/lib/admin_command.rb b/lib/admin_command.rb index ac7659b1a863583a52d100ecae49f2f5937e5939..1ce24ac7ae54ca11fca5bf51e1726113bedca107 100644 --- a/lib/admin_command.rb +++ b/lib/admin_command.rb @@ -6,6 +6,35 @@ require_relative "financial_info" require_relative "form_template" class AdminCommand + def self.for(target_customer, customer_repo) + if target_customer + new(target_customer, customer_repo) + else + Command.reply { |reply| + reply.allowed_actions = [:next, :complete] + reply.note_type = :error + reply.note_text = "Customer Not Found" + }.then { NoUser.new(customer_repo) } + end + end + + class NoUser + def initialize(customer_repo) + @customer_repo = customer_repo + end + + def start + Command.reply { |reply| + reply.allowed_actions = [:next] + reply.command << FormTemplate.render("customer_picker") + }.then { |response| + CustomerInfoForm.new(@customer_repo).find_customer(response) + }.then { |customer| + AdminCommand.for(customer, @customer_repo).then(&:start) + } + end + end + def initialize(target_customer, customer_repo) @target_customer = target_customer @customer_repo = customer_repo @@ -45,11 +74,7 @@ class AdminCommand def new_context(q) 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 + AdminCommand.for(new_customer, @customer_repo).then(&:start) end end diff --git a/lib/customer_info_form.rb b/lib/customer_info_form.rb index b4db45e16d3fb658694e60e1d7f526316b2091d3..0b3ca108caa5c15d29eca318ebc038b26ec9e9a4 100644 --- a/lib/customer_info_form.rb +++ b/lib/customer_info_form.rb @@ -13,29 +13,14 @@ class CustomerInfoForm parse_something(response.form.field("q").value) end - class NoCustomer - def form - FormTemplate.render("no_customer_info") - end - - def admin_info - self - end - - def registered? - false - end - end - def parse_something(value) - return EMPromise.resolve(NoCustomer.new) if value.to_s.empty? + return EMPromise.resolve(nil) if value.to_s.empty? EMPromise.all([ find_customer_one(value), find_customer_one(Blather::JID.new(value)), find_customer_one(ProxiedJID.proxy(value)), - find_customer_by_phone(value), - EMPromise.resolve(NoCustomer.new) + find_customer_by_phone(value) ]).then { |approaches| approaches.compact.first } end diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 2c6dcd9db52dc3c0e91f9212a111aac51553c508..4233fc0df807c13543fcebf7e3b5c92fb9ea13df 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -746,14 +746,7 @@ Command.new( bandwidth_tn_repo: EmptyRepo.new(BandwidthTnRepo.new) # No CNAM in admin ) - Command.reply { |reply| - reply.allowed_actions = [:next] - reply.command << FormTemplate.render("customer_picker") - }.then { |response| - CustomerInfoForm.new(customer_repo).find_customer(response) - }.then do |target_customer| - AdminCommand.new(target_customer, customer_repo).start - end + AdminCommand::NoUser.new(customer_repo).start end }.register(self).then(&CommandList.method(:register)) diff --git a/test/test_customer_info.rb b/test/test_customer_info.rb index 19e2454043001e958927285981e0b5b1da48d18c..98144d4a9b420f8cb6a5baaf8d6bbcaebeea39f3 100644 --- a/test/test_customer_info.rb +++ b/test/test_customer_info.rb @@ -129,10 +129,4 @@ class CustomerInfoTest < Minitest::Test assert_mock trust_repo end em :test_inactive_admin_info_does_not_crash - - def test_missing_customer_admin_info_does_not_crash - cust = CustomerInfoForm::NoCustomer.new - assert cust.admin_info.form - end - em :test_missing_customer_admin_info_does_not_crash end diff --git a/test/test_customer_info_form.rb b/test/test_customer_info_form.rb index 8d1d0cf2a4de3d2f686044a315ccbbdf2e6e419e..7d19e1ab617a31dc795b09f3401b9e595e372b17 100644 --- a/test/test_customer_info_form.rb +++ b/test/test_customer_info_form.rb @@ -48,10 +48,7 @@ class CustomerInfoFormTest < Minitest::Test end def test_nothing - assert_kind_of( - CustomerInfoForm::NoCustomer, - @info_form.parse_something("").sync - ) + assert_nil(@info_form.parse_something("").sync) end em :test_nothing @@ -101,19 +98,13 @@ class CustomerInfoFormTest < Minitest::Test def test_missing_customer_by_phone result = @info_form.parse_something("+17778889999").sync - assert_kind_of( - CustomerInfoForm::NoCustomer, - result - ) + assert_nil(result) end em :test_missing_customer_by_phone def test_garbage result = @info_form.parse_something("garbage").sync - assert_kind_of( - CustomerInfoForm::NoCustomer, - result - ) + assert_nil(result) end em :test_garbage end From 2fa5a83c0ff1e935d1627c607ae59ab781288273 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Sun, 1 May 2022 09:18:32 -0400 Subject: [PATCH 2/9] Invites Repo In preparation for another command I'd like to make I've first got to make a place where Invites live. There's probably other parts of the code that interact with Invites that I've missed, but this is a good start at least. --- lib/customer.rb | 6 ++---- lib/invites_repo.rb | 30 ++++++++++++++++++++++++++++++ lib/registration.rb | 19 +++++-------------- test/test_registration.rb | 2 +- 4 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 lib/invites_repo.rb diff --git a/lib/customer.rb b/lib/customer.rb index 0a378805a670438bb16b534f26139eb77c74e421..8b7361c648af5ddc7f52dbbea62ea8353e26392a 100644 --- a/lib/customer.rb +++ b/lib/customer.rb @@ -10,6 +10,7 @@ require_relative "./customer_ogm" require_relative "./customer_info" require_relative "./customer_finacials" require_relative "./backend_sgx" +require_relative "./invites_repo" require_relative "./payment_methods" require_relative "./plan" require_relative "./proxied_jid" @@ -75,10 +76,7 @@ class Customer end def unused_invites - promise = DB.query_defer(<<~SQL, [customer_id]) - SELECT code FROM unused_invites WHERE creator_id=$1 - SQL - promise.then { |result| result.map { |row| row["code"] } } + InvitesRepo.new(DB).unused_invites(customer_id) end def stanza_to(stanza) diff --git a/lib/invites_repo.rb b/lib/invites_repo.rb new file mode 100644 index 0000000000000000000000000000000000000000..c7249243071d5e2771270055f48ab0f91248b6b6 --- /dev/null +++ b/lib/invites_repo.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class InvitesRepo + class Invalid < StandardError; end + + def initialize(db=DB) + @db = db + end + + def unused_invites(customer_id) + promise = @db.query_defer(<<~SQL, [customer_id]) + SELECT code FROM unused_invites WHERE creator_id=$1 + SQL + promise.then { |result| result.map { |row| row["code"] } } + end + + def claim_code(customer_id, code, &blk) + EMPromise.resolve(nil).then do + @db.transaction do + valid = @db.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive? + UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP + WHERE code=$2 AND used_by_id IS NULL + SQL + raise Invalid, "Not a valid invite code: #{code}" unless valid + + blk.call + end + end + end +end diff --git a/lib/registration.rb b/lib/registration.rb index c03ddea8a617a8d9d2757a963300ece74b532d11..9bb03b7d1e20ee22e02aa7ca502b7f1dc346c2c0 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -8,6 +8,7 @@ require_relative "./alt_top_up_form" require_relative "./bandwidth_tn_order" require_relative "./command" require_relative "./em" +require_relative "./invites_repo" require_relative "./oob" require_relative "./proxied_jid" require_relative "./tel_selections" @@ -318,8 +319,6 @@ class Registration class InviteCode Payment.kinds[:code] = method(:new) - class Invalid < StandardError; end - FIELDS = [{ var: "code", type: "text-single", @@ -353,14 +352,14 @@ class Registration verify(iq.form.field("code")&.value&.to_s) }.then { Finish.new(@customer, @tel) - }.catch_only(Invalid, &method(:invalid_code)).then(&:write) + }.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write) end protected def guard_too_many_tries REDIS.get("jmp_invite_tries-#{customer_id}").then do |t| - raise Invalid, "Too many wrong attempts" if t.to_i > 10 + raise InvitesRepo::Invalid, "Too many wrong attempts" if t.to_i > 10 end end @@ -378,16 +377,8 @@ class Registration end def verify(code) - EMPromise.resolve(nil).then do - DB.transaction do - valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive? - UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP - WHERE code=$2 AND used_by_id IS NULL - SQL - raise Invalid, "Not a valid invite code: #{code}" unless valid - - @customer.activate_plan_starting_now - end + InvitesRepo.new(DB).claim_code(customer_id, code) do + @customer.activate_plan_starting_now end end end diff --git a/test/test_registration.rb b/test/test_registration.rb index dbd26191538c387fcc8f0e2e000ca3b642097e28..c2561d3af146680540921bd6288a86f88f1816f6 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -551,7 +551,7 @@ class RegistrationTest < Minitest::Test ["jmp_invite_tries-test"] ) Registration::Payment::InviteCode::DB.expect(:transaction, []) do - raise Registration::Payment::InviteCode::Invalid, "wut" + raise InvitesRepo::Invalid, "wut" end Registration::Payment::InviteCode::REDIS.expect( :incr, From 4e2b2873868221fc36c0dc4a1c339e695b349796 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Mon, 6 Jun 2022 16:31:38 -0400 Subject: [PATCH 3/9] AdminAction and AdminActionRepo This allows me to enqueue a description of a change to a stream and then run it. And then later find / list them and undo any of them. The goal here is to make these safe to run and safe to reverse so the user can just run things with confidence knowing that undo's always got their back. This allows me to avoid confirmation boxes on everything and careful scrutinizing of each command before it's run just in case... --- lib/admin_action.rb | 139 +++++++++++++++++++++++++++++++++++++++ lib/admin_action_repo.rb | 86 ++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 lib/admin_action.rb create mode 100644 lib/admin_action_repo.rb diff --git a/lib/admin_action.rb b/lib/admin_action.rb new file mode 100644 index 0000000000000000000000000000000000000000..576140d8cbe8b8c5bc400631e53b3b12c388db5f --- /dev/null +++ b/lib/admin_action.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require "delegate" + +class AdminAction + class NoOp + def to_s + "NoOp" + end + end + + module Direction + class InvalidDirection < StandardError; end + + def self.for(direction) + { + forward: Forward, + reverse: Reverse, + reforward: Reforward + }.fetch(direction.to_sym) { raise InvalidDirection } + end + + class Forward < SimpleDelegator + def with(**kwargs) + self.class.new(__getobj__.with(**kwargs)) + end + + def perform + check_forward.then { forward }.then { |x| self.class.new(x) } + end + + def to_h + super.merge(direction: :forward) + end + + def undo + Reverse.new(__getobj__.with(parent_id: id)) + end + end + + class Reverse < SimpleDelegator + def with(**kwargs) + self.class.new(__getobj__.with(**kwargs)) + end + + def perform + check_reverse.then { reverse }.then { |x| self.class.new(x) } + end + + def to_s + "UNDO(#{parent_id}) #{super}" + end + + def to_h + super.merge(direction: :reverse) + end + + def undo + Reforward.new(__getobj__) + end + end + + class Reforward < Forward + def with(**kwargs) + self.class.new(__getobj__.with(**kwargs)) + end + + def to_s + "REDO(#{parent_id}) #{super}" + end + + def to_h + super.merge(direction: :reforward) + end + + def undo + Reverse.new(__getobj__) + end + end + end + + def self.for(**kwargs) + Direction::Forward.new(new(**kwargs)) + end + + def initialize(**kwargs) + @attributes = kwargs + end + + def with(**kwargs) + self.class.new(@attributes.merge(kwargs)) + end + + def id + @attributes[:id] + end + + def parent_id + @attributes[:parent_id] + end + + def actor_id + @attributes[:actor_id] + end + + def check_forward + EMPromise.resolve(nil) + end + + def forward + EMPromise.resolve(self) + end + + def check_reverse + EMPromise.resolve(nil) + end + + def reverse + EMPromise.resolve(self) + end + + def to_h + @attributes.merge({ + class: self.class.to_s.delete_prefix("AdminAction::") + }.compact) + end + + module Isomorphic + def check_reverse + to_reverse.check_forward + end + + def reverse + # We don't want it to return the reversed one + # We want it to return itself but with the reverse state + to_reverse.forward.then { self } + end + end +end diff --git a/lib/admin_action_repo.rb b/lib/admin_action_repo.rb new file mode 100644 index 0000000000000000000000000000000000000000..4dc59a3f466f3d9f0fa771b3c0eb5275a8541d30 --- /dev/null +++ b/lib/admin_action_repo.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +class AdminActionRepo + class NotFound < StandardError; end + + def initialize(redis: REDIS) + @redis = redis + end + + def build(klass:, direction:, **kwargs) + dir = AdminAction::Direction.for(direction) + dir.new(AdminAction.const_get(klass).new(**kwargs)) + end + + # I'm using hash subset test for pred + # So if you give me any keys I'll find only things where those keys are + # present and set to that value + def find(limit, max="+", **pred) + return EMPromise.resolve([]) unless limit.positive? + + xrevrange( + "admin_actions", max: max, min: "-", count: limit + ).then { |new_max, results| + next [] if results.empty? + + selected = results.select { |_id, values| pred < values } + .map { |id, values| build(id: id, **rename_class(values)) } + + find(limit - selected.length, "(#{new_max}", **pred) + .then { |r| selected + r } + } + end + + def create(action) + push_to_redis(**action.to_h).then { |id| + action.with(id: id) + } + end + +protected + + def rename_class(hash) + hash.transform_keys { |k| k == :class ? :klass : k } + end + + # Turn value into a hash, paper over redis version issue, return earliest ID + def xrevrange(stream, min:, max:, count:) + min = next_id(min[1..-1]) if min.start_with?("(") + max = previous_id(max[1..-1]) if max.start_with?("(") + + @redis.xrevrange(stream, max, min, "COUNT", count).then { |result| + next ["+", []] if result.empty? + + [ + result.last.first, # Reverse order, so this is the lowest ID + result.map { |id, values| [id, Hash[*values].transform_keys(&:to_sym)] } + ] + } + end + + # Versions of REDIS after 6.2 can just do "(#{current_id}" to make an + # exclusive version + def previous_id(current_id) + time, seq = current_id.split("-") + if seq == "0" + "#{time.to_i - 1}-18446744073709551615" + else + "#{time}-#{seq.to_i - 1}" + end + end + + # Versions of REDIS after 6.2 can just do "(#{current_id}" to make an + # exclusive version + def next_id(current_id) + time, seq = current_id.split("-") + if seq == "18446744073709551615" + "#{time.to_i + 1}-0" + else + "#{time}-#{seq.to_i + 1}" + end + end + + def push_to_redis(**kwargs) + @redis.xadd("admin_actions", "*", *kwargs.flatten) + end +end From 3162f5f6160477ee6b221b109120bba1ee324d4b Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Mon, 6 Jun 2022 17:17:57 -0400 Subject: [PATCH 4/9] Move Cancel and Financial Admin Commands This class gets pretty tight later, so I have to move these out to make room in the class for the other commands. This Simple class is a bit weird on its own here, but it makes a bit more sense later when compared to another class. --- lib/admin_actions/cancel.rb | 18 +++++++++ lib/admin_actions/financial.rb | 45 +++++++++++++++++++++ lib/admin_command.rb | 72 +++++++++++++++------------------- lib/financial_info.rb | 2 + 4 files changed, 97 insertions(+), 40 deletions(-) create mode 100644 lib/admin_actions/cancel.rb create mode 100644 lib/admin_actions/financial.rb diff --git a/lib/admin_actions/cancel.rb b/lib/admin_actions/cancel.rb new file mode 100644 index 0000000000000000000000000000000000000000..a45d9f32882d52404e7a80d688dcaece3d31eedf --- /dev/null +++ b/lib/admin_actions/cancel.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AdminAction + class CancelCustomer + def self.call(customer, customer_repo:, **) + m = Blather::Stanza::Message.new + m.from = CONFIG[:notify_from] + m.body = "Your JMP account has been cancelled." + customer.stanza_to(m).then { + EMPromise.all([ + customer.stanza_to(Blather::Stanza::Iq::IBR.new(:set).tap(&:remove!)), + customer.deregister!, + customer_repo.disconnect_tel(customer) + ]) + } + end + end +end diff --git a/lib/admin_actions/financial.rb b/lib/admin_actions/financial.rb new file mode 100644 index 0000000000000000000000000000000000000000..732c70d2b383f4dfce6d8d710a376b4495b2a8b8 --- /dev/null +++ b/lib/admin_actions/financial.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative "../admin_action" +require_relative "../financial_info" +require_relative "../form_template" + +class AdminAction + class Financial + def self.call(customer_id, reply:, **) + new(customer_id, reply: reply).call + end + + def initialize(customer_id, reply:) + @customer_id = customer_id + @reply = reply + end + + def call + AdminFinancialInfo.for(@customer_id).then do |financial_info| + @reply.call(FormTemplate.render( + "admin_financial_info", + info: financial_info + )).then { + pay_methods(financial_info) + }.then { + transactions(financial_info) + } + end + end + + def pay_methods(financial_info) + @reply.call(FormTemplate.render( + "admin_payment_methods", + **financial_info.to_h + )) + end + + def transactions(financial_info) + @reply.call(FormTemplate.render( + "admin_transaction_list", + transactions: financial_info.transactions + )) + end + end +end diff --git a/lib/admin_command.rb b/lib/admin_command.rb index 1ce24ac7ae54ca11fca5bf51e1726113bedca107..84378e9d4b16bcff840ec8ca2e086080f12551a6 100644 --- a/lib/admin_command.rb +++ b/lib/admin_command.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "admin_actions/cancel" +require_relative "admin_actions/financial" require_relative "bill_plan_command" require_relative "customer_info_form" require_relative "financial_info" @@ -83,53 +85,43 @@ class AdminCommand new_context(@target_customer.customer_id) end - def action_financial - AdminFinancialInfo.for(@target_customer).then do |financial_info| - reply(FormTemplate.render( - "admin_financial_info", - info: financial_info - )).then { - pay_methods(financial_info) - }.then { - transactions(financial_info) - } - end - end - def action_bill_plan BillPlanCommand.for(@target_customer).call end - def notify_customer(body) - m = Blather::Stanza::Message.new - m.from = CONFIG[:notify_from] - m.body = body - @target_customer.stanza_to(m) - end + class Simple + def initialize(klass) + @klass = klass + end - def action_cancel_account - notify_customer("Your JMP account has been cancelled.").then { - EMPromise.all([ - @target_customer.stanza_to( - Blather::Stanza::Iq::IBR.new(:set).tap(&:remove!) - ), - @target_customer.deregister!, - @customer_repo.disconnect_tel(@target_customer) - ]) - } - end + def call(customer_id, customer_repo:, **) + @klass.call( + customer_id, + reply: method(:reply), + customer_repo: customer_repo + ) + end - def pay_methods(financial_info) - reply(FormTemplate.render( - "admin_payment_methods", - **financial_info.to_h - )) + def reply(form=nil, note_type: nil, note_text: nil) + Command.reply { |reply| + reply.allowed_actions = [:next, :complete] + reply.command << form if form + reply.note_type = note_type if note_type + reply.note_text = note_text if note_text + } + end end - def transactions(financial_info) - reply(FormTemplate.render( - "admin_transaction_list", - transactions: financial_info.transactions - )) + [ + [:cancel_account, Simple.new(AdminAction::CancelCustomer)], + [:financial, Simple.new(AdminAction::Financial)] + ].each do |action, handler| + define_method("action_#{action}") do + handler.call( + @target_customer, + admin_action_repo: @admin_action_repo, + customer_repo: @customer_repo + ) + end end end diff --git a/lib/financial_info.rb b/lib/financial_info.rb index ca394d797ad741a0aecf8af21660843c5a120025..712c261cf5039d26a7c4da969495e8301b7d640f 100644 --- a/lib/financial_info.rb +++ b/lib/financial_info.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require "value_semantics/monkey_patched" +require_relative "customer_finacials" +require_relative "payment_methods" class AdminFinancialInfo value_semantics do From 67609fee446efdbf8875997407d432d304d8e79a Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Tue, 7 Jun 2022 11:19:57 -0400 Subject: [PATCH 5/9] Undo and Undoable Command Harness It's a little weird to start with Undo when there's nothing to Undo yet, but it's laying the groundwork for what's to come. This gives me a harness I can use here that gets the action, performs it, gets the result of that performed action, and then persists that to the log and reports success or failure. And the first such action I have just grabs the most recent item and undoes it. --- forms/admin_menu.rb | 3 +- lib/admin_command.rb | 76 ++++++++++++++++++++++++++++++++++---- test/test_admin_command.rb | 1 + 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/forms/admin_menu.rb b/forms/admin_menu.rb index aeb8b7ddc1b36ef9b7b9fc7dc5fed41e985dcf21..650642e6c101f4c1687ccf2077a06c8c5a777203 100644 --- a/forms/admin_menu.rb +++ b/forms/admin_menu.rb @@ -11,6 +11,7 @@ field( { value: "info", label: "Customer Info" }, { value: "financial", label: "Customer Billing Information" }, { value: "bill_plan", label: "Bill Customer" }, - { value: "cancel_account", label: "Cancel Customer" } + { value: "cancel_account", label: "Cancel Customer" }, + { value: "undo", label: "Undo" } ] ) diff --git a/lib/admin_command.rb b/lib/admin_command.rb index 84378e9d4b16bcff840ec8ca2e086080f12551a6..955db83831aa223822c37e95a5fcb29fe6b6c482 100644 --- a/lib/admin_command.rb +++ b/lib/admin_command.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "admin_action_repo" require_relative "admin_actions/cancel" require_relative "admin_actions/financial" require_relative "bill_plan_command" @@ -8,21 +9,26 @@ require_relative "financial_info" require_relative "form_template" class AdminCommand - def self.for(target_customer, customer_repo) + def self.for( + target_customer, + customer_repo, + admin_action_repo=AdminActionRepo.new + ) if target_customer - new(target_customer, customer_repo) + new(target_customer, customer_repo, admin_action_repo) else Command.reply { |reply| reply.allowed_actions = [:next, :complete] reply.note_type = :error reply.note_text = "Customer Not Found" - }.then { NoUser.new(customer_repo) } + }.then { NoUser.new(customer_repo, admin_action_repo) } end end class NoUser - def initialize(customer_repo) + def initialize(customer_repo, admin_action_repo=AdminActionRepo.new) @customer_repo = customer_repo + @admin_action_repo = admin_action_repo end def start @@ -32,14 +38,20 @@ class AdminCommand }.then { |response| CustomerInfoForm.new(@customer_repo).find_customer(response) }.then { |customer| - AdminCommand.for(customer, @customer_repo).then(&:start) + AdminCommand.for(customer, @customer_repo, @admin_action_repo) + .then(&:start) } end end - def initialize(target_customer, customer_repo) + def initialize( + target_customer, + customer_repo, + admin_action_repo=AdminActionRepo.new + ) @target_customer = target_customer @customer_repo = customer_repo + @admin_action_repo = admin_action_repo end def start @@ -76,7 +88,8 @@ class AdminCommand def new_context(q) CustomerInfoForm.new(@customer_repo) .parse_something(q).then do |new_customer| - AdminCommand.for(new_customer, @customer_repo).then(&:start) + AdminCommand.for(new_customer, @customer_repo, @admin_action_repo) + .then(&:start) end end @@ -89,6 +102,40 @@ class AdminCommand BillPlanCommand.for(@target_customer).call end + class Undoable + def initialize(klass) + @klass = klass + end + + def call(customer, admin_action_repo:, **) + @klass.for(customer, reply: method(:reply)).then { |action| + Command.customer.then { |actor| + action.with(actor_id: actor.customer_id).perform.then do |performed| + admin_action_repo.create(performed) + end + } + }.then(method(:success), method(:failure)) + end + + def reply(form=nil, note_type: nil, note_text: nil) + Command.reply { |reply| + reply.allowed_actions = [:next, :complete] + reply.command << form if form + reply.note_type = note_type if note_type + reply.note_text = note_text if note_text + } + end + + def success(action) + reply(note_type: :info, note_text: "Action #{action.id}: #{action}") + end + + def failure(err) + LOG.error "Action Failure", err + reply(note_type: :error, note_text: "Action Failed: #{err}") + end + end + class Simple def initialize(klass) @klass = klass @@ -112,9 +159,22 @@ class AdminCommand end end + class Undo + def self.for(target_customer, **) + AdminActionRepo.new + .find(1, customer_id: target_customer.customer_id) + .then { |actions| + raise "No actions found" if actions.empty? + + actions.first.undo + } + end + end + [ [:cancel_account, Simple.new(AdminAction::CancelCustomer)], - [:financial, Simple.new(AdminAction::Financial)] + [:financial, Simple.new(AdminAction::Financial)], + [:undo, Undoable.new(Undo)] ].each do |action, handler| define_method("action_#{action}") do handler.call( diff --git a/test/test_admin_command.rb b/test/test_admin_command.rb index 1b31f3dc530cfe91f1079e0335eb683ac16d76ad..506cc3a1b5fe1dc2a1f8ad1f9dc4791de2abf734 100644 --- a/test/test_admin_command.rb +++ b/test/test_admin_command.rb @@ -4,6 +4,7 @@ require "admin_command" BackendSgx::IQ_MANAGER = Minitest::Mock.new Customer::BLATHER = Minitest::Mock.new +AdminActionRepo::REDIS = Minitest::Mock.new class AdminCommandTest < Minitest::Test def admin_command(tel="+15556667777") From 36182e5f1a70061eb52a895076a469cc5e0b22f9 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Tue, 7 Jun 2022 11:24:25 -0400 Subject: [PATCH 6/9] ResetDeclines Command This is a nice simple first one. I set the declines to 0, and on Undo I set it back to what it was before. There's no extra form or information, it just does the thing. --- forms/admin_menu.rb | 3 +- lib/admin_actions/reset_declines.rb | 44 +++++++++++++++++++++++++++++ lib/admin_command.rb | 4 ++- lib/customer_finacials.rb | 8 ++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 lib/admin_actions/reset_declines.rb diff --git a/forms/admin_menu.rb b/forms/admin_menu.rb index 650642e6c101f4c1687ccf2077a06c8c5a777203..dc65dca7201cb80281809cf6ee60ccbcc60bba01 100644 --- a/forms/admin_menu.rb +++ b/forms/admin_menu.rb @@ -12,6 +12,7 @@ field( { value: "financial", label: "Customer Billing Information" }, { value: "bill_plan", label: "Bill Customer" }, { value: "cancel_account", label: "Cancel Customer" }, - { value: "undo", label: "Undo" } + { value: "undo", label: "Undo" }, + { value: "reset_declines", label: "Reset Declines" } ] ) diff --git a/lib/admin_actions/reset_declines.rb b/lib/admin_actions/reset_declines.rb new file mode 100644 index 0000000000000000000000000000000000000000..57bf2ee108f23da610bc11987895d8169f83ff02 --- /dev/null +++ b/lib/admin_actions/reset_declines.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "value_semantics/monkey_patched" +require_relative "../admin_action" + +class AdminAction + class ResetDeclines < AdminAction + class Command + def self.for(target_customer, **) + target_customer.declines.then { |declines| + AdminAction::ResetDeclines.for( + customer_id: target_customer.customer_id, + previous_value: declines + ) + } + end + end + + def customer_id + @attributes[:customer_id] + end + + def previous_value + @attributes[:previous_value].to_i + end + + def forward + CustomerFinancials.new(customer_id).set_declines(0).then { self } + end + + # I could make sure here that they're still set to 0 in the reverse case, so + # I know there haven't been any declines since I ran the command, but I + # think I don't care actually, and I should just set it back to what it was + # and trust the human knows what they're doing + def reverse + CustomerFinancials.new(customer_id).set_declines(previous_value) + .then { self } + end + + def to_s + "reset_declines(#{customer_id}): #{previous_value} -> 0" + end + end +end diff --git a/lib/admin_command.rb b/lib/admin_command.rb index 955db83831aa223822c37e95a5fcb29fe6b6c482..8e721c61787d7a2ed77eb2f324a535c051f5d4a4 100644 --- a/lib/admin_command.rb +++ b/lib/admin_command.rb @@ -3,6 +3,7 @@ require_relative "admin_action_repo" require_relative "admin_actions/cancel" require_relative "admin_actions/financial" +require_relative "admin_actions/reset_declines" require_relative "bill_plan_command" require_relative "customer_info_form" require_relative "financial_info" @@ -174,7 +175,8 @@ class AdminCommand [ [:cancel_account, Simple.new(AdminAction::CancelCustomer)], [:financial, Simple.new(AdminAction::Financial)], - [:undo, Undoable.new(Undo)] + [:undo, Undoable.new(Undo)], + [:reset_declines, Undoable.new(AdminAction::ResetDeclines::Command)] ].each do |action, handler| define_method("action_#{action}") do handler.call( diff --git a/lib/customer_finacials.rb b/lib/customer_finacials.rb index 07c33eacc86bb502a7dda6ceac52c645fc3bb377..8ff43d40fd7f8f477a8295327fe85a0c0c19117f 100644 --- a/lib/customer_finacials.rb +++ b/lib/customer_finacials.rb @@ -42,6 +42,14 @@ class CustomerFinancials end end + def set_declines(num) + if num.positive? + REDIS.set("jmp_pay_decline-#{@customer_id}", num) + else + REDIS.del("jmp_pay_decline-#{@customer_id}") + end + end + class TransactionInfo value_semantics do transaction_id String From c576c9af5f24f40fe0da06dde51a297a3ae3c756 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Tue, 7 Jun 2022 11:29:01 -0400 Subject: [PATCH 7/9] SetTrustLevel Command 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. --- 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(-) create mode 100644 forms/admin_set_trust_level.rb create mode 100644 lib/admin_actions/set_trust_level.rb 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) From a15d64cb041f20dcd1dc40637fe8ffd4337a1bc2 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Tue, 7 Jun 2022 11:32:53 -0400 Subject: [PATCH 8/9] AddInvites Command This one adds a certain number of invites to a user, and then records which ones were added so they can be removed in the undo case. --- forms/admin_add_invites.rb | 10 +++ forms/admin_menu.rb | 3 +- lib/admin_actions/add_invites.rb | 116 +++++++++++++++++++++++++++++++ lib/admin_command.rb | 4 +- lib/invites_repo.rb | 49 +++++++++++++ 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 forms/admin_add_invites.rb create mode 100644 lib/admin_actions/add_invites.rb diff --git a/forms/admin_add_invites.rb b/forms/admin_add_invites.rb new file mode 100644 index 0000000000000000000000000000000000000000..dd28687f0f9a298d9c99eea7240186f501fd655a --- /dev/null +++ b/forms/admin_add_invites.rb @@ -0,0 +1,10 @@ +form! +instructions "Add Invites" + +field( + var: "to_add", + type: "text-single", + datatype: "xs:integer", + label: "How many invites to add", + value: "0" +) diff --git a/forms/admin_menu.rb b/forms/admin_menu.rb index c4f50c16ddc94f2b5e1c994bdd28a309434b9296..8fc6e5288567865ae9c354105702b8f8bca71627 100644 --- a/forms/admin_menu.rb +++ b/forms/admin_menu.rb @@ -14,6 +14,7 @@ field( { value: "cancel_account", label: "Cancel Customer" }, { value: "undo", label: "Undo" }, { value: "reset_declines", label: "Reset Declines" }, - { value: "set_trust_level", label: "Set Trust Level" } + { value: "set_trust_level", label: "Set Trust Level" }, + { value: "add_invites", label: "Add Invites" } ] ) diff --git a/lib/admin_actions/add_invites.rb b/lib/admin_actions/add_invites.rb new file mode 100644 index 0000000000000000000000000000000000000000..48c457f276a5f20f1c2443f6916c37bcb9d981f9 --- /dev/null +++ b/lib/admin_actions/add_invites.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require "value_semantics/monkey_patched" +require_relative "../admin_action" +require_relative "../form_to_h" + +class AdminAction + class AddInvites < AdminAction + class Command + using FormToH + + def self.for(target_customer, reply:) + EMPromise.resolve( + new(customer_id: target_customer.customer_id) + ).then { |x| + reply.call(x.form).then(&x.method(:create)) + } + end + + def initialize(**bag) + @bag = bag + end + + def form + FormTemplate.render("admin_add_invites") + end + + def create(result) + AdminAction::AddInvites.for( + **@bag, + **result.form.to_h + .reject { |_k, v| v == "nil" }.transform_keys(&:to_sym) + ) + end + end + + CodesTaken = Struct.new(:codes) do + def to_s + "One of these tokens already exists: #{codes.join(', ')}" + end + end + + CodesClaimed = Struct.new(:codes) do + def to_s + "One of these tokens is already claimed: #{codes.join(', ')}" + end + end + + class MustHaveCodes + def to_s + "Action must have list of codes to reverse" + end + end + + def customer_id + @attributes[:customer_id] + end + + def to_add + @attributes[:to_add].to_i + end + + def check_forward + EMPromise.resolve(nil) + .then { check_noop } + .then { + next nil if chosen_invites.empty? + + InvitesRepo.new.any_existing?(chosen_invites).then { |taken| + EMPromise.reject(CodesTaken.new(chosen_invites)) if taken + } + } + end + + def forward + if chosen_invites.empty? + InvitesRepo.new.create_n_codes(customer_id, to_add).then { |codes| + with(invites: codes.join("\n")) + } + else + InvitesRepo.new.create_codes(customer_id, chosen_invites).then { self } + end + end + + def check_reverse + return EMPromise.reject(MustHaveCodes.new) if chosen_invites.empty? + + InvitesRepo.new.any_claimed?(chosen_invites).then { |claimed| + EMPromise.reject(CodesClaimed.new(chosen_invites)) if claimed + } + end + + def reverse + InvitesRepo.new.delete_codes(chosen_invites).then { self } + end + + def to_s + "add_invites(#{customer_id}): #{chosen_invites.join(', ')}" + end + + protected + + def check_noop + EMPromise.reject(NoOp.new) if to_add.zero? + end + + def check_too_many + max = split_invites.length + EMPromise.reject(TooMany.new(to_add, max)) if to_add > max + end + + def chosen_invites + @attributes[:invites]&.split("\n") || [] + end + end +end diff --git a/lib/admin_command.rb b/lib/admin_command.rb index 9c82e05f30a7c6d8a91a8cc711447917035ce40f..7ccd61aac05dd2ba9e119bd3c3b366bcf3b30a5c 100644 --- a/lib/admin_command.rb +++ b/lib/admin_command.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "admin_action_repo" +require_relative "admin_actions/add_invites" require_relative "admin_actions/cancel" require_relative "admin_actions/financial" require_relative "admin_actions/reset_declines" @@ -178,7 +179,8 @@ class AdminCommand [:financial, Simple.new(AdminAction::Financial)], [:undo, Undoable.new(Undo)], [:reset_declines, Undoable.new(AdminAction::ResetDeclines::Command)], - [:set_trust_level, Undoable.new(AdminAction::SetTrustLevel::Command)] + [:set_trust_level, Undoable.new(AdminAction::SetTrustLevel::Command)], + [:add_invites, Undoable.new(AdminAction::AddInvites::Command)] ].each do |action, handler| define_method("action_#{action}") do handler.call( diff --git a/lib/invites_repo.rb b/lib/invites_repo.rb index c7249243071d5e2771270055f48ab0f91248b6b6..a4dac5dbadf1991665afd62534066eca47e44730 100644 --- a/lib/invites_repo.rb +++ b/lib/invites_repo.rb @@ -27,4 +27,53 @@ class InvitesRepo end end end + + CREATE_N_SQL = <<~SQL + INSERT INTO invites + SELECT unnest(array_fill($1::text, array[$2::int])) + RETURNING code + SQL + + def create_n_codes(customer_id, num) + EMPromise.resolve(nil).then { + codes = @db.exec(CREATE_N_SQL, [customer_id, num]) + raise Invalid, "Failed to fetch codes" unless codes.cmd_tuples.positive? + + codes.map { |row| row["code"] } + } + end + + def any_existing?(codes) + promise = @db.query_one(<<~SQL, [codes]) + SELECT count(1) FROM invites WHERE code = ANY($1) + SQL + promise.then { |result| result[:count].positive? } + end + + def any_claimed?(codes) + promise = @db.query_one(<<~SQL, [codes]) + SELECT count(1) FROM invites WHERE code = ANY($1) AND used_by_id IS NOT NULL + SQL + promise.then { |result| result[:count].positive? } + end + + def create_codes(customer_id, codes) + custs = [customer_id] * codes.length + EMPromise.resolve(nil).then { + @db.transaction do + valid = @db.exec(<<~SQL, [custs, codes]).cmd_tuples.positive? + INSERT INTO invites(creator_id, code) SELECT unnest($1), unnest($2) + SQL + raise Invalid, "Failed to insert one of: #{codes}" unless valid + end + } + end + + def delete_codes(codes) + EMPromise.resolve(nil).then { + @db.exec(<<~SQL, [codes]) + DELETE FROM invites WHERE code = ANY($1) + SQL + } + end end From e2844680cb010bc3a28e3699914046f3853be65f Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Mon, 20 Jun 2022 11:36:28 -0400 Subject: [PATCH 9/9] Admin Command Flash Having a second response here just to show the result, and then have to skip by it to get back to the menu is dumb. So instead we just added a thing here so we can tag some info to show up on the next form, and then go to it. Much smoother, but it does depend on a change in the adhoc bot which previously didn't show notes when there was also a form. --- forms/admin_menu.rb | 7 +++++ forms/customer_picker.rb | 7 +++++ lib/admin_command.rb | 68 +++++++++++++++++++--------------------- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/forms/admin_menu.rb b/forms/admin_menu.rb index 8fc6e5288567865ae9c354105702b8f8bca71627..bb963bcb4b969631f277a98106721cde7b9a29bd 100644 --- a/forms/admin_menu.rb +++ b/forms/admin_menu.rb @@ -1,6 +1,13 @@ form! title "Menu" +if @notice + field( + type: "fixed", + value: @notice + ) +end + field( var: "action", type: "list-single", diff --git a/forms/customer_picker.rb b/forms/customer_picker.rb index 46f3c42d7bba3bce31a1a90f2fbc785f1e6b55d4..7be0a77cd7c45dca961e28cf8868b23afc1fdba5 100644 --- a/forms/customer_picker.rb +++ b/forms/customer_picker.rb @@ -6,6 +6,13 @@ instructions( "information for you" ) +if @notice + field( + type: "fixed", + value: @notice + ) +end + field( var: "q", type: "text-single", diff --git a/lib/admin_command.rb b/lib/admin_command.rb index 7ccd61aac05dd2ba9e119bd3c3b366bcf3b30a5c..5393e0a92141cc78c8aa12974c19c033670e0eb4 100644 --- a/lib/admin_command.rb +++ b/lib/admin_command.rb @@ -20,29 +20,28 @@ class AdminCommand if target_customer new(target_customer, customer_repo, admin_action_repo) else - Command.reply { |reply| - reply.allowed_actions = [:next, :complete] - reply.note_type = :error - reply.note_text = "Customer Not Found" - }.then { NoUser.new(customer_repo, admin_action_repo) } + NoUser.new(customer_repo, admin_action_repo, notice: "Customer Not Found") end end - class NoUser - def initialize(customer_repo, admin_action_repo=AdminActionRepo.new) + class NoUser < AdminCommand + def initialize( + customer_repo, + admin_action_repo=AdminActionRepo.new, + notice: nil + ) @customer_repo = customer_repo @admin_action_repo = admin_action_repo + @notice = notice end - def start - Command.reply { |reply| - reply.allowed_actions = [:next] - reply.command << FormTemplate.render("customer_picker") - }.then { |response| - CustomerInfoForm.new(@customer_repo).find_customer(response) - }.then { |customer| - AdminCommand.for(customer, @customer_repo, @admin_action_repo) - .then(&:start) + def start(command_action=:execute) + return Command.finish(@notice || "Done") if command_action == :complete + + reply( + FormTemplate.render("customer_picker", notice: @notice) + ).then { |response| + new_context(response.form.field("q").value, response.action) } end end @@ -57,10 +56,14 @@ class AdminCommand @admin_action_repo = admin_action_repo end - def start + def start(command_action=:execute) @target_customer.admin_info.then { |info| - reply(info.form) - }.then { menu_or_done } + if command_action == :complete + Command.finish { |iq| iq.command << info.form } + else + reply(info.form) + end + }.then { |response| menu_or_done(response.action) } end def reply(form) @@ -70,10 +73,10 @@ class AdminCommand } end - def menu_or_done(command_action=:execute) + def menu_or_done(command_action=:execute, notice: nil) return Command.finish("Done") if command_action == :complete - reply(FormTemplate.render("admin_menu")).then do |response| + reply(FormTemplate.render("admin_menu", notice: notice)).then do |response| if response.form.field("action") handle(response.form.field("action").value, response.action) end @@ -82,17 +85,19 @@ class AdminCommand def handle(action, command_action) if respond_to?("action_#{action}") - send("action_#{action}") + send("action_#{action}").then do |notice| + menu_or_done(command_action, notice: notice) + end else new_context(action) - end.then { menu_or_done(command_action) } + end end - def new_context(q) + def new_context(q, command_action=:execute) CustomerInfoForm.new(@customer_repo) .parse_something(q).then do |new_customer| AdminCommand.for(new_customer, @customer_repo, @admin_action_repo) - .then(&:start) + .then { |ac| ac.start(command_action) } end end @@ -117,7 +122,7 @@ class AdminCommand admin_action_repo.create(performed) end } - }.then(method(:success), method(:failure)) + }.then { |action| "Action #{action.id}: #{action}" } end def reply(form=nil, note_type: nil, note_text: nil) @@ -128,15 +133,6 @@ class AdminCommand reply.note_text = note_text if note_text } end - - def success(action) - reply(note_type: :info, note_text: "Action #{action.id}: #{action}") - end - - def failure(err) - LOG.error "Action Failure", err - reply(note_type: :error, note_text: "Action Failed: #{err}") - end end class Simple @@ -149,7 +145,7 @@ class AdminCommand customer_id, reply: method(:reply), customer_repo: customer_repo - ) + ).then { nil } end def reply(form=nil, note_type: nil, note_text: nil)