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