1# frozen_string_literal: true
2
3require "erb"
4require "ruby-bandwidth-iris"
5require "securerandom"
6
7require_relative "./alt_top_up_form"
8require_relative "./bandwidth_tn_order"
9require_relative "./bandwidth_tn_reservation_repo"
10require_relative "./command"
11require_relative "./em"
12require_relative "./invites_repo"
13require_relative "./oob"
14require_relative "./proxied_jid"
15require_relative "./tel_selections"
16
17class Registration
18 def self.for(customer, tel_selections)
19 if (reg = customer.registered?)
20 Registered.new(reg.phone)
21 else
22 tel_selections[customer.jid].then(&:choose_tel).then do |tel|
23 BandwidthTnReservationRepo.new.ensure(customer, tel)
24 FinishOrStartActivation.for(customer, tel)
25 end
26 end
27 end
28
29 class Registered
30 def initialize(tel)
31 @tel = tel
32 end
33
34 def write
35 Command.finish("You are already registered with JMP number #{@tel}")
36 end
37 end
38
39 class FinishOrStartActivation
40 def self.for(customer, tel)
41 if customer.active?
42 Finish.new(customer, tel)
43 elsif customer.balance >= CONFIG[:activation_amount_accept]
44 BillPlan.new(customer, tel)
45 else
46 new(customer, tel)
47 end
48 end
49
50 def initialize(customer, tel)
51 @customer = customer
52 @tel = tel
53 end
54
55 def write
56 Command.reply { |reply|
57 reply.allowed_actions = [:next]
58 reply.note_type = :info
59 reply.note_text = File.read("#{__dir__}/../fup.txt")
60 }.then { Activation.for(@customer, @tel).write }
61 end
62 end
63
64 class Activation
65 def self.for(customer, tel)
66 jid = ProxiedJID.new(customer.jid).unproxied
67 if CONFIG[:approved_domains].key?(jid.domain.to_sym)
68 Allow.for(customer, tel, jid)
69 else
70 new(customer, tel)
71 end
72 end
73
74 def initialize(customer, tel)
75 @customer = customer
76 @tel = tel
77 end
78
79 attr_reader :customer, :tel
80
81 def form(center)
82 FormTemplate.render(
83 "registration/activate",
84 tel: tel,
85 rate_center: center
86 )
87 end
88
89 def write
90 rate_center.then { |center|
91 Command.reply do |reply|
92 reply.allowed_actions = [:next]
93 reply.command << form(center)
94 end
95 }.then(&method(:next_step))
96 end
97
98 def next_step(iq)
99 EMPromise.resolve(nil).then {
100 Payment.for(iq, customer, tel)
101 }.then(&:write)
102 end
103
104 protected
105
106 def rate_center
107 EM.promise_fiber {
108 center = BandwidthIris::Tn.get(tel).get_rate_center
109 "#{center[:rate_center]}, #{center[:state]}"
110 }.catch { nil }
111 end
112
113 class Allow < Activation
114 def self.for(customer, tel, jid)
115 credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
116 new(customer, tel, credit_to)
117 end
118
119 def initialize(customer, tel, credit_to)
120 super(customer, tel)
121 @credit_to = credit_to
122 end
123
124 def form(center)
125 FormTemplate.render(
126 "registration/allow",
127 tel: tel,
128 rate_center: center,
129 domain: customer.jid.domain
130 )
131 end
132
133 def next_step(iq)
134 plan_name = iq.form.field("plan_name").value.to_s
135 @customer = customer.with_plan(plan_name)
136 EMPromise.resolve(nil).then { activate }.then do
137 Finish.new(customer, tel).write
138 end
139 end
140
141 protected
142
143 def activate
144 DB.transaction do
145 if @credit_to
146 DB.exec(<<~SQL, [@credit_to, customer.customer_id])
147 INSERT INTO invites (creator_id, used_by_id, used_at)
148 VALUES ($1, $2, LOCALTIMESTAMP)
149 SQL
150 end
151 @customer.activate_plan_starting_now
152 end
153 end
154 end
155 end
156
157 module Payment
158 def self.kinds
159 @kinds ||= {}
160 end
161
162 def self.for(iq, customer, tel, final_message: nil, finish: Finish)
163 plan_name = iq.form.field("plan_name").value.to_s
164 customer = customer.with_plan(plan_name)
165 customer.save_plan!.then do
166 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
167 raise "Invalid activation method"
168 }.call(customer, tel, final_message: final_message, finish: finish)
169 end
170 end
171
172 class Bitcoin
173 Payment.kinds[:bitcoin] = method(:new)
174
175 THIRTY_DAYS = 60 * 60 * 24 * 30
176
177 def initialize(customer, tel, final_message: nil, **)
178 @customer = customer
179 @customer_id = customer.customer_id
180 @tel = tel
181 @final_message = final_message
182 end
183
184 attr_reader :customer_id, :tel
185
186 def save
187 REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel)
188 end
189
190 def note_text(rate, addr)
191 amount = CONFIG[:activation_amount] / rate
192 <<~NOTE
193 Activate your account by sending at least #{'%.6f' % amount} BTC to
194 #{addr}
195
196 You will receive a notification when your payment is complete.
197 NOTE
198 end
199
200 def write
201 EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
202 Command.reply { |reply|
203 reply.allowed_actions = [:prev]
204 reply.status = :canceled
205 reply.note_type = :info
206 reply.note_text = note_text(rate, addr) + @final_message.to_s
207 }.then(&method(:handle_possible_prev))
208 end
209 end
210
211 protected
212
213 def handle_possible_prev(iq)
214 raise "Action not allowed" unless iq.prev?
215
216 Activation.for(@customer, @tel).then(&:write)
217 end
218
219 def addr_and_rate
220 EMPromise.all([
221 @customer.btc_addresses.then { |addrs|
222 addrs.first || @customer.add_btc_address
223 },
224 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
225 ])
226 end
227 end
228
229 class CreditCard
230 Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
231
232 def self.for(in_customer, tel, finish: Finish, **)
233 reload_customer(in_customer).then do |(customer, payment_methods)|
234 if customer.balance >= CONFIG[:activation_amount_accept]
235 next BillPlan.new(customer, tel, finish: finish)
236 end
237
238 if (method = payment_methods.default_payment_method)
239 next Activate.new(customer, method, tel, finish: finish)
240 end
241
242 new(customer, tel, finish: finish)
243 end
244 end
245
246 def self.reload_customer(customer)
247 EMPromise.all([
248 Command.execution.customer_repo.find(customer.customer_id),
249 customer.payment_methods
250 ])
251 end
252
253 def initialize(customer, tel, finish: Finish)
254 @customer = customer
255 @tel = tel
256 @finish = finish
257 end
258
259 def oob(reply)
260 oob = OOB.find_or_create(reply.command)
261 oob.url = CONFIG[:credit_card_url].call(
262 reply.to.stripped.to_s.gsub("\\", "%5C"),
263 @customer.customer_id
264 ) + "&amount=#{CONFIG[:activation_amount]}"
265 oob.desc = "Add credit card, save, then next here to continue"
266 oob
267 end
268
269 def write
270 Command.reply { |reply|
271 reply.allowed_actions = [:next, :prev]
272 toob = oob(reply)
273 reply.note_type = :info
274 reply.note_text = "#{toob.desc}: #{toob.url}"
275 }.then do |iq|
276 next Activation.for(@customer, @tel).then(&:write) if iq.prev?
277
278 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
279 end
280 end
281
282 class Activate
283 def initialize(customer, payment_method, tel, finish: Finish)
284 @customer = customer
285 @payment_method = payment_method
286 @tel = tel
287 @finish = finish
288 end
289
290 def write
291 CreditCardSale.create(
292 @customer,
293 amount: CONFIG[:activation_amount],
294 payment_method: @payment_method
295 ).then(
296 ->(_) { sold },
297 ->(_) { declined }
298 )
299 end
300
301 protected
302
303 def sold
304 BillPlan.new(@customer, @tel, finish: @finish).write
305 end
306
307 DECLINE_MESSAGE =
308 "Your bank declined the transaction. " \
309 "Often this happens when a person's credit card " \
310 "is a US card that does not support international " \
311 "transactions, as JMP is not based in the USA, though " \
312 "we do support transactions in USD.\n\n" \
313 "You may add another card"
314
315 def decline_oob(reply)
316 oob = OOB.find_or_create(reply.command)
317 oob.url = CONFIG[:credit_card_url].call(
318 reply.to.stripped.to_s.gsub("\\", "%5C"),
319 @customer.customer_id
320 ) + "&amount=#{CONFIG[:activation_amount]}"
321 oob.desc = DECLINE_MESSAGE
322 oob
323 end
324
325 def declined
326 Command.reply { |reply|
327 reply_oob = decline_oob(reply)
328 reply.allowed_actions = [:next]
329 reply.note_type = :error
330 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
331 }.then do
332 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
333 end
334 end
335 end
336 end
337
338 class InviteCode
339 Payment.kinds[:code] = method(:new)
340
341 FIELDS = [{
342 var: "code",
343 type: "text-single",
344 label: "Your referral code",
345 required: true
346 }].freeze
347
348 def initialize(customer, tel, error: nil, **)
349 @customer = customer
350 @tel = tel
351 @error = error
352 end
353
354 def add_form(reply)
355 form = reply.form
356 form.type = :form
357 form.title = "Enter Referral Code"
358 form.instructions = @error if @error
359 form.fields = FIELDS
360 end
361
362 def write
363 Command.reply { |reply|
364 reply.allowed_actions = [:next, :prev]
365 add_form(reply)
366 }.then(&method(:parse))
367 end
368
369 def parse(iq)
370 return Activation.for(@customer, @tel).then(&:write) if iq.prev?
371
372 guard_too_many_tries.then {
373 verify(iq.form.field("code")&.value&.to_s)
374 }.then {
375 Finish.new(@customer, @tel)
376 }.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
377 end
378
379 protected
380
381 def guard_too_many_tries
382 REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
383 raise InvitesRepo::Invalid, "Too many wrong attempts" if t.to_i > 10
384 end
385 end
386
387 def invalid_code(e)
388 EMPromise.all([
389 REDIS.incr("jmp_invite_tries-#{customer_id}").then do
390 REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
391 end,
392 InviteCode.new(@customer, @tel, error: e.message)
393 ]).then(&:last)
394 end
395
396 def customer_id
397 @customer.customer_id
398 end
399
400 def verify(code)
401 InvitesRepo.new(DB).claim_code(customer_id, code) do
402 @customer.activate_plan_starting_now
403 end
404 end
405 end
406
407 class Mail
408 Payment.kinds[:mail] = method(:new)
409
410 def initialize(customer, tel, final_message: nil, **)
411 @customer = customer
412 @tel = tel
413 @final_message = final_message
414 end
415
416 def form
417 FormTemplate.render(
418 "registration/mail",
419 currency: @customer.currency,
420 final_message: @final_message
421 )
422 end
423
424 def write
425 Command.reply { |reply|
426 reply.allowed_actions = [:prev]
427 reply.status = :canceled
428 reply.command << form
429 }.then { |iq|
430 raise "Action not allowed" unless iq.prev?
431
432 Activation.for(@customer, @tel).then(&:write)
433 }
434 end
435 end
436 end
437
438 class BillPlan
439 def initialize(customer, tel, finish: Finish)
440 @customer = customer
441 @tel = tel
442 @finish = finish
443 end
444
445 def write
446 @customer.bill_plan(note: "Bill #{@tel} for first month").then do
447 @finish.new(@customer, @tel).write
448 end
449 end
450 end
451
452 class Finish
453 def initialize(customer, tel)
454 @customer = customer
455 @tel = tel
456 end
457
458 def write
459 BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
460 BandwidthTNOrder.create(
461 @tel,
462 customer_order_id: @customer.customer_id,
463 reservation_id: rid
464 ).then(&:poll).then(
465 ->(_) { customer_active_tel_purchased },
466 method(:number_purchase_error)
467 )
468 end
469 end
470
471 protected
472
473 def number_purchase_error(e)
474 Command.log.error "number_purchase_error", e
475 TEL_SELECTIONS.delete(@customer.jid).then {
476 TelSelections::ChooseTel.new.choose_tel(
477 error: "The JMP number #{@tel} is no longer available."
478 )
479 }.then { |tel| Finish.new(@customer, tel).write }
480 end
481
482 def raise_setup_error(e)
483 Command.log.error "@customer.register! failed", e
484 Command.finish(
485 "There was an error setting up your number, " \
486 "please contact JMP support.",
487 type: :error
488 )
489 end
490
491 def customer_active_tel_purchased
492 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
493 EMPromise.all([
494 REDIS.del("pending_tel_for-#{@customer.jid}"),
495 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
496 uri: "xmpp:#{@customer.jid}", voicemail_enabled: true
497 ))
498 ])
499 }.then do
500 Command.finish("Your JMP account has been activated as #{@tel}")
501 end
502 end
503 end
504end