1# frozen_string_literal: true
  2
  3require "forwardable"
  4require "value_semantics/monkey_patched"
  5
  6require_relative "em"
  7require_relative "parented_domain"
  8require_relative "plan"
  9
 10class CustomerPlan
 11	extend Forwardable
 12
 13	def_delegator :plan, :name, :plan_name
 14	def_delegators :plan, :currency, :merchant_account,
 15	               :minute_limit, :message_limit
 16
 17	value_semantics do
 18		customer_id           String
 19		plan                  Anything(), default: nil, coerce: true
 20		expires_at            Either(Time, nil), default_generator: -> { Time.now }
 21		auto_top_up_amount    Integer, default: 0
 22		monthly_overage_limit Integer, default: 0
 23		pending               Bool(), default: false
 24		parent_customer_id    Either(String, nil), default: nil
 25		parent_plan           Anything(), default: nil, coerce: true
 26	end
 27
 28	class << self
 29		def default(customer_id, jid)
 30			new(customer_id, **ParentedDomain.for(jid)&.plan_kwargs || {})
 31		end
 32
 33		def extract(**kwargs)
 34			new(**kwargs.slice(
 35				*value_semantics.attributes.map(&:name), :plan_name, :parent_plan_name
 36			))
 37		end
 38
 39		def coerce_plan(plan_or_name_or_nil)
 40			return plan_or_name_or_nil if plan_or_name_or_nil.is_a?(OpenStruct)
 41			return OpenStruct.new unless plan_or_name_or_nil
 42
 43			Plan.for(plan_or_name_or_nil)
 44		end
 45		alias coerce_parent_plan coerce_plan
 46	end
 47
 48	def initialize(customer_id=nil, **kwargs)
 49		kwargs, customer_id = customer_id, nil if customer_id.is_a?(Hash)
 50		kwargs[:plan] = kwargs.delete(:plan_name) if kwargs.key?(:plan_name)
 51		if kwargs.key?(:parent_plan_name)
 52			kwargs[:parent_plan] = kwargs.delete(:parent_plan_name)
 53		end
 54		super(customer_id ? kwargs.merge(customer_id: customer_id) : kwargs)
 55	end
 56
 57	def active?
 58		plan_name && expires_at > Time.now
 59	end
 60
 61	def status
 62		return :active if active?
 63		return :pending if pending
 64
 65		:expired
 66	end
 67
 68	def monthly_price
 69		plan.monthly_price - (parent_plan&.subaccount_discount || 0)
 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			-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					LOWER(date_range),
168					GREATEST(UPPER(date_range), LOCALTIMESTAMP) + '1 month'
169				)
170			)
171			WHERE
172				customer_id=$1 AND
173				UPPER(date_range) = (SELECT MAX(UPPER(date_range)) FROM plan_log WHERE
174				customer_id=$1 AND date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
175		SQL
176	end
177end