1# frozen_string_literal: true
  2
  3require "date"
  4require "em_promise"
  5require "lazy_object"
  6require "value_semantics/monkey_patched"
  7
  8require_relative "blather_notify"
  9
 10class Tel
 11	def initialize(str)
 12		@tel = if str.is_a? Tel
 13			str.to_s
 14		else
 15			"+1#{str.sub(/^\+?1?/, '')}"
 16		end
 17	end
 18
 19	def to_s
 20		@tel
 21	end
 22
 23	def ==(other)
 24		to_s == other.to_s
 25	end
 26end
 27
 28class PortingStepRepo
 29	# Any thing that debounces messages must happen inside this.
 30	# The porting logic will pass everything in every time.
 31	class Outputs
 32		def info(port_id, kind, msg); end
 33
 34		def warn(port_id, kind, msg); end
 35
 36		def error(port_id, kind, e_or_msg); end
 37
 38		def to_customer(port_id, kind, tel, msg); end
 39	end
 40
 41	value_semantics do
 42		redis             Anything(), default: LazyObject.new { REDIS }
 43		blather_notify    Anything(), default: BlatherNotify
 44		admin_server      Anything(), default: CONFIG[:admin_server]
 45		testing_tel       Anything(), default: CONFIG[:testing_tel]
 46		output            Outputs, default: Outputs.new
 47	end
 48
 49	def find(port)
 50		if_processing(port) do
 51			case port.processing_status
 52			when "FOC"
 53				FOC.new(**to_h).find(port)
 54			when "COMPLETE"
 55				Complete.new(**to_h).find(port)
 56			else
 57				Wait.new(port, output: output)
 58			end
 59		end
 60	end
 61
 62	def if_processing(port)
 63		redis.exists("jmp_port_freeze-#{port.order_id}").then do |v|
 64			next Frozen.new(port, output: output) if v == 1
 65
 66			yield
 67		end
 68	end
 69
 70	class FOC < self
 71		# This is how long we'll wait for a port to move from FOC to COMPLETED
 72		# It's in fractional days because DateTime
 73		GRACE_PERIOD = 15.0 / (24 * 60)
 74
 75		def find(port)
 76			Alert.for(
 77				port,
 78				grace_period: GRACE_PERIOD,
 79				output: output, key: :late_foc,
 80				msg: "⚠ Port is still in FOC state a while past FOC",
 81				real_step: Wait.new(port, output: output)
 82			)
 83		end
 84	end
 85
 86	class Complete < self
 87		# If it's been 35 minutes and the number isn't reachable, a human
 88		# should get involved
 89		GRACE_PERIOD = 35.0 / (24 * 60)
 90
 91		def find(port)
 92			if_not_complete(port) do
 93				exe = blather_notify.command_execution(admin_server, "customer info")
 94				AdminCommand.new(exe: exe, **to_h).find(port).then do |step|
 95					Alert.for(
 96						port,
 97						grace_period: GRACE_PERIOD, output: output,
 98						key: :late_finish, msg: msg(port), real_step: step
 99					)
100				end
101			end
102		end
103
104		def if_not_complete(port)
105			redis.exists("jmp_port_complete-#{port.order_id}").then do |v|
106				next Done.new(port, output: output) if v == 1
107
108				yield
109			end
110		end
111
112		def msg(port)
113			"⚠ Port still hasn't finished. We'll keep trying unless you set redis "\
114				"key `jmp_port_freeze-#{port.order_id}`"
115		end
116
117		class AdminCommand < self
118			def initialize(exe:, **kwargs)
119				@exe = exe
120				super(**kwargs)
121			end
122
123			def to_h
124				super.merge(exe: @exe)
125			end
126
127			def find(port)
128				@exe.fetch_and_submit(q: port.customer_order_id).then do |form|
129					tel = Tel.new(port.billing_telephone_number)
130					if tel == Tel.new(form.tel)
131						GoodNumber.new(**to_h).find(port)
132					else
133						WrongNumber.new(right_number: tel, execution: @exe)
134					end
135				end
136			end
137
138			class GoodNumber < self
139				def find(port)
140					Reachability.new(type: "voice", **to_h).test(port) do
141						Reachability.new(type: "sms", **to_h).test(port) do
142							FinishUp.new(port, redis: redis, output: output)
143						end
144					end
145				end
146
147				class Reachability < self
148					def initialize(type:, **args)
149						@type = type
150						super(**args)
151					end
152
153					def test(port)
154						execute_command(port).then do |response|
155							if response.count == "0"
156								RunTest.new(
157									type: @type, tel: port.billing_telephone_number,
158									**to_h.slice(:blather_notify, :testing_tel, :admin_server)
159								)
160							else
161								yield
162							end
163						end
164					end
165
166					class RunTest
167						# This is here for tests
168						attr_reader :type
169
170						def initialize(
171							type:, tel:, blather_notify:, testing_tel:, admin_server:
172						)
173							@type = type
174							@tel = tel
175							@blather_notify = blather_notify
176							@testing_tel = testing_tel
177							@admin_server = admin_server
178						end
179
180						def perform_next_step
181							@blather_notify.command_execution(@admin_server, "reachability")
182								.fetch_and_submit(
183									tel: @tel, type: @type, reachability_tel: @testing_tel
184								)
185						end
186					end
187
188				protected
189
190					def execute_command(port)
191						blather_notify.command_execution(admin_server, "reachability")
192							.fetch_and_submit(tel: port.billing_telephone_number, type: @type)
193					end
194				end
195
196				class FinishUp
197					MESSAGE = "Hi!  This is JMP support - your number has "\
198					"successfully transferred in to JMP!  All calls/messages "\
199					"will now use your transferred-in number - your old JMP "\
200					"number has been disabled.  Let us know if you have any "\
201					"questions and thanks for using JMP!"
202
203					def initialize(port, redis:, output:)
204						@port = port
205						@redis = redis
206						@output = output
207					end
208
209					def set_key
210						@redis.set(
211							"jmp_port_complete-#{@port.order_id}",
212							DateTime.now.iso8601,
213							"EX",
214							60 * 60 * 24 * 2 ### 2 Days should be enough to not see it listed
215						)
216					end
217
218					def perform_next_step
219						set_key.then do
220							EMPromise.all([
221								@output.info(@port.order_id, :complete, "Port Complete!"),
222								@output.to_customer(
223									@port.order_id, :complete,
224									Tel.new(@port.billing_telephone_number), MESSAGE
225								)
226							])
227						end
228					end
229				end
230			end
231
232			class WrongNumber
233				def initialize(right_number:, execution:)
234					@right_number = right_number
235					@exe = execution
236				end
237
238				def perform_next_step
239					@exe.fetch_and_submit(action: "number_change").then do |_form|
240						@exe.fetch_and_submit(new_tel: @right_number, should_delete: "true")
241					end
242				end
243			end
244		end
245	end
246
247	# This doesn't do anything and just waits for something to happen later
248	class Wait
249		def initialize(port, output:)
250			@port = port
251			@output = output
252		end
253
254		def perform_next_step
255			@output.info(@port.order_id, :wait, "Waiting...")
256		end
257	end
258
259	# This also doesn't do anything but is more content about it
260	class Done
261		def initialize(port, output:)
262			@port = port
263			@output = output
264		end
265
266		def perform_next_step
267			@output.info(@port.order_id, :done, "Done.")
268		end
269	end
270
271	# This also also doesn't do anything but is intentional
272	class Frozen
273		def initialize(port, output:)
274			@port = port
275			@output = output
276		end
277
278		def perform_next_step
279			@output.info(@port.order_id, :frozen, "Frozen.")
280		end
281	end
282
283	# This class sends and error to the human to check things out
284	class Alert
285		def self.for(port, grace_period:, real_step:, **args)
286			if (DateTime.now - port.actual_foc_date) > grace_period
287				new(port, real_step: real_step, **args)
288			else
289				real_step
290			end
291		end
292
293		# For tests
294		attr_reader :key
295		attr_reader :real_step
296
297		def initialize(port, real_step:, output:, msg:, key:)
298			@port = port
299			@real_step = real_step
300			@output = output
301			@msg = msg
302			@key = key
303		end
304
305		def perform_next_step
306			@output.warn(@port.order_id, @key, @msg).then {
307				@real_step.perform_next_step
308			}
309		end
310	end
311end