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