Break out CustomerPlan

Stephen Paul Weber created

We had Plan and Customer but the relationship between the two lived
entirely in Customer, which was growing quite large. Break that
relationship out into its own concept and give it a name.

Change summary

lib/customer.rb       | 66 ++++++------------------------------------
lib/customer_plan.rb  | 69 +++++++++++++++++++++++++++++++++++++++++++++
test/test_customer.rb | 19 ++++++-----
3 files changed, 89 insertions(+), 65 deletions(-)

Detailed changes

lib/customer.rb 🔗

@@ -2,6 +2,7 @@
 
 require "forwardable"
 
+require_relative "./customer_plan"
 require_relative "./backend_sgx"
 require_relative "./ibr"
 require_relative "./payment_methods"
@@ -29,8 +30,8 @@ class Customer
 	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(
@@ -40,8 +41,11 @@ class Customer
 		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
@@ -51,29 +55,11 @@ class Customer
 		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
@@ -82,37 +68,5 @@ class Customer
 			.then(PaymentMethods.method(:for_braintree_customer))
 	end
 
-	def active?
-		@plan && @expires_at > Time.now
-	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

test/test_customer.rb 🔗

@@ -5,6 +5,7 @@ require "customer"
 
 Customer::REDIS = Minitest::Mock.new
 Customer::DB = Minitest::Mock.new
+CustomerPlan::DB = Minitest::Mock.new
 
 class CustomerTest < Minitest::Test
 	def test_for_jid
@@ -49,11 +50,11 @@ class CustomerTest < Minitest::Test
 	em :test_for_customer_id_not_found
 
 	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 +66,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 +93,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