~singpolyma/sgx-jmp

67611fad04149aa539725e1205b4eda22ff42498 — Christopher Vollick 1 year, 2 months ago 4e57aea
Command to Manually Add Money to Account

An admin can now add a transaction to an account without having to log
into the DB.

A few notes:
- The transaction ID allows a "%" in it which gets substituted with a
  unique value. This is so if you've got a transaction value already,
  like an Interac Transfer or something, you can just put it here.
  But if I'm making something up like "cash" I don't have to mash the
  keyboard just to get a good ID. I can just use "cash_%" and be content
  that I'll get a good value
- The notes have a few prefilled values, which is just there for
  convenience and consistency.
  They're an open list, though, for manual things. Except on clients
  that don't support open lists...
- There's an option to notify the user. I haven't built that in this
  commit and will come later. This is so that under normal operation we
  don't have to message from support and tell them "hey, we've got your
  money", and even better we don't have to tell them "hey, we've got
  your money, you may want to go talk to the bot to activate".

  But if support is already talking to them, we can disable it and tell
  them things in a more organic way.

  Like I said, I haven't built that in this commit, though.

So, this is a start, at least.
A forms/admin_add_transaction.rb => forms/admin_add_transaction.rb +37 -0
@@ 0,0 1,37 @@
form!
instructions "Add Transaction"

field(
	var: "transaction_id",
	type: "text-single",
	label: "Transaction ID",
	description: "a % will be replaced with a unique value"
)

field(
	var: "amount",
	type: "text-single",
	datatype: "xs:decimal",
	label: "Amount"
)

field(
	var: "note",
	type: "list-single",
	open: true,
	label: "Note",
	options: [
		{ value: "Bitcoin payment" },
		{ value: "Cash" },
		{ value: "Interac e-Transfer" },
		{ value: "Bitcoin Cash" },
		{ value: "PayPal Migration Bonus" }
	]
)

field(
	var: "bonus_eligible?",
	type: "boolean",
	label: "Compute bonus?",
	value: 1
)

M forms/admin_menu.rb => forms/admin_menu.rb +2 -1
@@ 23,6 23,7 @@ field(
		{ value: "reset_declines", label: "Reset Declines" },
		{ value: "set_trust_level", label: "Set Trust Level" },
		{ value: "add_invites", label: "Add Invites" },
		{ value: "number_change", label: "Number Change" }
		{ value: "number_change", label: "Number Change" },
		{ value: "add_transaction", label: "Add Transaction" }
	]
)

A lib/admin_actions/add_transaction.rb => lib/admin_actions/add_transaction.rb +138 -0
@@ 0,0 1,138 @@
# frozen_string_literal: true

require "bigdecimal/util"
require "securerandom"
require "time"
require "value_semantics/monkey_patched"

require_relative "../admin_action"
require_relative "../form_to_h"

class AdminAction
	class AddTransaction < AdminAction
		class Command
			using FormToH

			def self.for(target_customer, reply:)
				time = DateTime.now.iso8601
				EMPromise.resolve(
					new(
						customer_id: target_customer.customer_id,
						created_at: time, settled_after: time
					)
				).then { |x|
					reply.call(x.form).then(&x.method(:create))
				}
			end

			def initialize(**bag)
				@bag = bag
			end

			def form
				FormTemplate.render("admin_add_transaction")
			end

			def create(result)
				hash = result.form.to_h
					.reject { |_k, v| v == "nil" }.transform_keys(&:to_sym)
				hash[:transaction_id] = hash[:transaction_id]
					.sub("%", SecureRandom.uuid)

				AdminAction::AddTransaction.for(
					**@bag,
					**hash
				)
			end
		end

		TransactionExists = Struct.new(:transaction_id) do
			def to_s
				"The transaction #{transaction_id} already exists"
			end
		end

		TransactionDoesNotExist = Struct.new(:transaction_id) do
			def to_s
				"The transaction #{transaction_id} doesn't exist"
			end
		end

		def customer_id
			@attributes[:customer_id]
		end

		def amount
			@attributes[:amount].to_d
		end

		def transaction_id
			@attributes[:transaction_id]
		end

		def created_at
			@attributes[:created_at]
		end

		def settled_after
			@attributes[:settled_after]
		end

		def note
			@attributes[:note]
		end

		def bonus_eligible?
			["1", "true"].include?(@attributes[:bonus_eligible?])
		end

		def transaction
			@transaction ||= Transaction.new(
				**@attributes.slice(:customer_id, :transaction_id, :amount, :note),
				created_at: created_at, settled_after: settled_after,
				bonus_eligible?: bonus_eligible?
			)
		end

		def check_forward
			EMPromise.resolve(nil)
				.then { check_noop }
				.then { transaction.exists? }
				.then { |e|
					EMPromise.reject(TransactionExists.new(transaction_id)) if e
				}
		end

		def check_reverse
			EMPromise.resolve(nil)
				.then { check_noop }
				.then { transaction.exists? }
				.then { |e|
					EMPromise.reject(TransactionDoesNotExist.new(transaction_id)) unless e
				}
		end

		def to_s
			"add_transaction(#{customer_id}): #{note} (#{transaction_id}) "\
			"#{transaction}"
		end

		def forward
			transaction.insert.then {
				self
			}
		end

		def reverse
			transaction.delete.then {
				self
			}
		end

	protected

		def check_noop
			EMPromise.reject(NoOp.new) if amount.zero?
		end
	end
end

M lib/admin_command.rb => lib/admin_command.rb +3 -1
@@ 2,6 2,7 @@

require_relative "admin_action_repo"
require_relative "admin_actions/add_invites"
require_relative "admin_actions/add_transaction"
require_relative "admin_actions/cancel"
require_relative "admin_actions/financial"
require_relative "admin_actions/reset_declines"


@@ 178,7 179,8 @@ class AdminCommand
		[:reset_declines, Undoable.new(AdminAction::ResetDeclines::Command)],
		[:set_trust_level, Undoable.new(AdminAction::SetTrustLevel::Command)],
		[:add_invites, Undoable.new(AdminAction::AddInvites::Command)],
		[:number_change, Undoable.new(AdminAction::NumberChange::Command)]
		[:number_change, Undoable.new(AdminAction::NumberChange::Command)],
		[:add_transaction, Undoable.new(AdminAction::AddTransaction::Command)]
	].each do |action, handler|
		define_method("action_#{action}") do
			handler.call(

M lib/transaction.rb => lib/transaction.rb +2 -1
@@ 12,6 12,7 @@ class Transaction
		settled_after Time, coerce: ->(x) { Time.parse(x.to_s) }
		amount BigDecimal, coerce: ->(x) { BigDecimal(x, 4) }
		note String
		bonus_eligible? Bool(), default: true
	end

	def insert


@@ 42,7 43,7 @@ class Transaction
	end

	def bonus
		return BigDecimal(0) if amount <= 15
		return BigDecimal(0) unless bonus_eligible? && amount > 15

		amount *
			case amount