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