Merge branch 'premium'

Stephen Paul Weber created

* premium:
  Factor out BuyNumber
  Inject price
  Bill after charge
  Inline useless methods
  Remove unused method
  Fix test
  charge for premium nums
  refactor registration_test to use real Tn's
  two changes related to registration forms
  price functionality on Tns
  Remove legacy card charge behaviour
  two fixes related to eSIM nicks:
  Factor out reload customer, check balance, maybe bill
  Unify more invite code handling
  Remove mostly-unused final_message
  Remove legacy card charge behaviour

Change summary

forms/registration/activate.rb |   2 
forms/registration/bch.rb      |   5 
forms/registration/btc.rb      |   5 
forms/registration/mail.rb     |   5 
lib/invites_repo.rb            |   4 
lib/parent_code_repo.rb        |  17 +
lib/registration.rb            | 295 +++++++++------------
lib/statsd.rb                  |   4 
lib/tel_selections.rb          |  43 ++
sgx_jmp.rb                     |   1 
test/test_helper.rb            |   2 
test/test_registration.rb      | 489 +++++++++++++++++++++--------------
12 files changed, 486 insertions(+), 386 deletions(-)

Detailed changes

forms/registration/activate.rb 🔗

@@ -3,7 +3,7 @@ title "Activate JMP"
 
 instructions <<~I
 	You've selected #{@tel} as your JMP number.
-	To activate your account, you can either deposit $#{CONFIG[:activation_amount]} to your balance or enter your referral code if you have one.
+	To activate your account, you can either deposit $#{'%.2f' % (CONFIG[:activation_amount] + @tel.price)} to your balance or enter your referral code if you have one.
 	(If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
 I
 

forms/registration/bch.rb 🔗

@@ -13,7 +13,4 @@ field(
 	value: @addr
 )
 
-instructions(
-	"You will received a notification when your payment is complete." \
-	"#{@final_message}"
-)
+instructions "You will received a notification when your payment is complete."

forms/registration/btc.rb 🔗

@@ -13,7 +13,4 @@ field(
 	value: @addr
 )
 
-instructions(
-	"You will received a notification when your payment is complete." \
-	"#{@final_message}"
-)
+instructions "You will received a notification when your payment is complete."

forms/registration/mail.rb 🔗

@@ -3,10 +3,9 @@ title "Activate by Mail or Interac e-Tranfer"
 
 instructions(
 	"Activate your account by sending at least " \
-	"$#{CONFIG[:activation_amount]}\nWe support payment by " \
+	"$#{'%.2f' % @price}\nWe support payment by " \
 	"postal mail or, in Canada, by Interac e-Transfer.\n\n" \
-	"You will receive a notification when your payment is complete." \
-	"#{@final_message}"
+	"You will receive a notification when your payment is complete."
 )
 
 if @customer_id

lib/invites_repo.rb 🔗

@@ -133,7 +133,9 @@ protected
 	end
 
 	def invalid_code(customer_id, code)
-		@redis.incr("jmp_invite_tries-#{customer_id}").then {
+		stash_code(customer_id, code).then {
+			@redis.incr("jmp_invite_tries-#{customer_id}")
+		}.then {
 			@redis.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
 		}.then {
 			@redis.hexists("jmp_group_codes", code)

lib/parent_code_repo.rb 🔗

@@ -7,6 +7,8 @@ require_relative "customer"
 require_relative "trust_level_repo"
 
 class ParentCodeRepo
+	class Invalid < StandardError; end
+
 	def initialize(
 		redis: REDIS,
 		db: DB,
@@ -17,6 +19,21 @@ class ParentCodeRepo
 		@trust_level_repo = trust_level_repo
 	end
 
+	def claim_code(customer, code)
+		customer_domain = ProxiedJID.new(customer.jid).unproxied.domain
+		find(code).then do |parent|
+			raise Invalid, "Not a valid code" unless parent
+
+			if parent && customer_domain == CONFIG[:onboarding_domain]
+				raise "Please create a new Jabber ID before creating a subaccount."
+			end
+
+			plan_name = customer.plan_name
+			customer = customer.with_plan(plan_name, parent_customer_id: parent)
+			customer.save_plan!.then { block_given? ? yield(customer) : customer }
+		end
+	end
+
 	def find(code)
 		@redis.hget("jmp_parent_codes", code).then do |parent_id|
 			trust_level_guard(parent_id).then { parent_id }

lib/registration.rb 🔗

@@ -16,6 +16,12 @@ require_relative "./proxied_jid"
 require_relative "./tel_selections"
 require_relative "./welcome_message"
 
+def reload_customer(customer)
+	EMPromise.resolve(nil).then do
+		Command.execution.customer_repo.find(customer.customer_id)
+	end
+end
+
 class Registration
 	def self.for(customer, google_play_userid, tel_selections)
 		if (reg = customer.registered?)
@@ -41,13 +47,6 @@ class Registration
 		end
 	end
 
-	def self.guard_onboarding_subaccounts(customer)
-		customer_domain = ProxiedJID.new(customer.jid).domain
-		return unless customer_domain == CONFIG[:onboarding_domain]
-
-		raise "Please create a new Jabber ID before creating a subaccount."
-	end
-
 	class Registered
 		def self.for(customer, tel)
 			jid = ProxiedJID.new(customer.jid).unproxied
@@ -108,7 +107,6 @@ class Registration
 		def initialize(customer, tel)
 			@customer = customer
 			@tel = tel
-			@invites = InvitesRepo.new(DB, REDIS)
 		end
 
 		attr_reader :customer, :tel
@@ -125,33 +123,15 @@ class Registration
 		end
 
 		def next_step(iq)
-			code = iq.form.field("code")&.value&.to_s
-			save_customer_plan(iq, code).then {
-				finish_if_valid_invite(code)
-			}.catch_only(InvitesRepo::Invalid) do
-				@invites.stash_code(customer.customer_id, code).then do
-					Payment.for(iq, @customer, @tel).then(&:write)
-				end
-			end
-		end
-
-	protected
-
-		def finish_if_valid_invite(code)
-			@invites.claim_code(@customer.customer_id, code) {
-				@customer.activate_plan_starting_now
-			}.then do
-				Finish.new(@customer, @tel).write
-			end
-		end
-
-		def save_customer_plan(iq, code)
-			Registration.guard_onboarding_subaccounts(@customer)
-
-			ParentCodeRepo.new(redis: REDIS, db: DB).find(code).then do |parent|
+			EMPromise.resolve(nil).then do
 				plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
-				@customer = @customer.with_plan(plan.name, parent_customer_id: parent)
-				@customer.save_plan!
+				@customer = @customer.with_plan(plan.name)
+				Registration::Payment::InviteCode.new(
+					@customer, @tel, finish: Finish, db: DB, redis: REDIS
+				).parse(iq, force_save_plan: true)
+					.catch_only(InvitesRepo::Invalid) do
+						Payment.for(iq, @customer, @tel).then(&:write)
+					end
 			end
 		end
 
@@ -279,10 +259,16 @@ class Registration
 			@kinds ||= {}
 		end
 
-		def self.for(iq, customer, tel, final_message: nil, finish: Finish)
+		def self.for(
+			iq, customer, tel,
+			finish: Finish, maybe_bill: MaybeBill,
+			price: CONFIG[:activation_amount] + tel.price
+		)
 			kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
 				raise "Invalid activation method"
-			}.call(customer, tel, final_message: final_message, finish: finish)
+			}.call(
+				customer, tel, finish: finish, maybe_bill: maybe_bill, price: price
+			)
 		end
 
 		class CryptoPaymentMethod
@@ -298,11 +284,14 @@ class Registration
 				raise NotImplementedError, "Subclass must implement"
 			end
 
-			def initialize(customer, tel, final_message: nil, **)
+			def initialize(
+				customer, tel,
+				price: CONFIG[:activation_amount] + tel.price, **
+			)
 				@customer = customer
 				@customer_id = customer.customer_id
 				@tel = tel
-				@final_message = final_message
+				@price = price
 			end
 
 			def save
@@ -312,13 +301,10 @@ class Registration
 			attr_reader :customer_id, :tel
 
 			def form(rate, addr)
-				amount = CONFIG[:activation_amount] / rate
-
 				FormTemplate.render(
 					reg_form_name,
-					amount: amount,
-					addr: addr,
-					final_message: @final_message
+					amount: @price / rate,
+					addr: addr
 				)
 			end
 
@@ -392,34 +378,45 @@ class Registration
 			end
 		end
 
-		class CreditCard
-			Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
+		class MaybeBill
+			def initialize(customer, tel, finish: Finish)
+				@customer = customer
+				@tel = tel
+				@finish = finish
+			end
 
-			def self.for(in_customer, tel, finish: Finish, **)
-				reload_customer(in_customer).then do |(customer, payment_methods)|
+			def call
+				reload_customer(@customer).then do |customer|
 					if customer.balance >= CONFIG[:activation_amount_accept]
-						next BillPlan.new(customer, tel, finish: finish)
+						next BillPlan.new(customer, @tel, finish: @finish)
 					end
 
-					if (method = payment_methods.default_payment_method)
-						next Activate.new(customer, method, tel, finish: finish)
-					end
-
-					new(customer, tel, finish: finish)
+					yield customer
 				end
 			end
+		end
 
-			def self.reload_customer(customer)
-				EMPromise.all([
-					Command.execution.customer_repo.find(customer.customer_id),
-					customer.payment_methods
-				])
+		class JustCharge
+			def initialize(customer, *, **)
+				@customer = customer
 			end
 
-			def initialize(customer, tel, finish: Finish)
+			def call; end
+		end
+
+		class CreditCard
+			Payment.kinds[:credit_card] = method(:new)
+
+			def initialize(
+				customer, tel,
+				finish: Finish, maybe_bill: MaybeBill,
+				price: CONFIG[:activation_amount] + tel.price
+			)
 				@customer = customer
 				@tel = tel
 				@finish = finish
+				@maybe_bill = maybe_bill.new(customer, tel, finish: finish)
+				@price = price
 			end
 
 			def oob(reply)
@@ -427,8 +424,8 @@ class Registration
 				oob.url = CONFIG[:credit_card_url].call(
 					reply.to.stripped.to_s.gsub("\\", "%5C"),
 					@customer.customer_id
-				) + "&amount=#{CONFIG[:activation_amount]}"
-				oob.desc = "Add credit card, save, then next here to continue"
+				) + "&amount=#{@price.ceil}"
+				oob.desc = "Pay by credit card, save, then next here to continue"
 				oob
 			end
 
@@ -441,85 +438,13 @@ class Registration
 				}.then do |iq|
 					next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
 
-					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
-				end
-			end
-
-			class Activate
-				def initialize(customer, payment_method, tel, finish: Finish)
-					@customer = customer
-					@payment_method = payment_method
-					@tel = tel
-					@finish = finish
-				end
-
-				def write
-					CreditCardSale.create(
-						@customer,
-						amount: CONFIG[:activation_amount],
-						payment_method: @payment_method
-					).then(
-						->(_) { sold },
-						->(_) { declined }
-					)
-				end
-
-			protected
-
-				def sold
-					BillPlan.new(@customer, @tel, finish: @finish).write
-				end
-
-				DECLINE_MESSAGE =
-					"Your bank declined the transaction. " \
-					"Often this happens when a person's credit card " \
-					"is a US card that does not support international " \
-					"transactions, as JMP is not based in the USA, though " \
-					"we do support transactions in USD.\n\n" \
-					"You may add another card"
-
-				def decline_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
-					) + "&amount=#{CONFIG[:activation_amount]}"
-					oob.desc = DECLINE_MESSAGE
-					oob
-				end
-
-				def declined
-					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, finish: @finish).then(&:write)
-					end
+					@maybe_bill.call { self }&.then(&:write)
 				end
 			end
 		end
 
 		class InviteCode
-			Payment.kinds[:code] = ->(*args, **kw) { self.for(*args, **kw) }
-
-			def self.for(in_customer, tel, finish: Finish, **)
-				reload_customer(in_customer).then do |customer|
-					if customer.balance >= CONFIG[:activation_amount_accept]
-						next BillPlan.new(customer, tel, finish: finish)
-					end
-
-					msg = if customer.balance.positive?
-						"Account balance not enough to cover the activation"
-					end
-					new(customer, tel, error: msg, finish: Finish)
-				end
-			end
-
-			def self.reload_customer(customer)
-				Command.execution.customer_repo.find(customer.customer_id)
-			end
+			Payment.kinds[:code] = method(:new)
 
 			FIELDS = [{
 				var: "code",
@@ -528,12 +453,16 @@ class Registration
 				required: true
 			}].freeze
 
-			def initialize(customer, tel, error: nil, finish: Finish, **)
+			def initialize(
+				customer, tel,
+				error: nil, finish: Finish, db: DB, redis: REDIS, **
+			)
 				@customer = customer
 				@tel = tel
 				@error = error
 				@finish = finish
-				@parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB)
+				@invites_repo = InvitesRepo.new(db, redis)
+				@parent_code_repo = ParentCodeRepo.new(db: db, redis: redis)
 			end
 
 			def add_form(reply)
@@ -548,48 +477,44 @@ class Registration
 				Command.reply { |reply|
 					reply.allowed_actions = [:next, :prev]
 					add_form(reply)
-				}.then(&method(:parse))
+				}.then(&method(:parse)).catch_only(InvitesRepo::Invalid) { |e|
+					invalid_code(e).write
+				}
 			end
 
-			def parse(iq)
+			def parse(iq, force_save_plan: false)
 				return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
 
-				verify(iq.form.field("code")&.value&.to_s)
-					.catch_only(InvitesRepo::Invalid, &method(:invalid_code))
+				verify(iq.form.field("code")&.value&.to_s, force_save_plan)
 					.then(&:write)
 			end
 
 		protected
 
 			def invalid_code(e)
-				InviteCode.new(@customer, @tel, error: e.message)
+				self.class.new(@customer, @tel, error: e.message, finish: @finish)
 			end
 
 			def customer_id
 				@customer.customer_id
 			end
 
-			def verify(code)
-				@parent_code_repo.find(code).then do |parent_customer_id|
-					if parent_customer_id
-						set_parent(parent_customer_id)
-					else
-						InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) {
+			def verify(code, force_save_plan)
+				@parent_code_repo.claim_code(@customer, code) {
+					check_parent_balance
+				}.catch_only(ParentCodeRepo::Invalid) {
+					(@customer.save_plan! if force_save_plan).then do
+						@invites_repo.claim_code(customer_id, code) {
 							@customer.activate_plan_starting_now
-						}.then { Finish.new(@customer, @tel) }
+						}.then { @finish.new(@customer, @tel) }
 					end
-				end
+				}
 			end
 
-			def set_parent(parent_customer_id)
-				Registration.guard_onboarding_subaccounts(@customer)
-
-				@customer = @customer.with_plan(
-					@customer.plan_name,
-					parent_customer_id: parent_customer_id
-				)
-				@customer.save_plan!.then do
-					self.class.for(@customer, @tel, finish: @finish)
+			def check_parent_balance
+				MaybeBill.new(@customer, @tel, finish: @finish).call do
+					msg = "Account balance not enough to cover the activation"
+					invalid_code(RuntimeError.new(msg))
 				end
 			end
 		end
@@ -597,17 +522,20 @@ class Registration
 		class Mail
 			Payment.kinds[:mail] = method(:new)
 
-			def initialize(customer, tel, final_message: nil, **)
+			def initialize(
+				customer, tel,
+				price: CONFIG[:activation_amount] + tel.price, **
+			)
 				@customer = customer
 				@tel = tel
-				@final_message = final_message
+				@price = price
 			end
 
 			def form
 				FormTemplate.render(
 					"registration/mail",
 					currency: @customer.currency,
-					final_message: @final_message,
+					price: @price,
 					**onboarding_extras
 				)
 			end
@@ -645,11 +573,39 @@ class Registration
 
 		def write
 			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
-				@finish.new(@customer, @tel).write
+				updated_customer =
+					@customer.with_balance(@customer.balance - @customer.monthly_price)
+				@finish.new(updated_customer, @tel).write
 			end
 		end
 	end
 
+	class BuyNumber
+		def initialize(customer, tel)
+			@customer = customer
+			@tel = tel
+		end
+
+		def write
+			Command.reply { |reply|
+				reply.command << FormTemplate.render(
+					"registration/buy_number",
+					tel: @tel
+				)
+			}.then(&method(:parse)).then(&:write)
+		end
+
+	protected
+
+		def parse(iq)
+			Payment.for(
+				iq, @customer, @tel,
+				maybe_bill: ::Registration::Payment::JustCharge,
+				price: @tel.price
+			)
+		end
+	end
+
 	class Finish
 		def initialize(customer, tel)
 			@customer = customer
@@ -658,6 +614,8 @@ class Registration
 		end
 
 		def write
+			return buy_number if @customer.balance < @tel.price
+
 			@tel.order(DB, @customer).then(
 				->(_) { customer_active_tel_purchased },
 				method(:number_purchase_error)
@@ -666,6 +624,14 @@ class Registration
 
 	protected
 
+		def buy_number
+			BuyNumber.new(@customer, @tel).write.then do
+				reload_customer(@customer).then { |customer|
+					Finish.new(customer, @tel)
+				}.then(&:write)
+			end
+		end
+
 		def number_purchase_error(e)
 			Command.log.error "number_purchase_error", e
 			TEL_SELECTIONS.delete(@customer.jid).then {
@@ -712,7 +678,8 @@ class Registration
 				EMPromise.all([
 					TEL_SELECTIONS.delete(@customer.jid),
 					put_default_fwd,
-					use_referral_code
+					use_referral_code,
+					@tel.charge(@customer)
 				])
 			}.then do
 				FinishOnboarding.for(@customer, @tel).then(&:write)

lib/statsd.rb 🔗

@@ -17,10 +17,6 @@ Registration::Payment::Bitcoin.statsd_count :write, "registration.payment.bitcoi
 Registration::Payment::CreditCard.extend StatsD::Instrument
 Registration::Payment::CreditCard.statsd_count :write, "registration.payment.credit_card"
 
-Registration::Payment::CreditCard::Activate.extend StatsD::Instrument
-Registration::Payment::CreditCard::Activate.statsd_count :write, "registration.payment.credit_card.activate"
-Registration::Payment::CreditCard::Activate.statsd_count :declined, "registration.payment.credit_card.activate_declined"
-
 Registration::Payment::InviteCode.extend StatsD::Instrument
 Registration::Payment::InviteCode.statsd_count :write, "registration.payment.invite_code"
 

lib/tel_selections.rb 🔗

@@ -155,7 +155,7 @@ class TelSelections
 							full_number: row["tel"].sub(/\A\+1/, ""),
 							city: row["locality"],
 							state: row["region"]
-						), row["bandwidth_account_id"])
+						), row["bandwidth_account_id"], price: row["premium_price"])
 					}
 				}
 			end
@@ -215,13 +215,41 @@ class TelSelections
 
 			def self.for_pending_value(value)
 				if value.start_with?("LocalInventory/")
-					tel, account = value.sub(/\ALocalInventory\//, "").split("/", 2)
-					LocalInventory.new(Tn.new(tel), account)
+					tel, account, price =
+						value.sub(/\ALocalInventory\//, "").split("/", 3)
+					LocalInventory.new(Tn.new(tel), account, price: price.to_d)
 				else
 					Bandwidth.new(Tn.new(value))
 				end
 			end
 
+			def price
+				0
+			end
+
+			# Creates and inserts transaction charging the customer
+			# for the phone number. If price <= 0 this is a noop.
+			# This method never checks customer balance.
+			#
+			# @param customer [Customer] the customer to charge
+			def charge(customer)
+				return if price <= 0
+
+				transaction(customer).insert
+			end
+
+			# @param customer [Customer] the customer to charge
+			def transaction(customer)
+				Transaction.new(
+					customer_id: customer.customer_id,
+					transaction_id:
+						"#{customer.customer_id}-bill-#{@tel}-at-#{Time.now.to_i}",
+					amount: -price,
+					note: "One-time charge for number: #{formatted_tel}",
+					ignore_duplicate: false
+				)
+			end
+
 			def initialize(tel)
 				@tel = tel
 			end
@@ -286,6 +314,8 @@ class TelSelections
 			end
 
 			class LocalInventory < SimpleDelegator
+				attr_reader :price
+
 				def self.fetch(tn, db: DB)
 					db.query_defer("SELECT * FROM tel_inventory WHERE tel = $1", [tn])
 						.then { |rows|
@@ -294,18 +324,19 @@ class TelSelections
 								full_number: row["tel"].sub(/\A\+1/, ""),
 								city: row["locality"],
 								state: row["region"]
-							), row["bandwidth_account_id"])
+							), row["bandwidth_account_id"], price: row["premium_price"])
 						}
 					}
 				end
 
-				def initialize(tn, bandwidth_account_id)
+				def initialize(tn, bandwidth_account_id, price: 0)
 					super(tn)
 					@bandwidth_account_id = bandwidth_account_id
+					@price = price
 				end
 
 				def pending_value
-					"LocalInventory/#{tel}/#{@bandwidth_account_id}"
+					"LocalInventory/#{tel}/#{@bandwidth_account_id}/#{price}"
 				end
 
 				def reserve(*)

sgx_jmp.rb 🔗

@@ -621,7 +621,6 @@ Command.new(
 		customer.save_plan!.then {
 			Registration::Payment.for(
 				iq, customer, customer.registered?.phone,
-				final_message: PaypalDone::MESSAGE,
 				finish: PaypalDone
 			)
 		}.then(&:write).catch_only(Command::Execution::FinalStanza) do |s|

test/test_helper.rb 🔗

@@ -304,7 +304,7 @@ class FakeRedis
 	end
 
 	def hget(key, field)
-		@values.dig(key, field)
+		EMPromise.resolve(@values.dig(key, field))
 	end
 
 	def hexists(key, field)

test/test_registration.rb 🔗

@@ -155,7 +155,8 @@ class RegistrationTest < Minitest::Test
 
 		def setup
 			@customer = Minitest::Mock.new(customer)
-			@activation = Registration::Activation.new(@customer, "+15555550000")
+			@tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
+			@activation = Registration::Activation.new(@customer, @tel)
 		end
 
 		def test_write
@@ -167,7 +168,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
@@ -179,7 +180,7 @@ class RegistrationTest < Minitest::Test
 			Registration::Activation::Payment.expect(
 				:for,
 				EMPromise.reject(:test_result),
-				[Blather::Stanza::Iq, @customer, "+15555550000"]
+				[Blather::Stanza::Iq, @customer, @tel]
 			)
 			assert_equal(
 				:test_result,
@@ -191,6 +192,43 @@ class RegistrationTest < Minitest::Test
 		end
 		em :test_write
 
+		def test_write_with_onboarding_jid
+			Command::COMMAND_MANAGER.expect(
+				:write,
+				EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+					iq.form.fields = [{ var: "plan_name", value: "test_usd" }]
+				}),
+				[Matching.new do |iq|
+					assert_equal :form, iq.form.type
+					assert_equal(
+						"You've selected (555) 555-0000 as your JMP number.",
+						iq.form.instructions.lines.first.chomp
+					)
+				end]
+			)
+			@customer.expect(
+				:jid,
+				Blather::JID.new("test\\40onboarding.example.com@proxy")
+			)
+			@customer.expect(:with_plan, @customer) do |*args, **|
+				assert_equal ["test_usd"], args
+			end
+			@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
+			Registration::Activation::Payment.expect(
+				:for,
+				EMPromise.reject(:test_result),
+				[Blather::Stanza::Iq, @customer, @tel]
+			)
+			assert_equal(
+				:test_result,
+				execute_command { @activation.write.catch { |e| e } }
+			)
+			assert_mock Command::COMMAND_MANAGER
+			assert_mock @customer
+			assert_mock Registration::Activation::Payment
+		end
+		em :test_write_with_onboarding_jid
+
 		def test_write_bad_plan
 			Command::COMMAND_MANAGER.expect(
 				:write,
@@ -200,7 +238,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
@@ -226,7 +264,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
@@ -245,7 +283,7 @@ class RegistrationTest < Minitest::Test
 			Registration::Activation::Finish.expect(
 				:new,
 				OpenStruct.new(write: EMPromise.reject(:test_result)),
-				[@customer, "+15555550000"]
+				[@customer, @tel]
 			)
 			assert_equal(
 				:test_result,
@@ -270,7 +308,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
@@ -288,7 +326,7 @@ class RegistrationTest < Minitest::Test
 			Registration::Activation::Payment.expect(
 				:for,
 				EMPromise.reject(:test_result),
-				[Blather::Stanza::Iq, @customer, "+15555550000"]
+				[Blather::Stanza::Iq, @customer, @tel]
 			)
 			assert_equal(
 				:test_result,
@@ -308,52 +346,57 @@ class RegistrationTest < Minitest::Test
 		em :test_write_with_group_code
 
 		def test_write_with_parent_code
-			Command::COMMAND_MANAGER.expect(
-				:write,
-				EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
-					iq.form.fields = [
-						{ var: "plan_name", value: "test_usd" },
-						{ var: "code", value: "PARENT_CODE" }
-					]
-				}),
-				[Matching.new do |iq|
-					assert_equal :form, iq.form.type
-					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
-						iq.form.instructions.lines.first.chomp
-					)
-				end]
-			)
-			Registration::Activation::DB.expect(
-				:query_one, {}, [String, "1"], default: {}
-			)
-			Registration::Activation::DB.expect(
-				:query_one, { c: 0 }, [String, "1"], default: { c: 0 }
-			)
-			@customer.expect(:with_plan, @customer) do |*args, **kwargs|
-				assert_equal ["test_usd"], args
-				assert_equal({ parent_customer_id: "1" }, kwargs)
+			execute_command do
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+						iq.form.fields = [
+							{ var: "plan_name", value: "test_usd" },
+							{ var: "code", value: "PARENT_CODE" }
+						]
+					}),
+					[Matching.new do |iq|
+						assert_equal :form, iq.form.type
+						assert_equal(
+							"You've selected (555) 555-0000 as your JMP number.",
+							iq.form.instructions.lines.first.chomp
+						)
+					end]
+				)
+				Registration::Activation::DB.expect(
+					:query_one, {}, [String, "1"], default: {}
+				)
+				Registration::Activation::DB.expect(
+					:query_one, { c: 0 }, [String, "1"], default: { c: 0 }
+				)
+				@customer.expect(:with_plan, @customer) do |*args, **|
+					assert_equal ["test_usd"], args
+				end
+				@customer.expect(:with_plan, @customer) do |*, **kwargs|
+					assert_equal({ parent_customer_id: "1" }, kwargs)
+				end
+				@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
+				@customer.expect(:balance, 100, [])
+				Command.execution.customer_repo.expect(
+					:find,
+					EMPromise.resolve(@customer), ["test"]
+				)
+				Registration::Payment::MaybeBill::BillPlan.expect(
+					:new,
+					EMPromise.reject(:test_result)
+				) do |*args, **|
+					assert_equal @tel, args[1]
+				end
+				assert_equal(
+					:test_result,
+					@activation.write.catch { |e| e }.sync
+				)
 			end
-			@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
-			Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call }
-			Registration::Activation::DB.expect(
-				:exec,
-				OpenStruct.new(cmd_tuples: 0),
-				[String, ["test", "PARENT_CODE"]]
-			)
-			Registration::Activation::Payment.expect(
-				:for,
-				EMPromise.reject(:test_result),
-				[Blather::Stanza::Iq, @customer, "+15555550000"]
-			)
-			assert_equal(
-				:test_result,
-				execute_command { @activation.write.catch { |e| e } }
-			)
 			assert_mock Command::COMMAND_MANAGER
 			assert_mock @customer
 			assert_mock Registration::Activation::Payment
 			assert_mock Registration::Activation::DB
+			assert_mock Registration::Payment::MaybeBill::BillPlan
 		end
 		em :test_write_with_parent_code
 
@@ -369,14 +412,24 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
 			)
-			@customer.expect(:jid, Blather::JID.new("test@onboarding.example.com"))
+			Registration::Activation::DB.expect(
+				:query_one, {}, [String, "1"], default: {}
+			)
+			Registration::Activation::DB.expect(
+				:query_one, { c: 0 }, [String, "1"], default: { c: 0 }
+			)
+			@customer.expect(:with_plan, @customer, ["test_usd"])
+			@customer.expect(
+				:jid,
+				Blather::JID.new("test\\40onboarding.example.com@proxy")
+			)
 			iq = Blather::Stanza::Iq::Command.new
-			iq.from = "test@onboarding.example.com"
+			iq.from = "test\\40onboarding.example.com@proxied"
 			assert_equal(
 				"Please create a new Jabber ID before creating a subaccount.",
 				execute_command(iq) { @activation.write.catch(&:to_s) }
@@ -400,7 +453,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
@@ -434,7 +487,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
@@ -462,8 +515,9 @@ class RegistrationTest < Minitest::Test
 		Registration::Activation::Allow::DB = Minitest::Mock.new
 
 		def test_write_credit_to_nil
+			tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 			cust = Minitest::Mock.new(customer("test"))
-			allow = Registration::Activation::Allow.new(cust, "+15555550000", nil)
+			allow = Registration::Activation::Allow.new(cust, tel, nil)
 
 			Command::COMMAND_MANAGER.expect(
 				:write,
@@ -473,7 +527,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 					assert_equal 1, iq.form.fields.length
@@ -498,8 +552,9 @@ class RegistrationTest < Minitest::Test
 
 		def test_write_credit_to_refercust
 			cust = Minitest::Mock.new(customer("test"))
+			tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 			allow = Registration::Activation::Allow.new(
-				cust, "+15555550000", "refercust"
+				cust, tel, "refercust"
 			)
 
 			Command::COMMAND_MANAGER.expect(
@@ -510,7 +565,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 					assert_equal 1, iq.form.fields.length
@@ -545,10 +600,11 @@ class RegistrationTest < Minitest::Test
 
 		def setup
 			@customer = customer
+			@tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 			@google_play = Registration::Activation::GooglePlay.new(
 				@customer,
 				"abcd",
-				"+15555550000"
+				@tel
 			)
 		end
 
@@ -561,7 +617,7 @@ class RegistrationTest < Minitest::Test
 				[Matching.new do |iq|
 					assert_equal :form, iq.form.type
 					assert_equal(
-						"You've selected +15555550000 as your JMP number.",
+						"You've selected (555) 555-0000 as your JMP number.",
 						iq.form.instructions.lines.first.chomp
 					)
 				end]
@@ -580,7 +636,7 @@ class RegistrationTest < Minitest::Test
 			Registration::Activation::GooglePlay::Finish.expect(
 				:new,
 				OpenStruct.new(write: EMPromise.reject(:test_result)),
-				[Customer, "+15555550000"]
+				[Customer, @tel]
 			)
 			result = execute_command { @google_play.write.catch { |e| e } }
 			assert_equal :test_result, result
@@ -596,37 +652,29 @@ class RegistrationTest < Minitest::Test
 		CustomerFinancials::BRAINTREE = Minitest::Mock.new
 
 		def test_for_bitcoin
+			tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 			iq = Blather::Stanza::Iq::Command.new
 			iq.form.fields = [
 				{ var: "activation_method", value: "bitcoin" },
 				{ var: "plan_name", value: "test_usd" }
 			]
-			result = Registration::Payment.for(iq, customer, "+15555550000")
+			result = Registration::Payment.for(iq, customer, tel)
 			assert_kind_of Registration::Payment::Bitcoin, result
 		end
 
 		def test_for_bch
+			tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 			iq = Blather::Stanza::Iq::Command.new
 			iq.form.fields = [
 				{ var: "activation_method", value: "bch" },
 				{ var: "plan_name", value: "test_usd" }
 			]
-			result = Registration::Payment.for(iq, customer, "+15555550000")
+			result = Registration::Payment.for(iq, customer, tel)
 			assert_kind_of Registration::Payment::BCH, result
 		end
 
 		def test_for_credit_card
-			braintree_customer = Minitest::Mock.new
-			CustomerFinancials::BRAINTREE.expect(
-				:customer,
-				braintree_customer
-			)
-			CustomerFinancials::REDIS.expect(:smembers, [], ["block_credit_cards"])
-			braintree_customer.expect(
-				:find,
-				EMPromise.resolve(OpenStruct.new(payment_methods: [])),
-				["test"]
-			)
+			tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 			iq = Blather::Stanza::Iq::Command.new
 			iq.from = "test@example.com"
 			iq.form.fields = [
@@ -639,8 +687,8 @@ class RegistrationTest < Minitest::Test
 				Registration::Payment.for(
 					iq,
 					cust,
-					""
-				).sync
+					tel
+				)
 			end
 			assert_kind_of Registration::Payment::CreditCard, result
 		end
@@ -658,7 +706,7 @@ class RegistrationTest < Minitest::Test
 				Registration::Payment.for(
 					iq,
 					cust,
-					"+15555550000"
+					TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 				)
 			end
 			assert_kind_of Registration::Payment::InviteCode, result
@@ -677,9 +725,10 @@ class RegistrationTest < Minitest::Test
 					:add_btc_address,
 					EMPromise.resolve("testaddr")
 				)
+				@tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 				@bitcoin = Registration::Payment::Bitcoin.new(
 					@customer,
-					"+15555550000"
+					@tel
 				)
 			end
 
@@ -726,9 +775,10 @@ class RegistrationTest < Minitest::Test
 					:add_bch_address,
 					EMPromise.resolve("testaddr")
 				)
+				@tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 				@bch = Registration::Payment::BCH.new(
 					@customer,
-					"+15555550000"
+					@tel
 				)
 			end
 
@@ -765,13 +815,15 @@ class RegistrationTest < Minitest::Test
 
 		class CreditCardTest < Minitest::Test
 			def setup
+				@tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 				@credit_card = Registration::Payment::CreditCard.new(
 					customer,
-					"+15555550000"
+					@tel
 				)
 			end
 
-			def test_for
+			def test_new
+				tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 				cust = Minitest::Mock.new(customer)
 				cust.expect(
 					:payment_methods,
@@ -780,32 +832,12 @@ class RegistrationTest < Minitest::Test
 				execute_command do
 					Command.execution.customer_repo.expect(:find, cust, ["test"])
 					assert_kind_of(
-						Registration::Payment::CreditCard::Activate,
-						Registration::Payment::CreditCard.for(
-							cust,
-							"+15555550000"
-						).sync
+						Registration::Payment::CreditCard,
+						Registration::Payment::CreditCard.new(cust, tel)
 					)
 				end
 			end
-			em :test_for
-
-			def test_for_has_balance
-				cust = Minitest::Mock.new(customer)
-				cust.expect(:balance, 100)
-				cust.expect(:payment_methods, EMPromise.resolve(nil))
-				execute_command do
-					Command.execution.customer_repo.expect(:find, cust, ["test"])
-					assert_kind_of(
-						Registration::BillPlan,
-						Registration::Payment::CreditCard.for(
-							cust,
-							"+15555550000"
-						).sync
-					)
-				end
-			end
-			em :test_for_has_balance
+			em :test_new
 
 			def test_write
 				result = execute_command do
@@ -815,7 +847,7 @@ class RegistrationTest < Minitest::Test
 						[Matching.new do |reply|
 							assert_equal [:execute, :next, :prev], reply.allowed_actions
 							assert_equal(
-								"Add credit card, save, then next here to continue: " \
+								"Pay by credit card, save, then next here to continue: " \
 								"http://creditcard.example.com?&amount=1",
 								reply.note.content
 							)
@@ -832,9 +864,10 @@ class RegistrationTest < Minitest::Test
 
 		class MailTest < Minitest::Test
 			def setup
+				@tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
 				@mail = Registration::Payment::Mail.new(
 					customer(plan_name: "test_cad"),
-					"+15555550000"
+					@tel
 				)
 			end
 
@@ -865,88 +898,6 @@ class RegistrationTest < Minitest::Test
 			em :test_write
 		end
 
-		class ActivateTest < Minitest::Test
-			Registration::Payment::CreditCard::Activate::Finish =
-				Minitest::Mock.new
-			Registration::Payment::CreditCard::Activate::CreditCardSale =
-				Minitest::Mock.new
-			Command::COMMAND_MANAGER = Minitest::Mock.new
-
-			def test_write
-				customer = Minitest::Mock.new(
-					customer(plan_name: "test_usd")
-				)
-				Registration::Payment::CreditCard::Activate::CreditCardSale.expect(
-					:create,
-					EMPromise.resolve(nil)
-				) do |acustomer, amount:, payment_method:|
-					assert_operator customer, :===, acustomer
-					assert_equal CONFIG[:activation_amount], amount
-					assert_equal :test_default_method, payment_method
-				end
-				customer.expect(
-					:bill_plan,
-					nil,
-					note: "Bill +15555550000 for first month"
-				)
-				Registration::Payment::CreditCard::Activate::Finish.expect(
-					:new,
-					OpenStruct.new(write: nil),
-					[customer, "+15555550000"]
-				)
-				execute_command do
-					Registration::Payment::CreditCard::Activate.new(
-						customer,
-						:test_default_method,
-						"+15555550000"
-					).write
-				end
-				Registration::Payment::CreditCard::Activate::CreditCardSale.verify
-				customer.verify
-				Registration::Payment::CreditCard::Activate::Finish.verify
-			end
-			em :test_write
-
-			def test_write_declines
-				customer = Minitest::Mock.new(
-					customer(plan_name: "test_usd")
-				)
-				iq = Blather::Stanza::Iq::Command.new
-				iq.from = "test@example.com"
-				msg = Registration::Payment::CreditCard::Activate::DECLINE_MESSAGE
-				Command::COMMAND_MANAGER.expect(
-					:write,
-					EMPromise.reject(:test_result),
-					[Matching.new do |reply|
-						assert_equal :error, reply.note_type
-						assert_equal(
-							"#{msg}: http://creditcard.example.com?&amount=1",
-							reply.note.content
-						)
-					end]
-				)
-				result = execute_command do
-					Registration::Payment::CreditCard::Activate::CreditCardSale.expect(
-						:create,
-						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::CreditCardSale.verify
-			end
-			em :test_write_declines
-		end
-
 		class InviteCodeTest < Minitest::Test
 			Registration::Payment::InviteCode::DB =
 				Minitest::Mock.new
@@ -955,9 +906,13 @@ class RegistrationTest < Minitest::Test
 			Command::COMMAND_MANAGER = Minitest::Mock.new
 			Registration::Payment::InviteCode::Finish =
 				Minitest::Mock.new
-			Registration::Payment::InviteCode::BillPlan =
+			Registration::Payment::MaybeBill::BillPlan =
 				Minitest::Mock.new
 
+			def setup
+				@tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
+			end
+
 			def test_write
 				customer = customer(plan_name: "test_usd")
 				Registration::Payment::InviteCode::DB.expect(:transaction, true, [])
@@ -966,7 +921,7 @@ class RegistrationTest < Minitest::Test
 					OpenStruct.new(write: nil),
 					[
 						customer,
-						"+15555550000"
+						@tel
 					]
 				)
 				execute_command do
@@ -995,7 +950,7 @@ class RegistrationTest < Minitest::Test
 
 					Registration::Payment::InviteCode.new(
 						customer,
-						"+15555550000"
+						@tel
 					).write
 				end
 				assert_mock Command::COMMAND_MANAGER
@@ -1007,7 +962,7 @@ class RegistrationTest < Minitest::Test
 
 			def test_write_parent_code
 				customer = customer(plan_name: "test_usd")
-				Registration::Payment::InviteCode::BillPlan.expect(
+				Registration::Payment::MaybeBill::BillPlan.expect(
 					:new,
 					OpenStruct.new(write: nil)
 				) { |*| true }
@@ -1058,19 +1013,24 @@ class RegistrationTest < Minitest::Test
 
 					Registration::Payment::InviteCode.new(
 						customer,
-						"+15555550000"
+						@tel
 					).write
 				end
 				assert_mock Command::COMMAND_MANAGER
 				assert_mock Registration::Payment::InviteCode::DB
 				assert_mock Registration::Payment::InviteCode::REDIS
-				assert_mock Registration::Payment::InviteCode::BillPlan
+				assert_mock Registration::Payment::MaybeBill::BillPlan
 			end
 			em :test_write_parent_code
 
 			def test_write_bad_code
 				result = execute_command do
 					customer = customer(plan_name: "test_usd")
+					Registration::Payment::InviteCode::REDIS.expect(
+						:set,
+						EMPromise.resolve(nil),
+						["jmp_customer_pending_invite-test", "abc"]
+					)
 					Registration::Payment::InviteCode::REDIS.expect(
 						:get,
 						EMPromise.resolve(0),
@@ -1131,7 +1091,7 @@ class RegistrationTest < Minitest::Test
 
 					Registration::Payment::InviteCode.new(
 						customer,
-						"+15555550000"
+						@tel
 					).write.catch { |e| e }
 				end
 				assert_equal :test_result, result
@@ -1144,6 +1104,11 @@ class RegistrationTest < Minitest::Test
 			def test_write_group_code
 				result = execute_command do
 					customer = customer(plan_name: "test_usd")
+					Registration::Payment::InviteCode::REDIS.expect(
+						:set,
+						EMPromise.resolve(nil),
+						["jmp_customer_pending_invite-test", "abc"]
+					)
 					Registration::Payment::InviteCode::REDIS.expect(
 						:get,
 						EMPromise.resolve(0),
@@ -1204,7 +1169,7 @@ class RegistrationTest < Minitest::Test
 
 					Registration::Payment::InviteCode.new(
 						customer,
-						"+15555550000"
+						@tel
 					).write.catch { |e| e }
 				end
 				assert_equal :test_result, result
@@ -1249,7 +1214,7 @@ class RegistrationTest < Minitest::Test
 					)
 					Registration::Payment::InviteCode.new(
 						customer,
-						"+15555550000"
+						@tel
 					).write.catch { |e| e }
 				end
 				assert_equal :test_result, result
@@ -1697,6 +1662,136 @@ class RegistrationTest < Minitest::Test
 		end
 		em :test_write_local_inventory
 
+		def test_write_local_inventory_must_pay
+			low_cust = customer(
+				sgx: @sgx,
+				jid: Blather::JID.new("test\\40onboarding.example.com@proxy")
+			).with_balance(5.0)
+			high_cust = customer(
+				sgx: @sgx,
+				jid: Blather::JID.new("test\\40onboarding.example.com@proxy")
+			).with_balance(100.0)
+
+			stub_request(
+				:post,
+				"https://dashboard.bandwidth.com/v1.0/accounts/moveto/moveTns"
+			).with(
+				body: {
+					CustomerOrderId: "test",
+					SourceAccountId: "bandwidth_account_id",
+					SiteId: "test_site",
+					SipPeerId: "test_peer",
+					TelephoneNumbers: { TelephoneNumber: "5555550000" }
+				}.to_xml(indent: 0, root: "MoveTnsOrder")
+			).to_return(status: 200, body: "", headers: {})
+
+			Registration::Finish::REDIS.expect(
+				:get,
+				nil,
+				["jmp_customer_pending_invite-test"]
+			)
+			Registration::Finish::REDIS.expect(
+				:del,
+				nil,
+				["jmp_customer_pending_invite-test"]
+			)
+			Registration::Finish::REDIS.expect(
+				:hget,
+				nil,
+				["jmp_group_codes", nil]
+			)
+			Registration::Finish::DB.expect(
+				:exec_defer,
+				EMPromise.resolve(OpenStruct.new(cmd_tuples: 1)),
+				[String, ["+15555550000"]]
+			)
+			Bwmsgsv2Repo::REDIS.expect(
+				:get,
+				EMPromise.resolve(nil),
+				["jmp_customer_backend_sgx-test"]
+			)
+			Bwmsgsv2Repo::REDIS.expect(
+				:set,
+				nil,
+				[
+					"catapult_fwd-+15555550000",
+					"xmpp:test\\40onboarding.example.com@proxy"
+				]
+			)
+			Bwmsgsv2Repo::REDIS.expect(
+				:set,
+				nil,
+				["catapult_fwd_timeout-customer_test@component", 25]
+			)
+
+			local_tel = Minitest::Mock.new(
+				TelSelections::ChooseTel::Tn::LocalInventory.new(
+					TelSelections::ChooseTel::Tn.new("+15555550000"),
+					"bandwidth_account_id",
+					price: 10.0
+				)
+			)
+
+			result = execute_command do
+				local_tel.expect(:charge, EMPromise.reject(:test_result), [Customer])
+				@sgx.expect(
+					:register!,
+					EMPromise.resolve(@sgx.with(
+						registered?: Blather::Stanza::Iq::IBR.new.tap do |ibr|
+							ibr.phone = "+15555550000"
+						end
+					)),
+					["+15555550000"]
+				)
+
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+						iq.from = "customer@example.org"
+						iq.form.fields = [
+							{ var: "activation_method", value: "credit_card" }
+						]
+					}),
+					[Matching.new do |iq|
+						assert_equal :form, iq.form.type
+						assert_equal "Purchase Number", iq.form.title
+					end]
+				)
+
+				Command.execution.customer_repo.expect(
+					:find,
+					EMPromise.resolve(high_cust),
+					["test"]
+				)
+
+				Command::COMMAND_MANAGER.expect(
+					:write,
+					EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+						iq.from = "customer@example.org"
+					}),
+					[Matching.new do |iq|
+						assert_equal(
+							"Pay by credit card, save, then next here to continue: " \
+							"http://creditcard.example.com?&amount=10",
+							iq.note.text
+						)
+					end]
+				)
+
+				Registration::Finish.new(
+					low_cust,
+					local_tel
+				).write.catch { |e| e }
+			end
+
+			assert_equal :test_result, result
+			assert_mock @sgx
+			assert_mock Registration::Finish::REDIS
+			assert_mock Bwmsgsv2Repo::REDIS
+			assert_mock Command::COMMAND_MANAGER
+		end
+		em :test_write_local_inventory_must_pay
+
 		def test_write_tn_fail
 			create_order = stub_request(
 				:post,