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