Undo and Undoable Command Harness

Christopher Vollick created

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.

Change summary

forms/admin_menu.rb        |  3 +
lib/admin_command.rb       | 76 +++++++++++++++++++++++++++++++++++----
test/test_admin_command.rb |  1 
3 files changed, 71 insertions(+), 9 deletions(-)

Detailed changes

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" }
 	]
 )

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(

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