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