Happy path for credit card signup

Stephen Paul Weber created

Assuming credit card payment works, you can now activate a new account by paying
with one.

Change summary

config.dhall.sample       |  12 +
lib/registration.rb       |  49 ++++++++++-
sgx_jmp.rb                |  21 +++-
test/test_helper.rb       |  12 ++
test/test_registration.rb | 173 ++++++++++++++++++++++++++++++++++++++--
5 files changed, 240 insertions(+), 27 deletions(-)

Detailed changes

config.dhall.sample 🔗

@@ -8,11 +8,12 @@
 		port = 5347
 	},
 	sgx = "component2.localhost",
-	creds = toMap {
-		nick = "userid",
-		username = "token",
-		password = "secret"
+	creds = {
+		account = "00000",
+		username = "dashboard user",
+		password = "dashboard password"
 	},
+	bandwidth_site = "",
 	braintree = {
 		environment = "sandbox",
 		merchant_id = "",
@@ -24,6 +25,9 @@
 		}
 	},
 	plans = ./plans.dhall
+	electrum = ./electrum.dhall,
+	oxr_app_id = "",
+	activation_amount = 15,
 	credit_card_url = \(jid: Text) -> \(customer_id: Text) ->
 		"https://pay.jmp.chat/${jid}/credit_cards?customer_id=${customer_id}"
 }

lib/registration.rb 🔗

@@ -65,7 +65,7 @@ class Registration
 					},
 					{
 						value: "credit_card",
-						label: "Credit Card"
+						label: "Credit Card ($#{CONFIG[:activation_amount]})"
 					},
 					{
 						value: "code",
@@ -173,8 +173,8 @@ class Registration
 
 			def self.for(iq, customer, tel)
 				customer.payment_methods.then do |payment_methods|
-					if payment_methods.default_payment_method
-						Activate.new(iq, customer, tel)
+					if (method = payment_methods.default_payment_method)
+						Activate.new(iq, customer, method, tel)
 					else
 						new(iq, customer, tel)
 					end
@@ -210,10 +210,49 @@ class Registration
 			end
 
 			class Activate
-				def initialize(_iq, _customer, _tel)
-					raise "TODO"
+				def initialize(iq, customer, payment_method, tel)
+					@iq = iq
+					@customer = customer
+					@payment_method = payment_method
+					@tel = tel
+				end
+
+				def write
+					Transaction.sale(
+						@customer.merchant_account,
+						@payment_method,
+						CONFIG[:activation_amount]
+					).then(&:insert).then {
+						@customer.bill_plan
+					}.then do
+						Finish.new(@iq, @customer, @tel).write
+					end
 				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}"
+			@customer = customer
+			@tel = tel
+		end
+
+		def write
+			BandwidthTNOrder.create(@tel).then(&:poll).then(
+				->(_) { @customer.register!(@tel).then { BLATHER << @reply } },
+				lambda do |_|
+					@reply.note_type = :error
+					@reply.note_text =
+						"The JMP number #{@tel} is no longer available, " \
+						"please visit https://jmp.chat and choose another."
+					BLATHER << @reply
+				end
+			)
+		end
+	end
 end

sgx_jmp.rb 🔗

@@ -8,12 +8,20 @@ require "braintree"
 require "dhall"
 require "em-hiredis"
 require "em_promise"
+require "ruby-bandwidth-iris"
+
+CONFIG =
+	Dhall::Coder
+	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
+	.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
 
 singleton_class.class_eval do
 	include Blather::DSL
 	Blather::DSL.append_features(self)
 end
 
+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/customer"
@@ -24,13 +32,15 @@ require_relative "lib/registration"
 require_relative "lib/transaction"
 require_relative "lib/web_register_manager"
 
-CONFIG =
-	Dhall::Coder
-	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
-	.load(ARGV[0], transform_keys: ->(k) { k&.to_sym })
-
 ELECTRUM = Electrum.new(**CONFIG[:electrum])
 
+Faraday.default_adapter = :em_synchrony
+BandwidthIris::Client.global_options = {
+	account_id: CONFIG[:creds][:account],
+	username: CONFIG[:creds][:username],
+	password: CONFIG[:creds][:password]
+}
+
 # Braintree is not async, so wrap in EM.defer for now
 class AsyncBraintree
 	def initialize(environment:, merchant_id:, public_key:, private_key:, **)
@@ -67,6 +77,7 @@ class AsyncBraintree
 end
 
 BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
+BACKEND_SGX = BackendSgx.new
 
 def panic(e)
 	m = e.respond_to?(:message) ? e.message : e

test/test_helper.rb 🔗

@@ -32,16 +32,24 @@ rescue LoadError
 	nil
 end
 
+require "backend_sgx"
+
 CONFIG = {
 	sgx: "sgx",
 	component: {
 		jid: "component"
 	},
+	creds: {
+		account: "test_bw_account",
+		username: "test_bw_user",
+		password: "test_bw_password"
+	},
 	activation_amount: 1,
 	plans: [
 		{
 			name: "test_usd",
-			currency: :USD
+			currency: :USD,
+			monthly_price: 1000
 		},
 		{
 			name: "test_bad_currency",
@@ -56,6 +64,8 @@ CONFIG = {
 	credit_card_url: ->(*) { "http://creditcard.example.com" }
 }.freeze
 
+BACKEND_SGX = Minitest::Mock.new(BackendSgx.new)
+
 BLATHER = Class.new {
 	def <<(*); end
 }.new.freeze

test/test_registration.rb 🔗

@@ -4,8 +4,6 @@ require "test_helper"
 require "registration"
 
 class RegistrationTest < Minitest::Test
-	Customer::IQ_MANAGER = Minitest::Mock.new
-
 	def test_for_activated
 		skip "Registration#for activated not implemented yet"
 		iq = Blather::Stanza::Iq::Command.new
@@ -14,10 +12,10 @@ class RegistrationTest < Minitest::Test
 	em :test_for_activated
 
 	def test_for_not_activated_with_customer_id
-		Customer::IQ_MANAGER.expect(
-			:write,
+		BACKEND_SGX.expect(
+			:registered?,
 			EMPromise.resolve(nil),
-			[Blather::Stanza::Iq]
+			["test"]
 		)
 		web_manager = WebRegisterManager.new
 		web_manager["test@example.com"] = "+15555550000"
@@ -128,12 +126,9 @@ class RegistrationTest < Minitest::Test
 					EMPromise.resolve("testaddr")
 				)
 				iq = Blather::Stanza::Iq::Command.new
-				iq.form.fields = [
-					{ var: "plan_name", value: "test_usd" }
-				]
 				@bitcoin = Registration::Payment::Bitcoin.new(
 					iq,
-					Customer.new("test"),
+					Customer.new("test", plan_name: "test_usd"),
 					"+15555550000"
 				)
 			end
@@ -169,15 +164,32 @@ class RegistrationTest < Minitest::Test
 
 		class CreditCardTest < Minitest::Test
 			def setup
-				iq = Blather::Stanza::Iq::Command.new
-				iq.from = "test@example.com"
+				@iq = Blather::Stanza::Iq::Command.new
+				@iq.from = "test@example.com"
 				@credit_card = Registration::Payment::CreditCard.new(
-					iq,
+					@iq,
 					Customer.new("test"),
 					"+15555550000"
 				)
 			end
 
+			def test_for
+				customer = Minitest::Mock.new(Customer.new("test"))
+				customer.expect(
+					:payment_methods,
+					EMPromise.resolve(OpenStruct.new(default_payment_method: :test))
+				)
+				assert_kind_of(
+					Registration::Payment::CreditCard::Activate,
+					Registration::Payment::CreditCard.for(
+						@iq,
+						customer,
+						"+15555550000"
+					).sync
+				)
+			end
+			em :test_for
+
 			def test_reply
 				assert_equal [:execute, :next], @credit_card.reply.allowed_actions
 				assert_equal(
@@ -187,5 +199,142 @@ class RegistrationTest < Minitest::Test
 				)
 			end
 		end
+
+		class ActivateTest < Minitest::Test
+			Registration::Payment::CreditCard::Activate::Finish =
+				Minitest::Mock.new
+			Registration::Payment::CreditCard::Activate::Transaction =
+				Minitest::Mock.new
+
+			def test_write
+				transaction = PromiseMock.new
+				transaction.expect(
+					:insert,
+					EMPromise.resolve(nil)
+				)
+				Registration::Payment::CreditCard::Activate::Transaction.expect(
+					:sale,
+					transaction,
+					[
+						"merchant_usd",
+						:test_default_method,
+						CONFIG[:activation_amount]
+					]
+				)
+				iq = Blather::Stanza::Iq::Command.new
+				customer = Minitest::Mock.new(
+					Customer.new("test", plan_name: "test_usd")
+				)
+				customer.expect(:bill_plan, nil)
+				Registration::Payment::CreditCard::Activate::Finish.expect(
+					:new,
+					OpenStruct.new(write: nil),
+					[Blather::Stanza::Iq, customer, "+15555550000"]
+				)
+				Registration::Payment::CreditCard::Activate.new(
+					iq,
+					customer,
+					:test_default_method,
+					"+15555550000"
+				).write.sync
+				Registration::Payment::CreditCard::Activate::Transaction.verify
+				transaction.verify
+				customer.verify
+			end
+			em :test_write
+		end
+	end
+
+	class FinishTest < Minitest::Test
+		Registration::Finish::BLATHER = Minitest::Mock.new
+
+		def setup
+			@finish = Registration::Finish.new(
+				Blather::Stanza::Iq::Command.new,
+				Customer.new("test"),
+				"+15555550000"
+			)
+		end
+
+		def test_write
+			create_order = stub_request(
+				:post,
+				"https://dashboard.bandwidth.com/v1.0/accounts//orders"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<OrderResponse>
+					<Order>
+						<id>test_order</id>
+					</Order>
+				</OrderResponse>
+			RESPONSE
+			stub_request(
+				:get,
+				"https://dashboard.bandwidth.com/v1.0/accounts//orders/test_order"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<OrderResponse>
+					<OrderStatus>COMPLETE</OrderStatus>
+				</OrderResponse>
+			RESPONSE
+			BACKEND_SGX.expect(
+				:register!,
+				EMPromise.resolve(OpenStruct.new(error?: false)),
+				["test", "+15555550000"]
+			)
+			Registration::Finish::BLATHER.expect(
+				:<<,
+				nil,
+				[Matching.new do |reply|
+					assert_equal :completed, reply.status
+					assert_equal :info, reply.note_type
+					assert_equal(
+						"Your JMP account has been activated as +15555550000",
+						reply.note.content
+					)
+				end]
+			)
+			@finish.write.sync
+			assert_requested create_order
+			BACKEND_SGX.verify
+			Registration::Finish::BLATHER.verify
+		end
+		em :test_write
+
+		def test_write_tn_fail
+			create_order = stub_request(
+				:post,
+				"https://dashboard.bandwidth.com/v1.0/accounts//orders"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<OrderResponse>
+					<Order>
+						<id>test_order</id>
+					</Order>
+				</OrderResponse>
+			RESPONSE
+			stub_request(
+				:get,
+				"https://dashboard.bandwidth.com/v1.0/accounts//orders/test_order"
+			).to_return(status: 201, body: <<~RESPONSE)
+				<OrderResponse>
+					<OrderStatus>FAILED</OrderStatus>
+				</OrderResponse>
+			RESPONSE
+			Registration::Finish::BLATHER.expect(
+				:<<,
+				nil,
+				[Matching.new do |reply|
+					assert_equal :completed, reply.status
+					assert_equal :error, reply.note_type
+					assert_equal(
+						"The JMP number +15555550000 is no longer available, " \
+						"please visit https://jmp.chat and choose another.",
+						reply.note.content
+					)
+				end]
+			)
+			@finish.write.sync
+			assert_requested create_order
+			Registration::Finish::BLATHER.verify
+		end
+		em :test_write_tn_fail
 	end
 end