~singpolyma/sgx-jmp

9a1a09ee69626d82266b85be09cb41f868e02e87 — Stephen Paul Weber 2 years ago d799a92
Refactor commands to have Command and Command::Execution objects

Brings the common elements of all commands together, and threads the most useful
state (such as ability to reply) through automatically using the new EMPromise
fiber trampoline.
M .rubocop.yml => .rubocop.yml +4 -0
@@ 14,6 14,10 @@ Metrics/MethodLength:
  Exclude:
    - test/*

Metrics/BlockLength:
  Exclude:
    - test/*

Metrics/ClassLength:
  Exclude:
    - test/*

M Gemfile => Gemfile +2 -2
@@ 10,12 10,12 @@ gem "em-hiredis"
gem "em-http-request"
gem "em-pg-client", git: "https://github.com/royaltm/ruby-em-pg-client"
gem "em-synchrony"
gem "em_promise.rb", "~> 0.0.2"
gem "em_promise.rb", "~> 0.0.3"
gem "eventmachine"
gem "money-open-exchange-rates"
gem "ougai"
gem "ruby-bandwidth-iris"
gem "sentry-ruby"
gem "sentry-ruby", "<= 4.3.1"
gem "statsd-instrument", git: "https://github.com/singpolyma/statsd-instrument.git", branch: "graphite"
gem "value_semantics", git: "https://github.com/singpolyma/value_semantics"


M lib/bandwidth_tn_order.rb => lib/bandwidth_tn_order.rb +2 -2
@@ 8,7 8,7 @@ require_relative "./catapult"

class BandwidthTNOrder
	def self.get(id)
		EM.promise_fiber do
		EMPromise.resolve(nil).then do
			self.for(BandwidthIris::Order.get_order_response(
				# https://github.com/Bandwidth/ruby-bandwidth-iris/issues/44
				BandwidthIris::Client.new,


@@ 18,7 18,7 @@ class BandwidthTNOrder
	end

	def self.create(tel, name: "sgx-jmp order #{tel}")
		EM.promise_fiber do
		EMPromise.resolve(nil).then do
			Received.new(BandwidthIris::Order.create(
				name: name,
				site_id: CONFIG[:bandwidth_site],

A lib/command.rb => lib/command.rb +146 -0
@@ 0,0 1,146 @@
# frozen_string_literal: true

require "sentry-ruby"
require "statsd-instrument"

require_relative "customer_repo"

class Command
	def self.execution
		Thread.current[:execution]
	end

	def self.reply(stanza=nil, &blk)
		execution.reply(stanza, &blk)
	end

	def self.finish(*args, **kwargs, &blk)
		execution.finish(*args, **kwargs, &blk)
	end

	def self.customer
		execution.customer
	end

	def self.log
		execution.log
	end

	class Execution
		attr_reader :customer_repo, :log, :iq

		def initialize(customer_repo, blather, format_error, iq)
			@customer_repo = customer_repo
			@blather = blather
			@format_error = format_error
			@iq = iq
			@log = LOG.child(node: iq.node)
		end

		def execute
			StatsD.increment("command", tags: ["node:#{iq.node}"])
			EMPromise.resolve(nil).then {
				Thread.current[:execution] = self
				sentry_hub
				catch_after(yield self)
			}.catch(&method(:panic))
		end

		def reply(stanza=nil)
			stanza ||= iq.reply.tap do |reply|
				reply.status = :executing
			end
			yield stanza if block_given?
			COMMAND_MANAGER.write(stanza).then do |new_iq|
				@iq = new_iq
			end
		end

		def finish(text=nil, type: :info, status: :completed)
			reply = @iq.reply
			reply.status = status
			yield reply if block_given?
			if text
				reply.note_type = type
				reply.note_text = text
			end
			raise ErrorToSend, reply
		end

		def sentry_hub
			return @sentry_hub if @sentry_hub

			# Stored on Fiber-local in 4.3.1 and earlier
			# https://github.com/getsentry/sentry-ruby/issues/1495
			@sentry_hub = Sentry.get_current_hub
			raise "Sentry.init has not been called" unless @sentry_hub

			@sentry_hub.push_scope
			@sentry_hub.current_scope.clear_breadcrumbs
			@sentry_hub.current_scope.set_transaction_name(@iq.node)
			@sentry_hub.current_scope.set_user(jid: @iq.from.stripped.to_s)
			@sentry_hub
		end

		def customer
			@customer ||= @customer_repo.find_by_jid(@iq.from.stripped).then do |c|
				sentry_hub.current_scope.set_user(
					id: c.customer_id,
					jid: @iq.from.stripped
				)
				c
			end
		end

	protected

		def catch_after(promise)
			promise.catch_only(ErrorToSend) { |e|
				@blather << e.stanza
			}.catch do |e|
				log_error(e)
				finish(@format_error.call(e), type: :error)
			end
		end

		def log_error(e)
			@log.error(
				"Error raised during #{iq.node}: #{e.class}",
				e
			)
			if e.is_a?(::Exception)
				sentry_hub.capture_exception(e)
			else
				sentry_hub.capture_message(e.to_s)
			end
		end
	end

	attr_reader :node, :name

	def initialize(
		node,
		name,
		list_for: ->(tel:, **) { !!tel },
		format_error: ->(e) { e.respond_to?(:message) ? e.message : e.to_s },
		&blk
	)
		@node = node
		@name = name
		@list_for = list_for
		@format_error = format_error
		@blk = blk
	end

	def register(blather)
		blather.command(:execute?, node: @node, sessionid: nil) do |iq|
			customer_repo = CustomerRepo.new
			Execution.new(customer_repo, blather, @format_error, iq).execute(&@blk)
		end
		self
	end

	def list_for?(**kwargs)
		@list_for.call(**kwargs)
	end
end

M lib/command_list.rb => lib/command_list.rb +22 -51
@@ 3,65 3,36 @@
class CommandList
	include Enumerable

	def self.for(customer)
		EMPromise.resolve(customer&.registered?).catch { nil }.then do |reg|
			next Registered.for(customer, reg.phone) if reg
			CommandList.new
		end
	def self.register(command)
		@commands ||= []
		@commands << command
	end

	def each
		yield node: "jabber:iq:register", name: "Register"
	end

	class Registered < CommandList
		def self.for(customer, tel)
			EMPromise.all([
				REDIS.get("catapult_fwd-#{tel}"),
				customer.plan_name ? customer.payment_methods : []
			]).then do |(fwd, payment_methods)|
				Registered.new(*[
					(HAS_CREDIT_CARD unless payment_methods.empty?),
					(HAS_CURRENCY if customer.currency),
					(HAS_FORWARDING if fwd)
				].compact)
	def self.for(customer)
		EMPromise.resolve(customer&.registered?).catch { nil }.then do |reg|
			args_for(customer, reg).then do |kwargs|
				new(@commands.select { |c| c.list_for?(**kwargs) })
			end
		end
	end

		def initialize(*args)
			@extra = args
		end

		ALWAYS = [
			{ node: "number-display", name: "Display JMP Number" },
			{ node: "configure-calls", name: "Configure Calls" },
			{ node: "usage", name: "Show Monthly Usage" },
			{ node: "reset sip account", name: "Create or Reset SIP Account" },
			{
				node: "credit cards",
				name: "Credit Card Settings and Management"
			}
		].freeze
	def self.args_for(customer, reg)
		args = { customer: customer, tel: reg ? reg.phone : nil }
		return EMPromise.resolve(args) unless args[:tel]

		def each
			super
			([ALWAYS] + @extra).each do |commands|
				commands.each { |x| yield x }
			end
		EMPromise.all([
			REDIS.get("catapult_fwd-#{args[:tel]}"),
			customer.plan_name ? customer.payment_methods : []
		]).then do |(fwd, payment_methods)|
			args.merge(fwd: fwd, payment_methods: payment_methods)
		end
	end

	HAS_CURRENCY = [
		node: "alt top up",
		name: "Buy Account Credit by Bitcoin, Mail, or Interac eTransfer"
	].freeze

	HAS_FORWARDING = [
		node: "record-voicemail-greeting",
		name: "Record Voicemail Greeting"
	].freeze
	def initialize(commands)
		@commands = commands
	end

	HAS_CREDIT_CARD = [
		node: "top up", name: "Buy Account Credit by Credit Card"
	].freeze
	def each(&blk)
		@commands.map { |c| { node: c.node, name: c.name } }.each(&blk)
	end
end

A lib/polyfill.rb => lib/polyfill.rb +5 -0
@@ 0,0 1,5 @@
# frozen_string_literal: true

class Object
	alias then yield_self
end

M lib/registration.rb => lib/registration.rb +97 -120
@@ 5,6 5,7 @@ require "ruby-bandwidth-iris"
require "securerandom"

require_relative "./alt_top_up_form"
require_relative "./command"
require_relative "./bandwidth_tn_order"
require_relative "./em"
require_relative "./error_to_send"


@@ 12,54 13,44 @@ require_relative "./oob"
require_relative "./web_register_manager"

class Registration
	def self.for(iq, customer, web_register_manager)
	def self.for(customer, web_register_manager)
		jid = Command.execution.iq.from.stripped
		customer.registered?.then do |registered|
			if registered
				Registered.new(iq, registered.phone)
				Registered.new(registered.phone)
			else
				web_register_manager.choose_tel(iq).then do |(riq, tel)|
					Activation.for(riq, customer, tel)
				web_register_manager[jid].choose_tel.then do |tel|
					Activation.for(customer, tel)
				end
			end
		end
	end

	class Registered
		def initialize(iq, tel)
			@reply = iq.reply
			@reply.status = :completed
		def initialize(tel)
			@tel = tel
		end

		def write
			@reply.note_type = :info
			@reply.note_text = <<~NOTE
				You are already registered with JMP number #{@tel}
			NOTE
			BLATHER << @reply
			nil
			Command.finish("You are already registered with JMP number #{@tel}")
		end
	end

	class Activation
		def self.for(iq, customer, tel)
		def self.for(customer, tel)
			if customer.active?
				Finish.new(iq, customer, tel)
				Finish.new(customer, tel)
			else
				EMPromise.resolve(new(iq, customer, tel))
				EMPromise.resolve(new(customer, tel))
			end
		end

		def initialize(iq, customer, tel)
			@reply = iq.reply
			@reply.status = :executing
			@reply.allowed_actions = [:next]

		def initialize(customer, tel)
			@customer = customer
			@tel = tel
		end

		attr_reader :reply, :customer, :tel
		attr_reader :customer, :tel

		FORM_FIELDS = [
			{


@@ 130,17 121,16 @@ class Registration
		end

		def write
			rate_center.then do |center|
				form = reply.form
				form.type = :form
				form.title = "Activate JMP"
				add_instructions(form, center)
				form.fields = FORM_FIELDS

				COMMAND_MANAGER.write(reply).then { |iq|
					Payment.for(iq, customer, tel)
				}.then(&:write)
			end
			rate_center.then { |center|
				Command.reply do |reply|
					reply.allowed_actions = [:next]
					form = reply.form
					form.type = :form
					form.title = "Activate JMP"
					add_instructions(form, center)
					form.fields = FORM_FIELDS
				end
			}.then { |iq| Payment.for(iq, customer, tel) }.then(&:write)
		end

	protected


@@ 163,23 153,19 @@ class Registration
			customer = customer.with_plan(plan_name)
			kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
				raise "Invalid activation method"
			}.call(iq, customer, tel)
			}.call(customer, tel)
		end

		class Bitcoin
			Payment.kinds[:bitcoin] = method(:new)

			def initialize(iq, customer, tel)
				@reply = iq.reply
				reply.note_type = :info
				reply.status = :canceled

			def initialize(customer, tel)
				@customer = customer
				@customer_id = customer.customer_id
				@tel = tel
			end

			attr_reader :reply, :customer_id, :tel
			attr_reader :customer_id, :tel

			def legacy_session_save
				sid = SecureRandom.hex


@@ 215,9 201,7 @@ class Registration
					BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
				]).then do |(addr, _, rate)|
					min = CONFIG[:activation_amount] / rate
					reply.note_text = note_text(min, addr)
					BLATHER << reply
					nil
					Command.finish(note_text(min, addr), status: :canceled)
				end
			end



@@ 233,31 217,23 @@ class Registration
		class CreditCard
			Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }

			def self.for(iq, customer, tel)
			def self.for(customer, tel)
				customer.payment_methods.then do |payment_methods|
					if (method = payment_methods.default_payment_method)
						Activate.new(iq, customer, method, tel)
						Activate.new(customer, method, tel)
					else
						new(iq, customer, tel)
						new(customer, tel)
					end
				end
			end

			def initialize(iq, customer, tel)
			def initialize(customer, tel)
				@customer = customer
				@tel = tel

				@reply = iq.reply
				@reply.status = :executing
				@reply.allowed_actions = [:next]
				@reply.note_type = :info
				@reply.note_text = "#{oob.desc}: #{oob.url}"
			end

			attr_reader :reply

			def oob
				oob = OOB.find_or_create(@reply.command)
			def oob(reply)
				oob = OOB.find_or_create(reply.command)
				oob.url = CONFIG[:credit_card_url].call(
					reply.to.stripped.to_s.gsub("\\", "%5C"),
					@customer.customer_id


@@ 267,14 243,17 @@ class Registration
			end

			def write
				COMMAND_MANAGER.write(@reply).then do |riq|
					CreditCard.for(riq, @customer, @tel).write
				Command.reply { |reply|
					reply.allowed_actions = [:next]
					reply.note_type = :info
					reply.note_text = "#{oob(reply).desc}: #{oob(reply).url}"
				}.then do
					CreditCard.for(@customer, @tel).then(&:write)
				end
			end

			class Activate
				def initialize(iq, customer, payment_method, tel)
					@iq = iq
				def initialize(customer, payment_method, tel)
					@customer = customer
					@payment_method = payment_method
					@tel = tel


@@ 297,7 276,7 @@ class Registration
					tx.insert.then {
						@customer.bill_plan
					}.then do
						Finish.new(@iq, @customer, @tel).write
						Finish.new(@customer, @tel).write
					end
				end



@@ 323,14 302,13 @@ class Registration
				end

				def declined
					reply = @iq.reply
					reply_oob = decline_oob(reply)
					reply.status = :executing
					reply.allowed_actions = [:next]
					reply.note_type = :error
					reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
					COMMAND_MANAGER.write(reply).then do |riq|
						CreditCard.for(riq, @customer, @tel).write
					Command.reply { |reply|
						reply_oob = decline_oob(reply)
						reply.allowed_actions = [:next]
						reply.note_type = :error
						reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
					}.then do
						CreditCard.for(@customer, @tel).then(&:write)
					end
				end
			end


@@ 348,45 326,49 @@ class Registration
				required: true
			}].freeze

			def initialize(iq, customer, tel, error: nil)
			def initialize(customer, tel, error: nil)
				@customer = customer
				@tel = tel
				@reply = iq.reply
				@reply.status = :executing
				@reply.allowed_actions = [:next]
				@form = @reply.form
				@form.type = :form
				@form.title = "Enter Invite Code"
				@form.instructions = error
				@form.fields = FIELDS
				@error = error
			end

			def add_form(reply)
				form = reply.form
				form.type = :form
				form.title = "Enter Invite Code"
				form.instructions = @error if @error
				form.fields = FIELDS
			end

			def write
				COMMAND_MANAGER.write(@reply).then do |iq|
					guard_too_many_tries.then {
						verify(iq.form.field("code")&.value&.to_s)
					}.then {
						Finish.new(iq, @customer, @tel)
					}.catch_only(Invalid) { |e|
						invalid_code(iq, e)
					}.then(&:write)
				end
				Command.reply { |reply|
					reply.allowed_actions = [:next]
					add_form(reply)
				}.then(&method(:parse))
			end

			def parse(iq)
				guard_too_many_tries.then {
					verify(iq.form.field("code")&.value&.to_s)
				}.then {
					Finish.new(@customer, @tel)
				}.catch_only(Invalid, &method(:invalid_code)).then(&:write)
			end

		protected

			def guard_too_many_tries
				REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t|
				REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
					raise Invalid, "Too many wrong attempts" if t.to_i > 10
				end
			end

			def invalid_code(iq, e)
			def invalid_code(e)
				EMPromise.all([
					REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do
						REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60)
					REDIS.incr("jmp_invite_tries-#{customer_id}").then do
						REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
					end,
					InviteCode.new(iq, @customer, @tel, error: e.message)
					InviteCode.new(@customer, @tel, error: e.message)
				]).then(&:last)
			end



@@ 395,7 377,7 @@ class Registration
			end

			def verify(code)
				EM.promise_fiber do
				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


@@ 411,10 393,7 @@ class Registration
		class Mail
			Payment.kinds[:mail] = method(:new)

			def initialize(iq, _customer, _tel)
				@reply = iq.reply
				@reply.status = :canceled
			end
			def initialize(_customer, _tel); end

			def form
				form = Blather::Stanza::X.new(:result)


@@ 437,18 416,15 @@ class Registration
			end

			def write
				@reply.command << form
				BLATHER << @reply
				Command.finish(status: :canceled) do |reply|
					reply.command << form
				end
			end
		end
	end

	class Finish
		def initialize(iq, customer, tel)
			@reply = iq.reply
			@reply.status = :completed
			@reply.note_type = :info
			@reply.note_text = "Your JMP account has been activated as #{tel}"
		def initialize(customer, tel)
			@customer = customer
			@tel = tel
		end


@@ 457,11 433,11 @@ class Registration
			BandwidthTNOrder.create(@tel).then(&:poll).then(
				->(_) { customer_active_tel_purchased },
				lambda do |_|
					@reply.note_type = :error
					@reply.note_text =
					Command.finish(
						"The JMP number #{@tel} is no longer available, " \
						"please visit https://jmp.chat and choose another."
					BLATHER << @reply
						"please visit https://jmp.chat and choose another.",
						type: :error
					)
				end
			)
		end


@@ 469,27 445,28 @@ class Registration
	protected

		def cheogram_sip_addr
			"sip:#{ERB::Util.url_encode(@reply.to.stripped.to_s)}@sip.cheogram.com"
			jid = Command.execution.iq.from.stripped
			"sip:#{ERB::Util.url_encode(jid)}@sip.cheogram.com"
		end

		def raise_setup_error
			@reply.note_type = :error
			@reply.note_text =
		def raise_setup_error(e)
			Command.log.error "@customer.register! failed", e
			Command.finish(
				"There was an error setting up your number, " \
				"please contact JMP support."
			raise ErrorToSend, @reply
				"please contact JMP support.",
				type: :error
			)
		end

		def customer_active_tel_purchased
			@customer.register!(@tel).catch { |e|
				LOG.error "@customer.register! failed", e
				raise_setup_error
			}.then {
			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
				EMPromise.all([
					REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
					@customer.fwd_timeout = 25 # ~5 seconds / ring, 5 rings
				])
			}.then { BLATHER << @reply }
			}.then do
				Command.finish("Your JMP account has been activated as #{@tel}")
			end
		end
	end
end

M lib/web_register_manager.rb => lib/web_register_manager.rb +7 -13
@@ 15,29 15,23 @@ class WebRegisterManager
		@tel_map[jid.to_s]
	end

	def choose_tel(iq)
		self[iq&.from&.stripped].choose_tel(iq)
	end

	class HaveTel
		def initialize(tel)
			@tel = tel
		end

		def choose_tel(iq)
			EMPromise.resolve([iq, @tel])
		def choose_tel
			EMPromise.resolve(@tel)
		end
	end

	class ChooseTel
		def choose_tel(iq)
			reply = iq.reply
			reply.status = :completed
			reply.note_type = :error
			reply.note_text =
		def choose_tel
			Command.finish(
				"You have not chosen a phone number yet, please return to " \
				"https://jmp.chat and choose one now."
			raise ErrorToSend, reply
				"https://jmp.chat and choose one now.",
				type: :error
			)
		end
	end
end

M sgx_jmp.rb => sgx_jmp.rb +101 -150
@@ 36,12 36,14 @@ singleton_class.class_eval do
	Blather::DSL.append_features(self)
end

require_relative "lib/polyfill"
require_relative "lib/alt_top_up_form"
require_relative "lib/add_bitcoin_address"
require_relative "lib/backend_sgx"
require_relative "lib/bandwidth_tn_order"
require_relative "lib/btc_sell_prices"
require_relative "lib/buy_account_credit_form"
require_relative "lib/command"
require_relative "lib/command_list"
require_relative "lib/customer"
require_relative "lib/customer_repo"


@@ 113,7 115,10 @@ end
BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])

def panic(e, hub=nil)
	LOG.fatal "Error raised during event loop: #{e.class}", e
	(Thread.current[:log] || LOG).fatal(
		"Error raised during event loop: #{e.class}",
		e
	)
	if e.is_a?(::Exception)
		(hub || Sentry).capture_exception(e, hint: { background: false })
	else


@@ 370,174 375,120 @@ iq "/iq/ns:services", ns: "urn:xmpp:extdisco:2" do |iq|
	self << reply
end

command :execute?, node: "jabber:iq:register", sessionid: nil do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])

	sentry_hub = new_sentry_hub(iq, name: iq.node)
	EMPromise.resolve(nil).then {
		CustomerRepo.new.find_by_jid(iq.from.stripped)
	}.catch {
		sentry_hub.add_breadcrumb(Sentry::Breadcrumb.new(
			message: "Customer.create"
		))
		CustomerRepo.new.create(iq.from.stripped)
Command.new(
	"jabber:iq:register",
	"Register",
	list_for: ->(*) { true }
) {
	Command.customer.catch {
		Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Customer.create"))
		Command.execution.customer_repo.create(Command.execution.iq.from.stripped)
	}.then { |customer|
		sentry_hub.current_scope.set_user(
			id: customer.customer_id,
			jid: iq.from.stripped.to_s
		)
		sentry_hub.add_breadcrumb(Sentry::Breadcrumb.new(
			message: "Registration.for"
		))
		Registration.for(
			iq,
			customer,
			web_register_manager
		).then(&:write).then { StatsD.increment("registration.completed") }
	}.catch_only(ErrorToSend) { |e|
		self << e.stanza
	}.catch { |e| panic(e, sentry_hub) }
end

def reply_with_note(iq, text, type: :info)
	reply = iq.reply
	reply.status = :completed
	reply.note_type = type
	reply.note_text = text

	self << reply
end
		Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Registration.for"))
		Registration.for(customer, web_register_manager).then(&:write)
	}.then { StatsD.increment("registration.completed") }
}.register(self).then(&CommandList.method(:register))

# Commands that just pass through to the SGX
command node: [
	"number-display",
	"configure-calls",
	"record-voicemail-greeting"
] do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])

	sentry_hub = new_sentry_hub(iq, name: iq.node)
	CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
		sentry_hub.current_scope.set_user(
			id: customer.customer_id,
			jid: iq.from.stripped.to_s
		)

		customer.stanza_from(iq)
	}.catch { |e| panic(e, sentry_hub) }
{
	"number-display" => ["Display JMP Number"],
	"configure-calls" => ["Configure Calls"],
	"record-voicemail-greeting" => [
		"Record Voicemail Greeting",
		list_for: ->(fwd: nil, **) { !!fwd }
	]
}.each do |node, args|
	Command.new(node, *args) {
		Command.customer.then do |customer|
			customer.stanza_from(Command.execution.iq)
		end
	}.register(self).then(&CommandList.method(:register))
end

command :execute?, node: "credit cards", sessionid: nil do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])

	sentry_hub = new_sentry_hub(iq, name: iq.node)
	reply = iq.reply
	reply.status = :completed

	CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
		oob = OOB.find_or_create(reply.command)
		oob.url = CONFIG[:credit_card_url].call(
Command.new(
	"credit cards",
	"Credit Card Settings and Management"
) {
	Command.customer.then do |customer|
		url = CONFIG[:credit_card_url].call(
			reply.to.stripped.to_s.gsub("\\", "%5C"),
			customer.customer_id
		)
		oob.desc = "Manage credits cards and settings"

		reply.note_type = :info
		reply.note_text = "#{oob.desc}: #{oob.url}"

		self << reply
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "top up", sessionid: nil do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])

	sentry_hub = new_sentry_hub(iq, name: iq.node)
	reply = iq.reply
	reply.allowed_actions = [:complete]

	CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
		desc = "Manage credits cards and settings"
		Command.finish("#{desc}: #{url}") do |reply|
			oob = OOB.find_or_create(reply.command)
			oob.url = url
			oob.desc = desc
		end
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"top up",
	"Buy Account Credit by Credit Card",
	list_for: ->(payment_methods: [], **) { !payment_methods.empty? },
	format_error: ->(e) { "Failed to buy credit, system said: #{e.message}" }
) {
	Command.customer.then { |customer|
		BuyAccountCreditForm.for(customer).then do |credit_form|
			credit_form.add_to_form(reply.form)
			COMMAND_MANAGER.write(reply).then { |iq2| [customer, credit_form, iq2] }
			Command.reply { |reply|
				reply.allowed_actions = [:complete]
				credit_form.add_to_form(reply.form)
			}.then do |iq|
				Transaction.sale(customer, **credit_form.parse(iq.form))
			end
		end
	}.then { |(customer, credit_form, iq2)|
		iq = iq2 # This allows the catch to use it also
		Transaction.sale(customer, **credit_form.parse(iq2.form))
	}.then { |transaction|
		transaction.insert.then do
			reply_with_note(iq, "#{transaction} added to your account balance.")
			Command.finish("#{transaction} added to your account balance.")
		end
	}.catch_only(BuyAccountCreditForm::AmountValidationError) { |e|
		reply_with_note(iq, e.message, type: :error)
	}.catch { |e|
		sentry_hub.capture_exception(e)
		text = "Failed to buy credit, system said: #{e.message}"
		reply_with_note(iq, text, type: :error)
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "alt top up", sessionid: nil do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])

	sentry_hub = new_sentry_hub(iq, name: iq.node)
	reply = iq.reply
	reply.status = :executing
	reply.allowed_actions = [:complete]

	CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
		sentry_hub.current_scope.set_user(
			id: customer.customer_id,
			jid: iq.from.stripped.to_s
		)

	}.catch_only(BuyAccountCreditForm::AmountValidationError) do |e|
		Command.finish(e.message, type: :error)
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"alt top up",
	"Buy Account Credit by Bitcoin, Mail, or Interac eTransfer",
	list_for: ->(customer:, **) { !!customer.currency }
) {
	Command.customer.then { |customer|
		EMPromise.all([AltTopUpForm.for(customer), customer])
	}.then { |(alt_form, customer)|
		reply.command << alt_form.form

		COMMAND_MANAGER.write(reply).then do |iq2|
			AddBitcoinAddress.for(iq2, alt_form, customer).write
	}.then do |(alt_form, customer)|
		Command.reply { |reply|
			reply.allowed_actions = [:complete]
			reply.command << alt_form.form
		}.then do |iq|
			AddBitcoinAddress.for(iq, alt_form, customer).write
		end
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "reset sip account", sessionid: nil do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])

	sentry_hub = new_sentry_hub(iq, name: iq.node)
	CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
		sentry_hub.current_scope.set_user(
			id: customer.customer_id,
			jid: iq.from.stripped.to_s
		)
		customer.reset_sip_account
	}.then { |sip_account|
		reply = iq.reply
		reply.command << sip_account.form
		BLATHER << reply
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "usage", sessionid: nil do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])
	end
}.register(self).then(&CommandList.method(:register))

Command.new(
	"reset sip account",
	"Create or Reset SIP Account"
) {
	Command.customer.then(&:reset_sip_account).then do |sip_account|
		Command.finish do |reply|
			reply.command << sip_account.form
		end
	end
}.register(self).then(&CommandList.method(:register))

	sentry_hub = new_sentry_hub(iq, name: iq.node)
Command.new(
	"usage",
	"Show Monthly Usage"
) {
	report_for = (Date.today..(Date.today << 1))

	CustomerRepo.new.find_by_jid(iq.from.stripped).then { |customer|
		sentry_hub.current_scope.set_user(
			id: customer.customer_id,
			jid: iq.from.stripped.to_s
		)

	Command.customer.then { |customer|
		customer.usage_report(report_for)
	}.then { |usage_report|
		reply = iq.reply
		reply.status = :completed
		reply.command << usage_report.form
		BLATHER << reply
	}.catch { |e| panic(e, sentry_hub) }
end
	}.then do |usage_report|
		Command.finish do |reply|
			reply.command << usage_report.form
		end
	end
}.register(self).then(&CommandList.method(:register))

command :execute?, node: "web-register", sessionid: nil do |iq|
	StatsD.increment("command", tags: ["node:#{iq.node}"])

M test/test_command_list.rb => test/test_command_list.rb +35 -12
@@ 1,20 1,45 @@
# frozen_string_literal: true

require "test_helper"
require "command"
require "command_list"

CommandList::Customer = Minitest::Mock.new
CommandList::REDIS = Minitest::Mock.new

class CommandListTest < Minitest::Test
	SETUP = begin
		[
			Command.new("no_customer", "", list_for: ->(**) { true }),
			Command.new("registered", "", list_for: ->(tel:, **) { !!tel }),
			Command.new("fwd", "", list_for: ->(fwd: nil, **) { !!fwd }),
			Command.new(
				"currency", "",
				list_for: ->(customer: nil, **) { !!customer&.currency }
			),
			Command.new(
				"cc", "",
				list_for: ->(payment_methods: [], **) { !payment_methods.empty? }
			)
		].each do |c|
			CommandList.register(c)
		end
	end

	def test_for_no_customer
		assert_instance_of CommandList, CommandList.for(nil).sync
		assert_equal(
			["no_customer"],
			CommandList.for(nil).sync.map { |c| c[:node] }
		)
	end
	em :test_for_no_customer

	def test_for_unregistered
		customer = OpenStruct.new(registered?: false)
		assert_instance_of CommandList, CommandList.for(customer).sync
		assert_equal(
			["no_customer"],
			CommandList.for(customer).sync.map { |c| c[:node] }
		)
	end
	em :test_for_unregistered



@@ 29,9 54,8 @@ class CommandListTest < Minitest::Test
			payment_methods: EMPromise.resolve([])
		)
		assert_equal(
			["CommandList::Registered"],
			CommandList.for(customer).sync
			.class.ancestors.map(&:name).grep(/\ACommandList::/)
			["no_customer", "registered"],
			CommandList.for(customer).sync.map { |c| c[:node] }
		)
	end
	em :test_for_registered


@@ 47,8 71,8 @@ class CommandListTest < Minitest::Test
			payment_methods: EMPromise.resolve([])
		)
		assert_equal(
			CommandList::HAS_FORWARDING,
			CommandList::HAS_FORWARDING & CommandList.for(customer).sync.to_a
			["no_customer", "registered", "fwd"],
			CommandList.for(customer).sync.map { |c| c[:node] }
		)
	end
	em :test_for_registered_with_fwd


@@ 65,8 89,8 @@ class CommandListTest < Minitest::Test
			payment_methods: EMPromise.resolve([:boop])
		)
		assert_equal(
			CommandList::HAS_CREDIT_CARD,
			CommandList::HAS_CREDIT_CARD & CommandList.for(customer).sync.to_a
			["no_customer", "registered", "cc"],
			CommandList.for(customer).sync.map { |c| c[:node] }
		)
	end
	em :test_for_registered_with_credit_card


@@ 81,10 105,9 @@ class CommandListTest < Minitest::Test
			registered?: OpenStruct.new(phone: "1"),
			currency: :USD
		)

		assert_equal(
			CommandList::HAS_CURRENCY,
			CommandList::HAS_CURRENCY & CommandList.for(customer).sync.to_a
			["no_customer", "registered", "currency"],
			CommandList.for(customer).sync.map { |c| c[:node] }
		)
	end
	em :test_for_registered_with_currency

M test/test_helper.rb => test/test_helper.rb +13 -0
@@ 10,6 10,7 @@ require "em_promise"
require "fiber"
require "minitest/autorun"
require "rantly/minitest_extensions"
require "sentry-ruby"
require "webmock/minitest"
begin
	require "pry-rescue/minitest"


@@ 34,6 35,8 @@ end

require "backend_sgx"

Sentry.init

CONFIG = {
	sgx: "sgx",
	component: {


@@ 78,6 81,16 @@ CONFIG = {
	electrum_notify_url: ->(*) { "http://notify.example.com" }
}.freeze

def panic(e)
	raise e
end

LOG = Class.new {
	def child(*)
		Minitest::Mock.new
	end
}.new.freeze

BLATHER = Class.new {
	def <<(*); end
}.new.freeze

M test/test_registration.rb => test/test_registration.rb +235 -213
@@ 4,6 4,19 @@ require "test_helper"
require "customer"
require "registration"

def execute_command(
	iq=Blather::Stanza::Iq::Command.new.tap { |i| i.from = "test@example.com" },
	blather: BLATHER,
	&blk
)
	Command::Execution.new(
		Minitest::Mock.new,
		blather,
		:to_s.to_proc,
		iq
	).execute(&blk).sync
end

class RegistrationTest < Minitest::Test
	def test_for_registered
		sgx = OpenStruct.new(


@@ 11,11 24,12 @@ class RegistrationTest < Minitest::Test
		)
		iq = Blather::Stanza::Iq::Command.new
		iq.from = "test@example.com"
		result = Registration.for(
			iq,
			Customer.new("test", sgx: sgx),
			Minitest::Mock.new
		).sync
		result = execute_command(iq) do
			Registration.for(
				Customer.new("test", sgx: sgx),
				Minitest::Mock.new
			)
		end
		assert_kind_of Registration::Registered, result
	end
	em :test_for_registered


@@ 26,16 40,17 @@ class RegistrationTest < Minitest::Test
		web_manager["test@example.com"] = "+15555550000"
		iq = Blather::Stanza::Iq::Command.new
		iq.from = "test@example.com"
		result = Registration.for(
			iq,
			Customer.new(
				"test",
				plan_name: "test_usd",
				expires_at: Time.now + 999,
				sgx: sgx
			),
			web_manager
		).sync
		result = execute_command(iq) do
			Registration.for(
				Customer.new(
					"test",
					plan_name: "test_usd",
					expires_at: Time.now + 999,
					sgx: sgx
				),
				web_manager
			)
		end
		assert_kind_of Registration::Finish, result
	end
	em :test_for_activated


@@ 46,20 61,20 @@ class RegistrationTest < Minitest::Test
		web_manager["test@example.com"] = "+15555550000"
		iq = Blather::Stanza::Iq::Command.new
		iq.from = "test@example.com"
		result = Registration.for(
			iq,
			Customer.new("test", sgx: sgx),
			web_manager
		).sync
		result = execute_command(iq) do
			Registration.for(
				Customer.new("test", sgx: sgx),
				web_manager
			)
		end
		assert_kind_of Registration::Activation, result
	end
	em :test_for_not_activated_with_customer_id

	class ActivationTest < Minitest::Test
		Registration::Activation::COMMAND_MANAGER = Minitest::Mock.new
		Command::COMMAND_MANAGER = Minitest::Mock.new
		def setup
			iq = Blather::Stanza::Iq::Command.new
			@activation = Registration::Activation.new(iq, "test", "+15555550000")
			@activation = Registration::Activation.new("test", "+15555550000")
		end

		def test_write


@@ 82,12 97,9 @@ class RegistrationTest < Minitest::Test
					</TelephoneNumberDetails>
				</TelephoneNumberResponse>
			RESPONSE
			result = Minitest::Mock.new
			result.expect(:then, result)
			result.expect(:then, EMPromise.resolve(:test_result))
			Registration::Activation::COMMAND_MANAGER.expect(
			Command::COMMAND_MANAGER.expect(
				:write,
				result,
				EMPromise.reject(:test_result),
				[Matching.new do |iq|
					assert_equal :form, iq.form.type
					assert_equal(


@@ 96,7 108,11 @@ class RegistrationTest < Minitest::Test
					)
				end]
			)
			assert_equal :test_result, @activation.write.sync
			assert_equal(
				:test_result,
				execute_command { @activation.write.catch { |e| e } }
			)
			assert_mock Command::COMMAND_MANAGER
		end
		em :test_write
	end


@@ 161,7 177,6 @@ class RegistrationTest < Minitest::Test

		class BitcoinTest < Minitest::Test
			Registration::Payment::Bitcoin::BTC_SELL_PRICES = Minitest::Mock.new
			Registration::Payment::Bitcoin::BLATHER = Minitest::Mock.new
			Customer::REDIS = Minitest::Mock.new

			def setup


@@ 172,9 187,7 @@ class RegistrationTest < Minitest::Test
					:add_btc_address,
					EMPromise.resolve("testaddr")
				)
				iq = Blather::Stanza::Iq::Command.new
				@bitcoin = Registration::Payment::Bitcoin.new(
					iq,
					@customer,
					"+15555550000"
				)


@@ 192,7 205,8 @@ class RegistrationTest < Minitest::Test

					You will receive a notification when your payment is complete.
				NOTE
				Registration::Payment::Bitcoin::BLATHER.expect(
				blather = Minitest::Mock.new
				blather.expect(
					:<<,
					nil,
					[Matching.new do |reply|


@@ 207,19 221,18 @@ class RegistrationTest < Minitest::Test
					EMPromise.resolve(BigDecimal.new(1))
				)
				@bitcoin.stub(:save, EMPromise.resolve(nil)) do
					@bitcoin.write.sync
					execute_command(blather: blather) do
						@bitcoin.write
					end
				end
				Registration::Payment::Bitcoin::BLATHER.verify
				assert_mock blather
			end
			em :test_write
		end

		class CreditCardTest < Minitest::Test
			def setup
				@iq = Blather::Stanza::Iq::Command.new
				@iq.from = "test@example.com"
				@credit_card = Registration::Payment::CreditCard.new(
					@iq,
					Customer.new("test"),
					"+15555550000"
				)


@@ 234,7 247,6 @@ class RegistrationTest < Minitest::Test
				assert_kind_of(
					Registration::Payment::CreditCard::Activate,
					Registration::Payment::CreditCard.for(
						@iq,
						customer,
						"+15555550000"
					).sync


@@ 242,14 254,27 @@ class RegistrationTest < Minitest::Test
			end
			em :test_for

			def test_reply
				assert_equal [:execute, :next], @credit_card.reply.allowed_actions
				assert_equal(
					"Add credit card, then return here to continue: " \
					"http://creditcard.example.com",
					@credit_card.reply.note.content
				)
			def test_write
				result = execute_command do
					Command::COMMAND_MANAGER.expect(
						:write,
						EMPromise.reject(:test_result),
						[Matching.new do |reply|
							assert_equal [:execute, :next], reply.allowed_actions
							assert_equal(
								"Add credit card, then return here to continue: " \
								"http://creditcard.example.com",
								reply.note.content
							)
						end]
					)

					@credit_card.write.catch { |e| e }
				end

				assert_equal :test_result, result
			end
			em :test_write
		end

		class ActivateTest < Minitest::Test


@@ 257,8 282,7 @@ class RegistrationTest < Minitest::Test
				Minitest::Mock.new
			Registration::Payment::CreditCard::Activate::Transaction =
				Minitest::Mock.new
			Registration::Payment::CreditCard::Activate::COMMAND_MANAGER =
				Minitest::Mock.new
			Command::COMMAND_MANAGER = Minitest::Mock.new

			def test_write
				transaction = PromiseMock.new


@@ 277,19 301,19 @@ class RegistrationTest < Minitest::Test
					assert_equal CONFIG[:activation_amount], amount
					assert_equal :test_default_method, payment_method
				end
				iq = Blather::Stanza::Iq::Command.new
				customer.expect(:bill_plan, nil)
				Registration::Payment::CreditCard::Activate::Finish.expect(
					:new,
					OpenStruct.new(write: nil),
					[Blather::Stanza::Iq, customer, "+15555550000"]
					[customer, "+15555550000"]
				)
				Registration::Payment::CreditCard::Activate.new(
					iq,
					customer,
					:test_default_method,
					"+15555550000"
				).write.sync
				execute_command do
					Registration::Payment::CreditCard::Activate.new(
						customer,
						:test_default_method,
						"+15555550000"
					).write
				end
				Registration::Payment::CreditCard::Activate::Transaction.verify
				transaction.verify
				customer.verify


@@ 301,21 325,11 @@ class RegistrationTest < Minitest::Test
				customer = Minitest::Mock.new(
					Customer.new("test", plan_name: "test_usd")
				)
				Registration::Payment::CreditCard::Activate::Transaction.expect(
					:sale,
					EMPromise.reject("declined")
				) do |acustomer, amount:, payment_method:|
					assert_operator customer, :===, acustomer
					assert_equal CONFIG[:activation_amount], amount
					assert_equal :test_default_method, payment_method
				end
				iq = Blather::Stanza::Iq::Command.new
				iq.from = "test@example.com"
				result = Minitest::Mock.new
				result.expect(:then, nil)
				Registration::Payment::CreditCard::Activate::COMMAND_MANAGER.expect(
				Command::COMMAND_MANAGER.expect(
					:write,
					result,
					EMPromise.reject(:test_result),
					[Matching.new do |reply|
						assert_equal :error, reply.note_type
						assert_equal(


@@ 325,12 339,23 @@ class RegistrationTest < Minitest::Test
						)
					end]
				)
				Registration::Payment::CreditCard::Activate.new(
					iq,
					customer,
					:test_default_method,
					"+15555550000"
				).write.sync
				result = execute_command do
					Registration::Payment::CreditCard::Activate::Transaction.expect(
						:sale,
						EMPromise.reject("declined")
					) do |acustomer, amount:, payment_method:|
						assert_operator customer, :===, acustomer
						assert_equal CONFIG[:activation_amount], amount
						assert_equal :test_default_method, payment_method
					end

					Registration::Payment::CreditCard::Activate.new(
						customer,
						:test_default_method,
						"+15555550000"
					).write.catch { |e| e }
				end
				assert_equal :test_result, result
				Registration::Payment::CreditCard::Activate::Transaction.verify
			end
			em :test_write_declines


@@ 341,164 366,157 @@ class RegistrationTest < Minitest::Test
				Minitest::Mock.new
			Registration::Payment::InviteCode::REDIS =
				Minitest::Mock.new
			Registration::Payment::InviteCode::COMMAND_MANAGER =
				Minitest::Mock.new
			Command::COMMAND_MANAGER = Minitest::Mock.new
			Registration::Payment::InviteCode::Finish =
				Minitest::Mock.new

			def test_write
				customer = Customer.new("test", plan_name: "test_usd")
				Registration::Payment::InviteCode::REDIS.expect(
					:get,
					EMPromise.resolve(nil),
					["jmp_invite_tries-test"]
				)
				Registration::Payment::InviteCode::COMMAND_MANAGER.expect(
					:write,
					EMPromise.resolve(
						Blather::Stanza::Iq::Command.new.tap { |iq|
							iq.form.fields = [{ var: "code", value: "abc" }]
						}
					),
					[Matching.new do |reply|
						assert_equal :form, reply.form.type
						assert_nil reply.form.instructions
					end]
				)
				Registration::Payment::InviteCode::DB.expect(:transaction, true, [])
				Registration::Payment::InviteCode::Finish.expect(
					:new,
					OpenStruct.new(write: nil),
					[
						Blather::Stanza::Iq::Command,
						customer,
						"+15555550000"
					]
				)
				iq = Blather::Stanza::Iq::Command.new
				iq.from = "test@example.com"
				Registration::Payment::InviteCode.new(
					iq,
					customer,
					"+15555550000"
				).write.sync
				Registration::Payment::InviteCode::COMMAND_MANAGER.verify
				Registration::Payment::InviteCode::DB.verify
				Registration::Payment::InviteCode::REDIS.verify
				Registration::Payment::InviteCode::Finish.verify
				execute_command do
					Registration::Payment::InviteCode::REDIS.expect(
						:get,
						EMPromise.resolve(nil),
						["jmp_invite_tries-test"]
					)
					Command::COMMAND_MANAGER.expect(
						:write,
						EMPromise.resolve(
							Blather::Stanza::Iq::Command.new.tap { |iq|
								iq.form.fields = [{ var: "code", value: "abc" }]
							}
						),
						[Matching.new do |reply|
							assert_equal :form, reply.form.type
							assert_nil reply.form.instructions
						end]
					)

					Registration::Payment::InviteCode.new(
						customer,
						"+15555550000"
					).write
				end
				assert_mock Command::COMMAND_MANAGER
				assert_mock Registration::Payment::InviteCode::DB
				assert_mock Registration::Payment::InviteCode::REDIS
				assert_mock Registration::Payment::InviteCode::Finish
			end
			em :test_write

			def test_write_bad_code
				customer = Customer.new("test", plan_name: "test_usd")
				Registration::Payment::InviteCode::REDIS.expect(
					:get,
					EMPromise.resolve(0),
					["jmp_invite_tries-test"]
				)
				Registration::Payment::InviteCode::COMMAND_MANAGER.expect(
					:write,
					EMPromise.resolve(
						Blather::Stanza::Iq::Command.new.tap { |iq|
							iq.form.fields = [{ var: "code", value: "abc" }]
						}
					),
					[Matching.new do |reply|
						assert_equal :form, reply.form.type
						assert_nil reply.form.instructions
					end]
				)
				Registration::Payment::InviteCode::DB.expect(:transaction, []) do
					raise Registration::Payment::InviteCode::Invalid, "wut"
				end
				Registration::Payment::InviteCode::REDIS.expect(
					:incr,
					EMPromise.resolve(nil),
					["jmp_invite_tries-test"]
				)
				Registration::Payment::InviteCode::REDIS.expect(
					:expire,
					EMPromise.resolve(nil),
					["jmp_invite_tries-test", 60 * 60]
				)
				Registration::Payment::InviteCode::COMMAND_MANAGER.expect(
					:write,
					EMPromise.reject(Promise::Error.new),
					[Matching.new do |reply|
						assert_equal :form, reply.form.type
						assert_equal "wut", reply.form.instructions
					end]
				)
				iq = Blather::Stanza::Iq::Command.new
				iq.from = "test@example.com"
				assert_raises Promise::Error do
				result = execute_command do
					customer = Customer.new("test", plan_name: "test_usd")
					Registration::Payment::InviteCode::REDIS.expect(
						:get,
						EMPromise.resolve(0),
						["jmp_invite_tries-test"]
					)
					Registration::Payment::InviteCode::DB.expect(:transaction, []) do
						raise Registration::Payment::InviteCode::Invalid, "wut"
					end
					Registration::Payment::InviteCode::REDIS.expect(
						:incr,
						EMPromise.resolve(nil),
						["jmp_invite_tries-test"]
					)
					Registration::Payment::InviteCode::REDIS.expect(
						:expire,
						EMPromise.resolve(nil),
						["jmp_invite_tries-test", 60 * 60]
					)
					Command::COMMAND_MANAGER.expect(
						:write,
						EMPromise.resolve(
							Blather::Stanza::Iq::Command.new.tap { |iq|
								iq.form.fields = [{ var: "code", value: "abc" }]
							}
						),
						[Matching.new do |reply|
							assert_equal :form, reply.form.type
							assert_nil reply.form.instructions
						end]
					)
					Command::COMMAND_MANAGER.expect(
						:write,
						EMPromise.reject(:test_result),
						[Matching.new do |reply|
							assert_equal :form, reply.form.type
							assert_equal "wut", reply.form.instructions
						end]
					)

					Registration::Payment::InviteCode.new(
						iq,
						customer,
						"+15555550000"
					).write.sync
					).write.catch { |e| e }
				end
				Registration::Payment::InviteCode::COMMAND_MANAGER.verify
				Registration::Payment::InviteCode::DB.verify
				Registration::Payment::InviteCode::REDIS.verify
				assert_equal :test_result, result
				assert_mock Command::COMMAND_MANAGER
				assert_mock Registration::Payment::InviteCode::DB
				assert_mock Registration::Payment::InviteCode::REDIS
			end
			em :test_write_bad_code

			def test_write_bad_code_over_limit
				customer = Customer.new("test", plan_name: "test_usd")
				Registration::Payment::InviteCode::REDIS.expect(
					:get,
					EMPromise.resolve(11),
					["jmp_invite_tries-test"]
				)
				Registration::Payment::InviteCode::COMMAND_MANAGER.expect(
					:write,
					EMPromise.resolve(
						Blather::Stanza::Iq::Command.new.tap { |iq|
							iq.form.fields = [{ var: "code", value: "abc" }]
						}
					),
					[Matching.new do |reply|
						assert_equal :form, reply.form.type
						assert_nil reply.form.instructions
					end]
				)
				Registration::Payment::InviteCode::REDIS.expect(
					:incr,
					EMPromise.resolve(nil),
					["jmp_invite_tries-test"]
				)
				Registration::Payment::InviteCode::REDIS.expect(
					:expire,
					EMPromise.resolve(nil),
					["jmp_invite_tries-test", 60 * 60]
				)
				Registration::Payment::InviteCode::COMMAND_MANAGER.expect(
					:write,
					EMPromise.reject(Promise::Error.new),
					[Matching.new do |reply|
						assert_equal :form, reply.form.type
						assert_equal "Too many wrong attempts", reply.form.instructions
					end]
				)
				iq = Blather::Stanza::Iq::Command.new
				iq.from = "test@example.com"
				assert_raises Promise::Error do
				result = execute_command do
					customer = Customer.new("test", plan_name: "test_usd")
					Registration::Payment::InviteCode::REDIS.expect(
						:get,
						EMPromise.resolve(11),
						["jmp_invite_tries-test"]
					)
					Command::COMMAND_MANAGER.expect(
						:write,
						EMPromise.resolve(
							Blather::Stanza::Iq::Command.new.tap { |iq|
								iq.form.fields = [{ var: "code", value: "abc" }]
							}
						),
						[Matching.new do |reply|
							assert_equal :form, reply.form.type
							assert_nil reply.form.instructions
						end]
					)
					Registration::Payment::InviteCode::REDIS.expect(
						:incr,
						EMPromise.resolve(nil),
						["jmp_invite_tries-test"]
					)
					Registration::Payment::InviteCode::REDIS.expect(
						:expire,
						EMPromise.resolve(nil),
						["jmp_invite_tries-test", 60 * 60]
					)
					Command::COMMAND_MANAGER.expect(
						:write,
						EMPromise.reject(:test_result),
						[Matching.new do |reply|
							assert_equal :form, reply.form.type
							assert_equal "Too many wrong attempts", reply.form.instructions
						end]
					)
					Registration::Payment::InviteCode.new(
						iq,
						customer,
						"+15555550000"
					).write.sync
					).write.catch { |e| e }
				end
				Registration::Payment::InviteCode::COMMAND_MANAGER.verify
				Registration::Payment::InviteCode::REDIS.verify
				assert_equal :test_result, result
				assert_mock Command::COMMAND_MANAGER
				assert_mock Registration::Payment::InviteCode::REDIS
			end
			em :test_write_bad_code_over_limit
		end
	end

	class FinishTest < Minitest::Test
		Registration::Finish::BLATHER = Minitest::Mock.new
		Registration::Finish::REDIS = Minitest::Mock.new
		BackendSgx::REDIS = Minitest::Mock.new



@@ 507,7 525,6 @@ class RegistrationTest < Minitest::Test
			iq = Blather::Stanza::Iq::Command.new
			iq.from = "test\\40example.com@cheogram.com"
			@finish = Registration::Finish.new(
				iq,
				Customer.new("test", sgx: @sgx),
				"+15555550000"
			)


@@ 547,17 564,12 @@ class RegistrationTest < Minitest::Test
					"Content-Type" => "application/json"
				}
			).to_return(status: 201)
			@sgx.expect(
				:register!,
				EMPromise.resolve(OpenStruct.new(error?: false)),
				["+15555550000"]
			)
			Registration::Finish::REDIS.expect(
				:set,
				nil,
				[
					"catapult_fwd-+15555550000",
					"sip:test%5C40example.com%40cheogram.com@sip.cheogram.com"
					"sip:test%40example.com@sip.cheogram.com"
				]
			)
			BackendSgx::REDIS.expect(


@@ 565,7 577,8 @@ class RegistrationTest < Minitest::Test
				nil,
				["catapult_fwd_timeout-customer_test@component", 25]
			)
			Registration::Finish::BLATHER.expect(
			blather = Minitest::Mock.new
			blather.expect(
				:<<,
				nil,
				[Matching.new do |reply|


@@ 577,12 590,20 @@ class RegistrationTest < Minitest::Test
					)
				end]
			)
			@finish.write.sync
			execute_command(blather: blather) do
				@sgx.expect(
					:register!,
					EMPromise.resolve(OpenStruct.new(error?: false)),
					["+15555550000"]
				)

				@finish.write
			end
			assert_requested create_order
			@sgx.verify
			Registration::Finish::REDIS.verify
			BackendSgx::REDIS.verify
			Registration::Finish::BLATHER.verify
			assert_mock @sgx
			assert_mock Registration::Finish::REDIS
			assert_mock BackendSgx::REDIS
			assert_mock blather
		end
		em :test_write



@@ 605,7 626,8 @@ class RegistrationTest < Minitest::Test
					<OrderStatus>FAILED</OrderStatus>
				</OrderResponse>
			RESPONSE
			Registration::Finish::BLATHER.expect(
			blather = Minitest::Mock.new
			blather.expect(
				:<<,
				nil,
				[Matching.new do |reply|


@@ 618,9 640,9 @@ class RegistrationTest < Minitest::Test
					)
				end]
			)
			@finish.write.sync
			execute_command(blather: blather) { @finish.write }
			assert_requested create_order
			Registration::Finish::BLATHER.verify
			assert_mock blather
		end
		em :test_write_tn_fail
	end

M test/test_web_register_manager.rb => test/test_web_register_manager.rb +3 -12
@@ 15,18 15,9 @@ class WebRegisterManagerTest < Minitest::Test
	end

	def test_choose_tel_have_tel
		@manager["jid@example.com"] = "+15555550000"
		iq = Blather::Stanza::Iq.new
		iq.from = "jid@example.com"
		assert_equal [iq, "+15555550000"], @manager.choose_tel(iq).sync
		jid = "jid@example.com"
		@manager[jid] = "+15555550000"
		assert_equal "+15555550000", @manager[jid].choose_tel.sync
	end
	em :test_choose_tel_have_tel

	def test_choose_tel_not_have_tel
		skip "ChooseTel not implemented yet"
		iq = Blather::Stanza::Iq.new
		iq.from = "jid@example.com"
		@manager.choose_tel(iq).sync
	end
	em :test_choose_tel_not_have_tel
end