Merge branch 'create_customer_id'

Stephen Paul Weber created

* create_customer_id:
  Create customer_id if it does not exist before we start registration
  Break out CustomerPlan
  Inject BackendSgx per customer

Change summary

lib/backend_sgx.rb        | 19 +++++---
lib/customer.rb           | 93 ++++++++++++----------------------------
lib/customer_plan.rb      | 69 ++++++++++++++++++++++++++++++
lib/registration.rb       |  9 +--
sgx_jmp.rb                |  3 
test/test_backend_sgx.rb  |  8 +-
test/test_customer.rb     | 40 +++++++++++++---
test/test_helper.rb       |  2 
test/test_registration.rb | 49 +++++++++------------
9 files changed, 169 insertions(+), 123 deletions(-)

Detailed changes

lib/backend_sgx.rb 🔗

@@ -1,13 +1,14 @@
 # frozen_string_literal: true
 
 class BackendSgx
-	def initialize(jid=CONFIG[:sgx], creds=CONFIG[:creds])
+	def initialize(customer_id, jid=CONFIG[:sgx], creds=CONFIG[:creds])
+		@customer_id = customer_id
 		@jid = jid
 		@creds = creds
 	end
 
-	def register!(customer_id, tel)
-		ibr = mkibr(:set, customer_id)
+	def register!(tel)
+		ibr = mkibr(:set)
 		ibr.nick = @creds[:account]
 		ibr.username = @creds[:username]
 		ibr.password = @creds[:password]
@@ -15,8 +16,8 @@ class BackendSgx
 		IQ_MANAGER.write(ibr)
 	end
 
-	def registered?(customer_id)
-		IQ_MANAGER.write(mkibr(:get, customer_id)).catch { nil }.then do |result|
+	def registered?
+		IQ_MANAGER.write(mkibr(:get)).catch { nil }.then do |result|
 			if result&.respond_to?(:registered?) && result&.registered?
 				result
 			else
@@ -27,9 +28,13 @@ class BackendSgx
 
 protected
 
-	def mkibr(type, customer_id)
+	def from_jid
+		"customer_#{@customer_id}@#{CONFIG[:component][:jid]}"
+	end
+
+	def mkibr(type)
 		ibr = IBR.new(type, @jid)
-		ibr.from = "customer_#{customer_id}@#{CONFIG[:component][:jid]}"
+		ibr.from = from_jid
 		ibr
 	end
 end

lib/customer.rb 🔗

@@ -2,6 +2,8 @@
 
 require "forwardable"
 
+require_relative "./customer_plan"
+require_relative "./backend_sgx"
 require_relative "./ibr"
 require_relative "./payment_methods"
 require_relative "./plan"
@@ -25,51 +27,52 @@ class Customer
 		end
 	end
 
+	def self.create(jid)
+		BRAINTREE.customer.create.then do |result|
+			raise "Braintree customer create failed" unless result.success?
+			cid = result.customer.id
+			REDIS.msetnx(
+				"jmp_customer_id-#{jid}", cid, "jmp_customer_jid-#{cid}", jid
+			).then do |redis_result|
+				raise "Saving new customer to redis failed" unless redis_result == 1
+				new(cid)
+			end
+		end
+	end
+
 	extend Forwardable
 
 	attr_reader :customer_id, :balance
-	def_delegator :@plan, :name, :plan_name
-	def_delegators :@plan, :currency, :merchant_account
+	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
+	               :currency, :merchant_account, :plan_name
+	def_delegators :@sgx, :register!, :registered?
 
 	def initialize(
 		customer_id,
 		plan_name: nil,
 		expires_at: Time.now,
-		balance: BigDecimal.new(0)
+		balance: BigDecimal.new(0),
+		sgx: BackendSgx.new(customer_id)
 	)
-		@plan = plan_name && Plan.for(plan_name)
-		@expires_at = expires_at
+		@plan = CustomerPlan.new(
+			customer_id,
+			plan: plan_name && Plan.for(plan_name),
+			expires_at: expires_at
+		)
 		@customer_id = customer_id
 		@balance = balance
+		@sgx = sgx
 	end
 
 	def with_plan(plan_name)
 		self.class.new(
 			@customer_id,
 			balance: @balance,
-			expires_at: @expires_at,
+			expires_at: expires_at,
 			plan_name: plan_name
 		)
 	end
 
-	def bill_plan
-		EM.promise_fiber do
-			DB.transaction do
-				charge_for_plan
-				add_one_month_to_current_plan unless activate_plan_starting_now
-			end
-		end
-	end
-
-	def activate_plan_starting_now
-		DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive?
-			INSERT INTO plan_log
-				(customer_id, plan_name, date_range)
-			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
-			ON CONFLICT DO NOTHING
-		SQL
-	end
-
 	def payment_methods
 		@payment_methods ||=
 			BRAINTREE
@@ -78,45 +81,5 @@ class Customer
 			.then(PaymentMethods.method(:for_braintree_customer))
 	end
 
-	def active?
-		@plan && @expires_at > Time.now
-	end
-
-	def register!(tel)
-		BACKEND_SGX.register!(customer_id, tel)
-	end
-
-	def registered?
-		BACKEND_SGX.registered?(customer_id)
-	end
-
-protected
-
-	def charge_for_plan
-		params = [
-			@customer_id,
-			"#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
-			-@plan.monthly_price
-		]
-		DB.exec(<<~SQL, params)
-			INSERT INTO transactions
-				(customer_id, transaction_id, created_at, amount)
-			VALUES ($1, $2, LOCALTIMESTAMP, $3)
-		SQL
-	end
-
-	def add_one_month_to_current_plan
-		DB.exec(<<~SQL, [@customer_id])
-			UPDATE plan_log SET date_range=range_merge(
-				date_range,
-				tsrange(
-					LOCALTIMESTAMP,
-					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
-				)
-			)
-			WHERE
-				customer_id=$1 AND
-				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
-		SQL
-	end
+	protected def_delegator :@plan, :expires_at
 end

lib/customer_plan.rb 🔗

@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "forwardable"
+
+class CustomerPlan
+	extend Forwardable
+
+	attr_reader :expires_at
+	def_delegator :@plan, :name, :plan_name
+	def_delegators :@plan, :currency, :merchant_account
+
+	def initialize(customer_id, plan: nil, expires_at: Time.now)
+		@customer_id = customer_id
+		@plan = plan
+		@expires_at = expires_at
+	end
+
+	def active?
+		@plan && @expires_at > Time.now
+	end
+
+	def bill_plan
+		EM.promise_fiber do
+			DB.transaction do
+				charge_for_plan
+				add_one_month_to_current_plan unless activate_plan_starting_now
+			end
+		end
+	end
+
+	def activate_plan_starting_now
+		DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive?
+			INSERT INTO plan_log
+				(customer_id, plan_name, date_range)
+			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
+			ON CONFLICT DO NOTHING
+		SQL
+	end
+
+protected
+
+	def charge_for_plan
+		params = [
+			@customer_id,
+			"#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
+			-@plan.monthly_price
+		]
+		DB.exec(<<~SQL, params)
+			INSERT INTO transactions
+				(customer_id, transaction_id, created_at, amount)
+			VALUES ($1, $2, LOCALTIMESTAMP, $3)
+		SQL
+	end
+
+	def add_one_month_to_current_plan
+		DB.exec(<<~SQL, [@customer_id])
+			UPDATE plan_log SET date_range=range_merge(
+				date_range,
+				tsrange(
+					LOCALTIMESTAMP,
+					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
+				)
+			)
+			WHERE
+				customer_id=$1 AND
+				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
+		SQL
+	end
+end

lib/registration.rb 🔗

@@ -6,7 +6,7 @@ require_relative "./oob"
 
 class Registration
 	def self.for(iq, customer, web_register_manager)
-		EMPromise.resolve(customer&.registered?).then do |registered|
+		customer.registered?.then do |registered|
 			if registered
 				Registered.new(iq, registered.phone)
 			else
@@ -36,13 +36,10 @@ class Registration
 
 	class Activation
 		def self.for(iq, customer, tel)
-			if customer&.active?
+			if customer.active?
 				Finish.new(iq, customer, tel)
-			elsif customer
-				EMPromise.resolve(new(iq, customer, tel))
 			else
-				# Create customer_id
-				raise "TODO"
+				EMPromise.resolve(new(iq, customer, tel))
 			end
 		end
 

sgx_jmp.rb 🔗

@@ -77,7 +77,6 @@ class AsyncBraintree
 end
 
 BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
-BACKEND_SGX = BackendSgx.new
 
 def panic(e)
 	m = e.respond_to?(:message) ? e.message : e
@@ -178,7 +177,7 @@ end
 
 command :execute?, node: "jabber:iq:register", sessionid: nil do |iq|
 	Customer.for_jid(iq.from.stripped).catch {
-		nil
+		Customer.create(iq.from.stripped)
 	}.then { |customer|
 		Registration.for(
 			iq,

test/test_backend_sgx.rb 🔗

@@ -7,7 +7,7 @@ BackendSgx::IQ_MANAGER = Minitest::Mock.new
 
 class BackendSgxTest < Minitest::Test
 	def setup
-		@sgx = BackendSgx.new
+		@sgx = BackendSgx.new("test")
 	end
 
 	def test_registered
@@ -19,7 +19,7 @@ class BackendSgxTest < Minitest::Test
 				assert_equal "customer_test@component", ibr.from.to_s
 			end]
 		)
-		assert @sgx.registered?("test").sync
+		assert @sgx.registered?.sync
 	end
 	em :test_registered
 
@@ -32,7 +32,7 @@ class BackendSgxTest < Minitest::Test
 				assert_equal "customer_test@component", ibr.from.to_s
 			end]
 		)
-		refute @sgx.registered?("test").sync
+		refute @sgx.registered?.sync
 	end
 	em :test_registered_not_registered
 
@@ -48,7 +48,7 @@ class BackendSgxTest < Minitest::Test
 				assert_equal "+15555550000", ibr.phone
 			end]
 		)
-		@sgx.register!("test", "+15555550000")
+		@sgx.register!("+15555550000")
 		BackendSgx::IQ_MANAGER.verify
 	end
 end

test/test_customer.rb 🔗

@@ -3,8 +3,10 @@
 require "test_helper"
 require "customer"
 
+Customer::BRAINTREE = Minitest::Mock.new
 Customer::REDIS = Minitest::Mock.new
 Customer::DB = Minitest::Mock.new
+CustomerPlan::DB = Minitest::Mock.new
 
 class CustomerTest < Minitest::Test
 	def test_for_jid
@@ -48,12 +50,32 @@ class CustomerTest < Minitest::Test
 	end
 	em :test_for_customer_id_not_found
 
+	def test_create
+		braintree_customer = Minitest::Mock.new
+		Customer::BRAINTREE.expect(:customer, braintree_customer)
+		braintree_customer.expect(:create, EMPromise.resolve(
+			OpenStruct.new(success?: true, customer: OpenStruct.new(id: "test"))
+		))
+		Customer::REDIS.expect(
+			:msetnx,
+			EMPromise.resolve(1),
+			[
+				"jmp_customer_id-test@example.com", "test",
+				"jmp_customer_jid-test", "test@example.com"
+			]
+		)
+		assert_kind_of Customer, Customer.create("test@example.com").sync
+		braintree_customer.verify
+		Customer::REDIS.verify
+	end
+	em :test_create
+
 	def test_bill_plan_activate
-		Customer::DB.expect(:transaction, nil) do |&block|
+		CustomerPlan::DB.expect(:transaction, nil) do |&block|
 			block.call
 			true
 		end
-		Customer::DB.expect(
+		CustomerPlan::DB.expect(
 			:exec,
 			nil,
 			[
@@ -65,22 +87,22 @@ class CustomerTest < Minitest::Test
 				end
 			]
 		)
-		Customer::DB.expect(
+		CustomerPlan::DB.expect(
 			:exec,
 			OpenStruct.new(cmd_tuples: 1),
 			[String, ["test", "test_usd"]]
 		)
 		Customer.new("test", plan_name: "test_usd").bill_plan.sync
-		Customer::DB.verify
+		CustomerPlan::DB.verify
 	end
 	em :test_bill_plan_activate
 
 	def test_bill_plan_update
-		Customer::DB.expect(:transaction, nil) do |&block|
+		CustomerPlan::DB.expect(:transaction, nil) do |&block|
 			block.call
 			true
 		end
-		Customer::DB.expect(
+		CustomerPlan::DB.expect(
 			:exec,
 			nil,
 			[
@@ -92,14 +114,14 @@ class CustomerTest < Minitest::Test
 				end
 			]
 		)
-		Customer::DB.expect(
+		CustomerPlan::DB.expect(
 			:exec,
 			OpenStruct.new(cmd_tuples: 0),
 			[String, ["test", "test_usd"]]
 		)
-		Customer::DB.expect(:exec, nil, [String, ["test"]])
+		CustomerPlan::DB.expect(:exec, nil, [String, ["test"]])
 		Customer.new("test", plan_name: "test_usd").bill_plan.sync
-		Customer::DB.verify
+		CustomerPlan::DB.verify
 	end
 	em :test_bill_plan_update
 end

test/test_helper.rb 🔗

@@ -70,8 +70,6 @@ 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 🔗

@@ -5,31 +5,34 @@ require "registration"
 
 class RegistrationTest < Minitest::Test
 	def test_for_registered
-		BACKEND_SGX.expect(
-			:registered?,
-			EMPromise.resolve(OpenStruct.new(phone: "+15555550000")),
-			["test"]
+		sgx = OpenStruct.new(
+			registered?: EMPromise.resolve(OpenStruct.new(phone: "+15555550000"))
 		)
 		iq = Blather::Stanza::Iq::Command.new
 		iq.from = "test@example.com"
-		result = Registration.for(iq, Customer.new("test"), Minitest::Mock.new).sync
+		result = Registration.for(
+			iq,
+			Customer.new("test", sgx: sgx),
+			Minitest::Mock.new
+		).sync
 		assert_kind_of Registration::Registered, result
 	end
 	em :test_for_registered
 
 	def test_for_activated
-		BACKEND_SGX.expect(
-			:registered?,
-			EMPromise.resolve(nil),
-			["test"]
-		)
+		sgx = OpenStruct.new(registered?: EMPromise.resolve(nil))
 		web_manager = WebRegisterManager.new
 		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),
+			Customer.new(
+				"test",
+				plan_name: "test_usd",
+				expires_at: Time.now + 999,
+				sgx: sgx
+			),
 			web_manager
 		).sync
 		assert_kind_of Registration::Finish, result
@@ -37,31 +40,20 @@ class RegistrationTest < Minitest::Test
 	em :test_for_activated
 
 	def test_for_not_activated_with_customer_id
-		BACKEND_SGX.expect(
-			:registered?,
-			EMPromise.resolve(nil),
-			["test"]
-		)
+		sgx = OpenStruct.new(registered?: EMPromise.resolve(nil))
 		web_manager = WebRegisterManager.new
 		web_manager["test@example.com"] = "+15555550000"
 		iq = Blather::Stanza::Iq::Command.new
 		iq.from = "test@example.com"
 		result = Registration.for(
 			iq,
-			Customer.new("test"),
+			Customer.new("test", sgx: sgx),
 			web_manager
 		).sync
 		assert_kind_of Registration::Activation, result
 	end
 	em :test_for_not_activated_with_customer_id
 
-	def test_for_not_activated_without_customer_id
-		skip "customer_id creation not implemented yet"
-		iq = Blather::Stanza::Iq::Command.new
-		Registration.for(iq, nil, Minitest::Mock.new).sync
-	end
-	em :test_for_not_activated_without_customer_id
-
 	class ActivationTest < Minitest::Test
 		Registration::Activation::COMMAND_MANAGER = Minitest::Mock.new
 		def setup
@@ -505,11 +497,12 @@ class RegistrationTest < Minitest::Test
 		Registration::Finish::REDIS = Minitest::Mock.new
 
 		def setup
+			@sgx = Minitest::Mock.new(BackendSgx.new("test"))
 			iq = Blather::Stanza::Iq::Command.new
 			iq.from = "test\\40example.com@cheogram.com"
 			@finish = Registration::Finish.new(
 				iq,
-				Customer.new("test"),
+				Customer.new("test", sgx: @sgx),
 				"+15555550000"
 			)
 		end
@@ -548,10 +541,10 @@ class RegistrationTest < Minitest::Test
 					"Content-Type" => "application/json"
 				}
 			).to_return(status: 201)
-			BACKEND_SGX.expect(
+			@sgx.expect(
 				:register!,
 				EMPromise.resolve(OpenStruct.new(error?: false)),
-				["test", "+15555550000"]
+				["+15555550000"]
 			)
 			Registration::Finish::REDIS.expect(
 				:set,
@@ -580,7 +573,7 @@ class RegistrationTest < Minitest::Test
 			)
 			@finish.write.sync
 			assert_requested create_order
-			BACKEND_SGX.verify
+			@sgx.verify
 			Registration::Finish::REDIS.verify
 			Registration::Finish::BLATHER.verify
 		end