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	            :parent_customer_id
 13
 14	def_delegator :@plan, :name, :plan_name
 15	def_delegators :@plan, :currency, :merchant_account, :monthly_price,
 16	               :minute_limit, :message_limit
 17
 18	def self.for(customer_id, plan_name: nil, **kwargs)
 19		new(customer_id, plan: plan_name&.then(&Plan.method(:for)), **kwargs)
 20	end
 21
 22	def self.default(customer_id, jid)
 23		config = CONFIG[:parented_domains][Blather::JID.new(jid).domain]
 24		if config
 25			self.for(
 26				customer_id,
 27				plan_name: config[:plan_name],
 28				parent_customer_id: config[:customer_id]
 29			)
 30		else
 31			new(customer_id)
 32		end
 33	end
 34
 35	def self.extract(customer_id, **kwargs)
 36		self.for(
 37			customer_id,
 38			**kwargs.slice(
 39				:plan_name, :expires_at, :parent_customer_id, :pending,
 40				:auto_top_up_amount, :monthly_overage_limit
 41			)
 42		)
 43	end
 44
 45	def initialize(
 46		customer_id,
 47		plan: nil,
 48		expires_at: Time.now,
 49		auto_top_up_amount: 0,
 50		monthly_overage_limit: 0,
 51		pending: false, parent_customer_id: nil
 52	)
 53		@customer_id = customer_id
 54		@plan = plan || OpenStruct.new
 55		@expires_at = expires_at
 56		@auto_top_up_amount = auto_top_up_amount || 0
 57		@monthly_overage_limit = monthly_overage_limit || 0
 58		@pending = pending
 59		@parent_customer_id = parent_customer_id
 60	end
 61
 62	def active?
 63		plan_name && @expires_at > Time.now
 64	end
 65
 66	def status
 67		return :active if active?
 68		return :pending if @pending
 69
 70		:expired
 71	end
 72
 73	def with_plan_name(plan_name)
 74		self.class.new(
 75			@customer_id,
 76			plan: Plan.for(plan_name),
 77			expires_at: @expires_at
 78		)
 79	end
 80
 81	def save_plan!
 82		DB.exec_defer(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
 83			INSERT INTO plan_log
 84				(customer_id, plan_name, parent_customer_id, date_range)
 85			VALUES (
 86				$1,
 87				$2,
 88				$3,
 89				tsrange(
 90					LOCALTIMESTAMP - '2 seconds'::interval,
 91					LOCALTIMESTAMP - '1 second'::interval
 92				)
 93			)
 94		SQL
 95	end
 96
 97	def bill_plan(note: nil)
 98		EMPromise.resolve(nil).then do
 99			DB.transaction do |db|
100				next false unless !block_given? || yield(db)
101
102				charge_for_plan(note)
103				extend_plan
104				true
105			end
106		end
107	end
108
109	def activate_plan_starting_now
110		activated = DB.exec(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
111			INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
112			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
113			ON CONFLICT DO NOTHING
114		SQL
115		activated = activated.cmd_tuples.positive?
116		return false unless activated
117
118		DB.exec(<<~SQL, [@customer_id])
119			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
120				AND upper(date_range) - lower(date_range) < '2 seconds'
121		SQL
122	end
123
124	def extend_plan
125		add_one_month_to_current_plan unless activate_plan_starting_now
126	end
127
128	def activation_date
129		DB.query_one(<<~SQL, @customer_id).then { |r| r[:start_date] }
130			SELECT
131				MIN(LOWER(date_range)) AS start_date
132			FROM plan_log WHERE customer_id = $1;
133		SQL
134	end
135
136protected
137
138	def charge_for_plan(note)
139		raise "No plan setup" unless @plan
140
141		params = [
142			@customer_id,
143			"#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
144			-@plan.monthly_price,
145			note
146		]
147		DB.exec(<<~SQL, params)
148			INSERT INTO transactions
149				(customer_id, transaction_id, created_at, settled_after, amount, note)
150			VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
151		SQL
152	end
153
154	def add_one_month_to_current_plan
155		DB.exec(<<~SQL, [@customer_id])
156			UPDATE plan_log SET date_range=range_merge(
157				date_range,
158				tsrange(
159					LOCALTIMESTAMP,
160					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
161				)
162			)
163			WHERE
164				customer_id=$1 AND
165				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
166		SQL
167	end
168end