Refactor CutomerPlan to use value semantics

Stephen Paul Weber created

Change summary

lib/customer.rb      |  2 
lib/customer_plan.rb | 82 ++++++++++++++++++++-------------------------
lib/plan.rb          |  2 +
3 files changed, 40 insertions(+), 46 deletions(-)

Detailed changes

lib/customer.rb 🔗

@@ -84,7 +84,7 @@ class Customer
 	def with_plan(plan_name, **kwargs)
 		self.class.new(
 			@customer_id, @jid,
-			plan: @plan.with_plan_name(plan_name, **kwargs),
+			plan: @plan.with(plan_name: plan_name, **kwargs),
 			balance: @balance, tndetails: @tndetails, sgx: @sgx
 		)
 	end

lib/customer_plan.rb 🔗

@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require "forwardable"
+require "value_semantics/monkey_patched"
 
 require_relative "em"
 require_relative "plan"
@@ -8,21 +9,24 @@ require_relative "plan"
 class CustomerPlan
 	extend Forwardable
 
-	attr_reader :expires_at, :auto_top_up_amount, :monthly_overage_limit,
-	            :parent_customer_id
-
-	def_delegator :@plan, :name, :plan_name
-	def_delegators :@plan, :currency, :merchant_account, :monthly_price,
+	def_delegator :plan, :name, :plan_name
+	def_delegators :plan, :currency, :merchant_account, :monthly_price,
 	               :minute_limit, :message_limit
 
-	def self.for(customer_id, plan_name: nil, **kwargs)
-		new(customer_id, plan: plan_name&.then(&Plan.method(:for)), **kwargs)
+	value_semantics do
+		customer_id           String
+		plan                  Anything(), default: nil, coerce: true
+		expires_at            Either(Time, nil), default_generator: -> { Time.now }
+		auto_top_up_amount    Integer, default: 0
+		monthly_overage_limit Integer, default: 0
+		pending               Bool(), default: false
+		parent_customer_id    Either(String, nil), default: nil
 	end
 
 	def self.default(customer_id, jid)
 		config = CONFIG[:parented_domains][Blather::JID.new(jid).domain]
 		if config
-			self.for(
+			new(
 				customer_id,
 				plan_name: config[:plan_name],
 				parent_customer_id: config[:customer_id]
@@ -33,7 +37,7 @@ class CustomerPlan
 	end
 
 	def self.extract(customer_id, **kwargs)
-		self.for(
+		new(
 			customer_id,
 			**kwargs.slice(
 				:plan_name, :expires_at, :parent_customer_id, :pending,
@@ -42,46 +46,32 @@ class CustomerPlan
 		)
 	end
 
-	def initialize(
-		customer_id,
-		plan: nil,
-		expires_at: Time.now,
-		auto_top_up_amount: 0,
-		monthly_overage_limit: 0,
-		pending: false, parent_customer_id: nil
-	)
-		@customer_id = customer_id
-		@plan = plan || OpenStruct.new
-		@expires_at = expires_at
-		@auto_top_up_amount = auto_top_up_amount || 0
-		@monthly_overage_limit = monthly_overage_limit || 0
-		@pending = pending
-		@parent_customer_id = parent_customer_id
+	def self.coerce_plan(plan_or_name_or_nil)
+		return OpenStruct.new unless plan_or_name_or_nil
+
+		Plan.for(plan_or_name_or_nil)
+	end
+
+	def initialize(customer_id=nil, **kwargs)
+		kwargs[:plan] = kwargs.delete(:plan_name) if kwargs.key?(:plan_name)
+		super(customer_id ? kwargs.merge(customer_id: customer_id) : kwargs)
 	end
 
 	def active?
-		plan_name && @expires_at > Time.now
+		plan_name && expires_at > Time.now
 	end
 
 	def status
 		return :active if active?
-		return :pending if @pending
+		return :pending if pending
 
 		:expired
 	end
 
-	def with_plan_name(plan_name, **kwargs)
-		self.class.new(
-			@customer_id,
-			plan: Plan.for(plan_name),
-			expires_at: @expires_at, **kwargs
-		)
-	end
-
 	def verify_parent!
-		return unless @parent_customer_id
+		return unless parent_customer_id
 
-		result = DB.query(<<~SQL, [@parent_customer_id])
+		result = DB.query(<<~SQL, [parent_customer_id])
 			SELECT plan_name FROM customer_plans WHERE customer_id=$1
 		SQL
 
@@ -93,7 +83,7 @@ class CustomerPlan
 
 	def save_plan!
 		verify_parent!
-		DB.exec_defer(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
+		DB.exec_defer(<<~SQL, [customer_id, plan_name, parent_customer_id])
 			INSERT INTO plan_log
 				(customer_id, plan_name, parent_customer_id, date_range)
 			VALUES (
@@ -122,7 +112,7 @@ class CustomerPlan
 
 	def activate_plan_starting_now
 		verify_parent!
-		activated = DB.exec(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
+		activated = DB.exec(<<~SQL, [customer_id, plan_name, parent_customer_id])
 			INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
 			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
 			ON CONFLICT DO NOTHING
@@ -130,7 +120,7 @@ class CustomerPlan
 		activated = activated.cmd_tuples.positive?
 		return false unless activated
 
-		DB.exec(<<~SQL, [@customer_id])
+		DB.exec(<<~SQL, [customer_id])
 			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
 				AND upper(date_range) - lower(date_range) < '2 seconds'
 		SQL
@@ -141,22 +131,24 @@ class CustomerPlan
 	end
 
 	def activation_date
-		DB.query_one(<<~SQL, @customer_id).then { |r| r[:start_date] }
+		DB.query_one(<<~SQL, customer_id).then { |r| r[:start_date] }
 			SELECT
 				MIN(LOWER(date_range)) AS start_date
 			FROM plan_log WHERE customer_id = $1;
 		SQL
 	end
 
+	protected :customer_id, :plan, :pending, :[]
+
 protected
 
 	def charge_for_plan(note)
-		raise "No plan setup" unless @plan
+		raise "No plan setup" unless plan
 
 		params = [
-			@customer_id,
-			"#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
-			-@plan.monthly_price,
+			customer_id,
+			"#{customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
+			-plan.monthly_price,
 			note
 		]
 		DB.exec(<<~SQL, params)
@@ -167,7 +159,7 @@ protected
 	end
 
 	def add_one_month_to_current_plan
-		DB.exec(<<~SQL, [@customer_id])
+		DB.exec(<<~SQL, [customer_id])
 			UPDATE plan_log SET date_range=range_merge(
 				date_range,
 				tsrange(

lib/plan.rb 🔗

@@ -2,6 +2,8 @@
 
 class Plan
 	def self.for(plan_name)
+		return plan_name if plan_name.is_a?(Plan)
+
 		plan = CONFIG[:plans].find { |p| p[:name] == plan_name }
 		raise "No plan by that name" unless plan