customer_plan.rb

  1# frozen_string_literal: true
  2
  3require "forwardable"
  4
  5require_relative "em"
  6require_relative "plan"
  7
  8class CustomerPlan
  9	extend Forwardable
 10
 11	attr_reader :expires_at, :auto_top_up_amount, :monthly_overage_limit
 12
 13	def_delegator :@plan, :name, :plan_name
 14	def_delegators :@plan, :currency, :merchant_account, :monthly_price,
 15	               :minute_limit, :message_limit
 16
 17	def self.for(customer_id, plan_name: nil, **kwargs)
 18		new(customer_id, plan: plan_name&.then(&Plan.method(:for)), **kwargs)
 19	end
 20
 21	def self.extract(customer_id, **kwargs)
 22		self.for(
 23			customer_id,
 24			**kwargs.slice(
 25				:plan_name, :expires_at,
 26				:auto_top_up_amount, :monthly_overage_limit
 27			)
 28		)
 29	end
 30
 31	def initialize(
 32		customer_id,
 33		plan: nil,
 34		expires_at: Time.now,
 35		auto_top_up_amount: 0,
 36		monthly_overage_limit: 0
 37	)
 38		@customer_id = customer_id
 39		@plan = plan || OpenStruct.new
 40		@expires_at = expires_at
 41		@auto_top_up_amount = auto_top_up_amount || 0
 42		@monthly_overage_limit = monthly_overage_limit || 0
 43	end
 44
 45	def active?
 46		plan_name && @expires_at > Time.now
 47	end
 48
 49	def with_plan_name(plan_name)
 50		self.class.new(
 51			@customer_id,
 52			plan: Plan.for(plan_name),
 53			expires_at: @expires_at
 54		)
 55	end
 56
 57	def save_plan!
 58		DB.exec_defer(<<~SQL, [@customer_id, plan_name])
 59			INSERT INTO plan_log
 60				(customer_id, plan_name, date_range)
 61			VALUES (
 62				$1,
 63				$2,
 64				tsrange(
 65					LOCALTIMESTAMP - '2 seconds'::interval,
 66					LOCALTIMESTAMP - '1 second'::interval
 67				)
 68			)
 69		SQL
 70	end
 71
 72	def bill_plan(note: nil)
 73		EM.promise_fiber do
 74			DB.transaction do
 75				charge_for_plan(note)
 76				add_one_month_to_current_plan unless activate_plan_starting_now
 77			end
 78		end
 79	end
 80
 81	def activate_plan_starting_now
 82		activated = DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive?
 83			INSERT INTO plan_log (customer_id, plan_name, date_range)
 84			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
 85			ON CONFLICT DO NOTHING
 86		SQL
 87		return false unless activated
 88
 89		DB.exec(<<~SQL, [@customer_id])
 90			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
 91				AND upper(date_range) - lower(date_range) < '2 seconds'
 92		SQL
 93	end
 94
 95	def activation_date
 96		DB.query_one(<<~SQL, @customer_id).then { |r| r[:start_date] }
 97			SELECT
 98				MIN(LOWER(date_range)) AS start_date
 99			FROM plan_log WHERE customer_id = $1;
100		SQL
101	end
102
103protected
104
105	def charge_for_plan(note)
106		params = [
107			@customer_id,
108			"#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
109			-@plan.monthly_price,
110			note
111		]
112		DB.exec(<<~SQL, params)
113			INSERT INTO transactions
114				(customer_id, transaction_id, created_at, settled_after, amount, note)
115			VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
116		SQL
117	end
118
119	def add_one_month_to_current_plan
120		DB.exec(<<~SQL, [@customer_id])
121			UPDATE plan_log SET date_range=range_merge(
122				date_range,
123				tsrange(
124					LOCALTIMESTAMP,
125					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
126				)
127			)
128			WHERE
129				customer_id=$1 AND
130				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
131		SQL
132	end
133end