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