# frozen_string_literal: true
require "forwardable"
require "value_semantics/monkey_patched"
require_relative "em"
require_relative "parented_domain"
require_relative "plan"
class CustomerPlan
extend Forwardable
def_delegator :plan, :name, :plan_name
def_delegators :plan, :currency, :merchant_account,
:minute_limit, :message_limit
value_semantics do
customer_id String
plan Anything(), default: nil, coerce: true
expires_at Either(Time, nil), default_generator: -> { Time.now }
auto_top_up_amount Integer, default: 0
monthly_overage_limit Integer, default: 0
pending Bool(), default: false
parent_customer_id Either(String, nil), default: nil
parent_plan Anything(), default: nil, coerce: true
end
class << self
def default(customer_id, jid)
new(customer_id, **ParentedDomain.for(jid)&.plan_kwargs || {})
end
def extract(**kwargs)
new(**kwargs.slice(
*value_semantics.attributes.map(&:name), :plan_name, :parent_plan_name
))
end
def coerce_plan(plan_or_name_or_nil)
return plan_or_name_or_nil if plan_or_name_or_nil.is_a?(OpenStruct)
return OpenStruct.new unless plan_or_name_or_nil
Plan.for(plan_or_name_or_nil)
end
alias coerce_parent_plan coerce_plan
end
def initialize(customer_id=nil, **kwargs)
kwargs, customer_id = customer_id, nil if customer_id.is_a?(Hash)
kwargs[:plan] = kwargs.delete(:plan_name) if kwargs.key?(:plan_name)
if kwargs.key?(:parent_plan_name)
kwargs[:parent_plan] = kwargs.delete(:parent_plan_name)
end
super(customer_id ? kwargs.merge(customer_id: customer_id) : kwargs)
end
def active?
plan_name && expires_at > Time.now
end
def status
return :active if active?
return :pending if pending
:expired
end
def monthly_price
plan.monthly_price - (parent_plan&.subaccount_discount || 0)
end
def verify_parent!
return unless parent_customer_id
result = DB.query(<<~SQL, [parent_customer_id])
SELECT plan_name FROM customer_plans WHERE customer_id=$1
SQL
raise "Invalid parent account" if !result || !result.first
plan = Plan.for(result.first["plan_name"])
raise "Parent currency mismatch" unless plan.currency == currency
end
def save_plan!
verify_parent!
DB.exec_defer(<<~SQL, [customer_id, plan_name, parent_customer_id])
INSERT INTO plan_log
(customer_id, plan_name, parent_customer_id, date_range)
VALUES (
$1,
$2,
$3,
tsrange(
LOCALTIMESTAMP - '2 seconds'::interval,
LOCALTIMESTAMP - '1 second'::interval
)
)
SQL
end
def bill_plan(note: nil)
EMPromise.resolve(nil).then do
DB.transaction do |db|
next false unless !block_given? || yield(db)
charge_for_plan(note)
extend_plan
true
end
end
end
def activate_plan_starting_now
verify_parent!
activated = DB.exec(<<~SQL, [customer_id, plan_name, parent_customer_id])
INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
ON CONFLICT DO NOTHING
SQL
activated = activated.cmd_tuples.positive?
return false unless activated
DB.exec(<<~SQL, [customer_id])
DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
AND upper(date_range) - lower(date_range) < '2 seconds'
SQL
end
def extend_plan
add_one_month_to_current_plan unless activate_plan_starting_now
end
def activation_date
DB.query_one(<<~SQL, customer_id).then { |r| r[:start_date] }
SELECT
MIN(LOWER(date_range)) AS start_date
FROM plan_log WHERE customer_id = $1;
SQL
end
protected :customer_id, :plan, :pending, :[]
protected
def charge_for_plan(note)
raise "No plan setup" unless plan
params = [
customer_id,
"#{customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
-monthly_price,
note
]
DB.exec(<<~SQL, params)
INSERT INTO transactions
(customer_id, transaction_id, created_at, settled_after, amount, note)
VALUES ($1, $2, LOCALTIMESTAMP, LOCALTIMESTAMP, $3, $4)
SQL
end
def add_one_month_to_current_plan
DB.exec(<<~SQL, [customer_id])
UPDATE plan_log SET date_range=range_merge(
date_range,
tsrange(
LOWER(date_range),
GREATEST(UPPER(date_range), LOCALTIMESTAMP) + '1 month'
)
)
WHERE
customer_id=$1 AND
UPPER(date_range) = (SELECT MAX(UPPER(date_range)) FROM plan_log WHERE
customer_id=$1 AND date_range && tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'))
SQL
end
end