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