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"
+)
Christopher Vollick created
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(-)
@@ -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"
+)
@@ -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" }
]
)
@@ -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
@@ -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(
@@ -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