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, :parent_customer_id,
 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		parent_customer_id: nil
 38	)
 39		@customer_id = customer_id
 40		@plan = plan || OpenStruct.new
 41		@expires_at = expires_at
 42		@auto_top_up_amount = auto_top_up_amount || 0
 43		@monthly_overage_limit = monthly_overage_limit || 0
 44		@parent_customer_id = parent_customer_id
 45	end
 46
 47	def active?
 48		plan_name && @expires_at > Time.now
 49	end
 50
 51	def with_plan_name(plan_name)
 52		self.class.new(
 53			@customer_id,
 54			plan: Plan.for(plan_name),
 55			expires_at: @expires_at
 56		)
 57	end
 58
 59	def save_plan!
 60		DB.exec_defer(<<~SQL, [@customer_id, plan_name])
 61			INSERT INTO plan_log
 62				(customer_id, plan_name, date_range)
 63			VALUES (
 64				$1,
 65				$2,
 66				tsrange(
 67					LOCALTIMESTAMP - '2 seconds'::interval,
 68					LOCALTIMESTAMP - '1 second'::interval
 69				)
 70			)
 71		SQL
 72	end
 73
 74	def bill_plan(note: nil)
 75		EMPromise.resolve(nil).then do
 76			DB.transaction do |db|
 77				next false unless !block_given? || yield(db)
 78
 79				charge_for_plan(note)
 80				add_one_month_to_current_plan unless activate_plan_starting_now
 81				true
 82			end
 83		end
 84	end
 85
 86	def activate_plan_starting_now
 87		activated = DB.exec(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
 88			INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
 89			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
 90			ON CONFLICT DO NOTHING
 91		SQL
 92		activated = activated.cmd_tuples.positive?
 93		return false unless activated
 94
 95		DB.exec(<<~SQL, [@customer_id])
 96			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
 97				AND upper(date_range) - lower(date_range) < '2 seconds'
 98		SQL
 99	end
100
101	def activation_date
102		DB.query_one(<<~SQL, @customer_id).then { |r| r[:start_date] }
103			SELECT
104				MIN(LOWER(date_range)) AS start_date
105			FROM plan_log WHERE customer_id = $1;
106		SQL
107	end
108
109protected
110
111	def charge_for_plan(note)
112		params = [
113			@customer_id,
114			"#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
115			-@plan.monthly_price,
116			note
117		]
118		DB.exec(<<~SQL, params)
119			INSERT INTO transactions
120				(customer_id, transaction_id, created_at, settled_after, amount, note)
121			VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
122		SQL
123	end
124
125	def add_one_month_to_current_plan
126		DB.exec(<<~SQL, [@customer_id])
127			UPDATE plan_log SET date_range=range_merge(
128				date_range,
129				tsrange(
130					LOCALTIMESTAMP,
131					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
132				)
133			)
134			WHERE
135				customer_id=$1 AND
136				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
137		SQL
138	end
139end