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		EMPromise.resolve(nil).then do
 74			DB.transaction do |db|
 75				next false unless !block_given? || yield(db)
 76
 77				charge_for_plan(note)
 78				add_one_month_to_current_plan unless activate_plan_starting_now
 79				true
 80			end
 81		end
 82	end
 83
 84	def activate_plan_starting_now
 85		activated = DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive?
 86			INSERT INTO plan_log (customer_id, plan_name, date_range)
 87			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
 88			ON CONFLICT DO NOTHING
 89		SQL
 90		return false unless activated
 91
 92		DB.exec(<<~SQL, [@customer_id])
 93			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
 94				AND upper(date_range) - lower(date_range) < '2 seconds'
 95		SQL
 96	end
 97
 98	def activation_date
 99		DB.query_one(<<~SQL, @customer_id).then { |r| r[:start_date] }
100			SELECT
101				MIN(LOWER(date_range)) AS start_date
102			FROM plan_log WHERE customer_id = $1;
103		SQL
104	end
105
106protected
107
108	def charge_for_plan(note)
109		params = [
110			@customer_id,
111			"#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
112			-@plan.monthly_price,
113			note
114		]
115		DB.exec(<<~SQL, params)
116			INSERT INTO transactions
117				(customer_id, transaction_id, created_at, settled_after, amount, note)
118			VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
119		SQL
120	end
121
122	def add_one_month_to_current_plan
123		DB.exec(<<~SQL, [@customer_id])
124			UPDATE plan_log SET date_range=range_merge(
125				date_range,
126				tsrange(
127					LOCALTIMESTAMP,
128					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
129				)
130			)
131			WHERE
132				customer_id=$1 AND
133				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
134		SQL
135	end
136end