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 - 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