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