call_attempt.rb

  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