~singpolyma/sgx-jmp

8e4c1cc7fe1a5da1fba9ba0dd650b157a5e51c17 — Stephen Paul Weber 1 year, 6 months ago 7f1efc3
Happy path for credit card signup

Assuming credit card payment works, you can now activate a new account by paying
with one.
5 files changed, 240 insertions(+), 27 deletions(-)

M config.dhall.sample
M lib/registration.rb
M sgx_jmp.rb
M test/test_helper.rb
M test/test_registration.rb
M config.dhall.sample => config.dhall.sample +8 -4
@@ 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}"
}

M lib/registration.rb => lib/registration.rb +44 -5
@@ 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

M sgx_jmp.rb => sgx_jmp.rb +16 -5
@@ 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

M test/test_helper.rb => test/test_helper.rb +11 -1
@@ 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

M test/test_registration.rb => test/test_registration.rb +161 -12
@@ 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