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, customer_id = customer_id, nil if customer_id.is_a?(Hash)
 57		kwargs[:plan] = kwargs.delete(:plan_name) if kwargs.key?(:plan_name)
 58		super(customer_id ? kwargs.merge(customer_id: customer_id) : kwargs)
 59	end
 60
 61	def active?
 62		plan_name && expires_at > Time.now
 63	end
 64
 65	def status
 66		return :active if active?
 67		return :pending if pending
 68
 69		:expired
 70	end
 71
 72	def verify_parent!
 73		return unless parent_customer_id
 74
 75		result = DB.query(<<~SQL, [parent_customer_id])
 76			SELECT plan_name FROM customer_plans WHERE customer_id=$1
 77		SQL
 78
 79		raise "Invalid parent account" if !result || !result.first
 80
 81		plan = Plan.for(result.first["plan_name"])
 82		raise "Parent currency mismatch" unless plan.currency == currency
 83	end
 84
 85	def save_plan!
 86		verify_parent!
 87		DB.exec_defer(<<~SQL, [customer_id, plan_name, parent_customer_id])
 88			INSERT INTO plan_log
 89				(customer_id, plan_name, parent_customer_id, date_range)
 90			VALUES (
 91				$1,
 92				$2,
 93				$3,
 94				tsrange(
 95					LOCALTIMESTAMP - '2 seconds'::interval,
 96					LOCALTIMESTAMP - '1 second'::interval
 97				)
 98			)
 99		SQL
100	end
101
102	def bill_plan(note: nil)
103		EMPromise.resolve(nil).then do
104			DB.transaction do |db|
105				next false unless !block_given? || yield(db)
106
107				charge_for_plan(note)
108				extend_plan
109				true
110			end
111		end
112	end
113
114	def activate_plan_starting_now
115		verify_parent!
116		activated = DB.exec(<<~SQL, [customer_id, plan_name, parent_customer_id])
117			INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
118			VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
119			ON CONFLICT DO NOTHING
120		SQL
121		activated = activated.cmd_tuples.positive?
122		return false unless activated
123
124		DB.exec(<<~SQL, [customer_id])
125			DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
126				AND upper(date_range) - lower(date_range) < '2 seconds'
127		SQL
128	end
129
130	def extend_plan
131		add_one_month_to_current_plan unless activate_plan_starting_now
132	end
133
134	def activation_date
135		DB.query_one(<<~SQL, customer_id).then { |r| r[:start_date] }
136			SELECT
137				MIN(LOWER(date_range)) AS start_date
138			FROM plan_log WHERE customer_id = $1;
139		SQL
140	end
141
142	protected :customer_id, :plan, :pending, :[]
143
144protected
145
146	def charge_for_plan(note)
147		raise "No plan setup" unless plan
148
149		params = [
150			customer_id,
151			"#{customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
152			-plan.monthly_price,
153			note
154		]
155		DB.exec(<<~SQL, params)
156			INSERT INTO transactions
157				(customer_id, transaction_id, created_at, settled_after, amount, note)
158			VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
159		SQL
160	end
161
162	def add_one_month_to_current_plan
163		DB.exec(<<~SQL, [customer_id])
164			UPDATE plan_log SET date_range=range_merge(
165				date_range,
166				tsrange(
167					LOCALTIMESTAMP,
168					GREATEST(upper(date_range), LOCALTIMESTAMP) + '1 month'
169				)
170			)
171			WHERE
172				customer_id=$1 AND
173				date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')
174		SQL
175	end
176end