customer_plan.rb

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