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