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