1# frozen_string_literal: true
2
3require "value_semantics/monkey_patched"
4
5require_relative "tts_template"
6require_relative "low_balance"
7
8class CallAttempt
9 def self.for(customer:, usage:, **kwargs)
10 credit = [(customer.minute_limit&.to_d || 100) - usage, 0].max +
11 customer.balance
12 @kinds.each do |kind|
13 ca = kind.call(
14 customer: customer, usage: usage, credit: credit,
15 **kwargs.merge(limits(customer, usage, credit, **kwargs))
16 )
17 return ca if ca
18 end
19
20 raise "No CallAttempt matched"
21 end
22
23 def self.limits(customer, usage, credit, rate:, **)
24 unless customer&.minute_limit && usage && rate && rate.positive?
25 return { limit_remaining: 100, max_minutes: 100 }
26 end
27
28 can_use = customer.minute_limit.to_d + customer.monthly_overage_limit
29 {
30 limit_remaining: ([can_use - usage, 0].max / rate).to_i,
31 max_minutes: (credit / rate).to_i
32 }
33 end
34
35 def self.register(&maybe_mk)
36 @kinds ||= []
37 @kinds << maybe_mk
38 end
39
40 value_semantics do
41 customer_id String
42 from String
43 to(/\A\+\d+\Z/)
44 sgx Blather::JID
45 call_id String
46 direction Either(:inbound, :outbound)
47 limit_remaining Integer
48 max_minutes Integer
49 end
50
51 def to_render
52 ["#{direction}/connect", { locals: to_h }]
53 end
54
55 def to_s
56 "Allowed(max_minutes: #{max_minutes}, limit_remaining: #{limit_remaining})"
57 end
58
59 def create_call(fwd, *args, &block)
60 fwd.create_call(*args, &block)
61 end
62
63 def as_json(*)
64 to_h
65 end
66
67 def to_json(*args)
68 as_json.to_json(*args)
69 end
70
71 class Expired
72 CallAttempt.register do |customer:, direction:, **|
73 new(direction: direction) if customer.plan_name && !customer.active?
74 end
75
76 value_semantics do
77 direction Either(:inbound, :outbound)
78 end
79
80 def view
81 "#{direction}/expired"
82 end
83
84 def tts
85 TTSTemplate.new(view).tts(self)
86 end
87
88 def to_render
89 [view]
90 end
91
92 def to_s
93 "Expired"
94 end
95
96 def create_call(*); end
97
98 def as_json(*)
99 tts.empty? ? {} : { tts: tts }
100 end
101
102 def to_json(*args)
103 as_json.to_json(*args)
104 end
105 end
106
107 class Unsupported
108 CallAttempt.register do |supported:, direction:, **|
109 new(direction: direction) unless supported
110 end
111
112 value_semantics do
113 direction Either(:inbound, :outbound)
114 end
115
116 def view
117 "#{direction}/unsupported"
118 end
119
120 def tts
121 TTSTemplate.new(view).tts(self)
122 end
123
124 def to_render
125 [view]
126 end
127
128 def to_s
129 "Unsupported"
130 end
131
132 def create_call(*); end
133
134 def as_json(*)
135 tts.empty? ? {} : { tts: tts }
136 end
137
138 def to_json(*args)
139 as_json.to_json(*args)
140 end
141 end
142
143 class NoBalance
144 CallAttempt.register do |credit:, rate:, **kwargs|
145 self.for(rate: rate, **kwargs) if credit < rate * 10
146 end
147
148 def self.for(customer:, direction:, low_balance: LowBalance, **kwargs)
149 low_balance.for(customer).then(&:notify!).then do |amount|
150 if amount&.positive?
151 CallAttempt.for(
152 customer: customer.with_balance(customer.balance + amount),
153 **kwargs.merge(direction: direction)
154 )
155 else
156 NoBalance.new(balance: customer.balance, direction: direction)
157 end
158 end
159 end
160
161 value_semantics do
162 balance Numeric
163 direction Either(:inbound, :outbound)
164 end
165
166 def view
167 "#{direction}/no_balance"
168 end
169
170 def tts
171 TTSTemplate.new(view).tts(self)
172 end
173
174 def to_render
175 [view, { locals: to_h }]
176 end
177
178 def to_s
179 "NoBalance"
180 end
181
182 def create_call(*); end
183
184 def as_json(*)
185 tts.empty? ? {} : { tts: tts }
186 end
187
188 def to_json(*args)
189 as_json.to_json(*args)
190 end
191 end
192
193 class AtLimit
194 value_semantics do
195 customer_id String
196 from String
197 to(/\A\+\d+\Z/)
198 call_id String
199 direction Either(:inbound, :outbound)
200 limit_remaining Integer
201 max_minutes Integer
202 end
203
204 CallAttempt.register do |digits: nil, limit_remaining:, customer:, **kwargs|
205 if digits != "1" && limit_remaining < 10
206 new(
207 **kwargs
208 .merge(
209 limit_remaining: limit_remaining,
210 customer_id: customer.customer_id
211 ).slice(*value_semantics.attributes.map(&:name))
212 )
213 end
214 end
215
216 def view
217 "#{direction}/at_limit"
218 end
219
220 def tts
221 TTSTemplate.new(view).tts(self)
222 end
223
224 def to_render
225 [view, { locals: to_h }]
226 end
227
228 def to_s
229 "AtLimit(max_minutes: #{max_minutes}, "\
230 "limit_remaining: #{limit_remaining})"
231 end
232
233 def create_call(fwd, *args, &block)
234 fwd.create_call(*args, &block)
235 end
236
237 def as_json(*)
238 {
239 tts: tts,
240 from: from,
241 to: to,
242 customer_id: customer_id,
243 limit_remaining: limit_remaining,
244 max_minutes: max_minutes
245 }
246 end
247
248 def to_json(*args)
249 as_json.to_json(*args)
250 end
251 end
252
253 register do |customer:, **kwargs|
254 new(
255 **kwargs
256 .merge(customer_id: customer.customer_id, sgx: customer.sgx)
257 .slice(*value_semantics.attributes.map(&:name))
258 )
259 end
260
261 class TollFree
262 CallAttempt.register do |rate:, customer:, **kwargs|
263 if rate&.zero?
264 new(
265 **kwargs
266 .merge(customer_id: customer.customer_id, sgx: customer.sgx)
267 .slice(*value_semantics.attributes.map(&:name))
268 )
269 end
270 end
271
272 value_semantics do
273 customer_id String
274 from String
275 to(/\A\+\d+\Z/)
276 sgx Blather::JID
277 call_id String
278 direction Either(:inbound, :outbound)
279 end
280
281 def to_render
282 ["#{direction}/connect", { locals: to_h }]
283 end
284
285 def to_s
286 "TollFree"
287 end
288
289 def create_call(fwd, *args, &block)
290 fwd.create_call(*args, &block)
291 end
292
293 def as_json(*)
294 to_h
295 end
296
297 def to_json(*args)
298 as_json.to_json(*args)
299 end
300 end
301end