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