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