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"
16require_relative "./welcome_message"
17
18class Registration
19 def self.for(customer, google_play_userid, tel_selections)
20 if (reg = customer.registered?)
21 Registered.for(customer, reg.phone)
22 else
23 tel_selections[customer.jid].then(&:choose_tel).then do |tel|
24 BandwidthTnReservationRepo.new.ensure(customer, tel)
25 FinishOrStartActivation.for(customer, google_play_userid, tel)
26 end
27 end
28 end
29
30 class Registered
31 def self.for(customer, tel)
32 jid = ProxiedJID.new(customer.jid).unproxied
33 if jid.domain == CONFIG[:onboarding_domain]
34 FinishOnboarding.for(customer, tel)
35 else
36 new(tel)
37 end
38 end
39
40 def initialize(tel)
41 @tel = tel
42 end
43
44 def write
45 Command.finish("You are already registered with JMP number #{@tel}")
46 end
47 end
48
49 class FinishOrStartActivation
50 def self.for(customer, google_play_userid, tel)
51 if customer.active?
52 Finish.new(customer, tel)
53 elsif customer.balance >= CONFIG[:activation_amount_accept]
54 BillPlan.new(customer, tel)
55 else
56 new(customer, google_play_userid, tel)
57 end
58 end
59
60 def initialize(customer, google_play_userid, tel)
61 @customer = customer
62 @tel = tel
63 @google_play_userid = google_play_userid
64 end
65
66 def write
67 Command.reply { |reply|
68 reply.allowed_actions = [:next]
69 reply.note_type = :info
70 reply.note_text = File.read("#{__dir__}/../fup.txt")
71 }.then { Activation.for(@customer, @google_play_userid, @tel).write }
72 end
73 end
74
75 class Activation
76 def self.for(customer, google_play_userid, tel)
77 jid = ProxiedJID.new(customer.jid).unproxied
78 if CONFIG[:approved_domains].key?(jid.domain.to_sym)
79 Allow.for(customer, tel, jid)
80 elsif google_play_userid
81 GooglePlay.new(customer, google_play_userid, tel)
82 else
83 new(customer, tel)
84 end
85 end
86
87 def initialize(customer, tel)
88 @customer = customer
89 @tel = tel
90 @invites = InvitesRepo.new(DB, REDIS)
91 end
92
93 attr_reader :customer, :tel
94
95 def form
96 FormTemplate.render("registration/activate", tel: tel)
97 end
98
99 def write
100 Command.reply { |reply|
101 reply.allowed_actions = [:next]
102 reply.command << form
103 }.then(&method(:next_step))
104 end
105
106 def next_step(iq)
107 code = iq.form.field("code")&.value&.to_s
108 save_customer_plan(iq).then {
109 finish_if_valid_invite(code)
110 }.catch_only(InvitesRepo::Invalid) do
111 @invites.stash_code(customer.customer_id, code).then do
112 Payment.for(iq, @customer, @tel).then(&:write)
113 end
114 end
115 end
116
117 protected
118
119 def finish_if_valid_invite(code)
120 @invites.claim_code(@customer.customer_id, code) {
121 @customer.activate_plan_starting_now
122 }.then do
123 Finish.new(@customer, @tel).write
124 end
125 end
126
127 def save_customer_plan(iq)
128 plan_name = iq.form.field("plan_name").value.to_s
129 @customer = @customer.with_plan(plan_name)
130 @customer.save_plan!
131 end
132
133 class GooglePlay
134 def initialize(customer, google_play_userid, tel)
135 @customer = customer
136 @google_play_userid = google_play_userid
137 @tel = tel
138 @invites = InvitesRepo.new(DB, REDIS)
139 end
140
141 def used
142 REDIS.sismember("google_play_userids", @google_play_userid)
143 end
144
145 def form
146 FormTemplate.render(
147 "registration/google_play",
148 tel: @tel
149 )
150 end
151
152 def write
153 used.then do |u|
154 next Activation.for(@customer, nil, @tel).write if u.to_s == "1"
155
156 Command.reply { |reply|
157 reply.allowed_actions = [:next]
158 reply.command << form
159 }.then(&method(:activate)).then do
160 Finish.new(@customer, @tel).write
161 end
162 end
163 end
164
165 def activate(iq)
166 REDIS.sadd("google_play_userids", @google_play_userid).then {
167 plan_name = iq.form.field("plan_name").value.to_s
168 @customer = @customer.with_plan(plan_name)
169 @customer.activate_plan_starting_now
170 }.then do
171 use_referral_code(iq.form.field("code")&.value&.to_s)
172 end
173 end
174
175 protected
176
177 def use_referral_code(code)
178 @invites.claim_code(@customer.customer_id, code) {
179 @customer.extend_plan
180 }.catch_only(InvitesRepo::Invalid) do
181 @invites.stash_code(customer.customer_id, code)
182 end
183 end
184 end
185
186 class Allow < Activation
187 def self.for(customer, tel, jid)
188 credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
189 new(customer, tel, credit_to)
190 end
191
192 def initialize(customer, tel, credit_to)
193 super(customer, tel)
194 @credit_to = credit_to
195 end
196
197 def form
198 FormTemplate.render(
199 "registration/allow",
200 tel: tel,
201 domain: customer.jid.domain
202 )
203 end
204
205 def next_step(iq)
206 plan_name = iq.form.field("plan_name").value.to_s
207 @customer = customer.with_plan(plan_name)
208 EMPromise.resolve(nil).then { activate }.then do
209 Finish.new(customer, tel).write
210 end
211 end
212
213 protected
214
215 def activate
216 DB.transaction do
217 if @credit_to
218 DB.exec(<<~SQL, [@credit_to, customer.customer_id])
219 INSERT INTO invites (creator_id, used_by_id, used_at)
220 VALUES ($1, $2, LOCALTIMESTAMP)
221 SQL
222 end
223 @customer.activate_plan_starting_now
224 end
225 end
226 end
227 end
228
229 module Payment
230 def self.kinds
231 @kinds ||= {}
232 end
233
234 def self.for(iq, customer, tel, final_message: nil, finish: Finish)
235 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
236 raise "Invalid activation method"
237 }.call(customer, tel, final_message: final_message, finish: finish)
238 end
239
240 class Bitcoin
241 Payment.kinds[:bitcoin] = method(:new)
242
243 THIRTY_DAYS = 60 * 60 * 24 * 30
244
245 def initialize(customer, tel, final_message: nil, **)
246 @customer = customer
247 @customer_id = customer.customer_id
248 @tel = tel
249 @final_message = final_message
250 end
251
252 attr_reader :customer_id, :tel
253
254 def save
255 REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel)
256 end
257
258 def form(rate, addr)
259 amount = CONFIG[:activation_amount] / rate
260
261 FormTemplate.render(
262 "registration/btc",
263 amount: amount,
264 addr: addr,
265 final_message: @final_message
266 )
267 end
268
269 def write
270 EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
271 Command.reply { |reply|
272 reply.allowed_actions = [:prev]
273 reply.status = :canceled
274 reply.command << form(rate, addr)
275 }.then(&method(:handle_possible_prev))
276 end
277 end
278
279 protected
280
281 def handle_possible_prev(iq)
282 raise "Action not allowed" unless iq.prev?
283
284 Activation.for(@customer, nil, @tel).then(&:write)
285 end
286
287 def addr_and_rate
288 EMPromise.all([
289 @customer.btc_addresses.then { |addrs|
290 addrs.first || @customer.add_btc_address
291 },
292 BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
293 ])
294 end
295 end
296
297 class CreditCard
298 Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
299
300 def self.for(in_customer, tel, finish: Finish, **)
301 reload_customer(in_customer).then do |(customer, payment_methods)|
302 if customer.balance >= CONFIG[:activation_amount_accept]
303 next BillPlan.new(customer, tel, finish: finish)
304 end
305
306 if (method = payment_methods.default_payment_method)
307 next Activate.new(customer, method, tel, finish: finish)
308 end
309
310 new(customer, tel, finish: finish)
311 end
312 end
313
314 def self.reload_customer(customer)
315 EMPromise.all([
316 Command.execution.customer_repo.find(customer.customer_id),
317 customer.payment_methods
318 ])
319 end
320
321 def initialize(customer, tel, finish: Finish)
322 @customer = customer
323 @tel = tel
324 @finish = finish
325 end
326
327 def oob(reply)
328 oob = OOB.find_or_create(reply.command)
329 oob.url = CONFIG[:credit_card_url].call(
330 reply.to.stripped.to_s.gsub("\\", "%5C"),
331 @customer.customer_id
332 ) + "&amount=#{CONFIG[:activation_amount]}"
333 oob.desc = "Add credit card, save, then next here to continue"
334 oob
335 end
336
337 def write
338 Command.reply { |reply|
339 reply.allowed_actions = [:next, :prev]
340 toob = oob(reply)
341 reply.note_type = :info
342 reply.note_text = "#{toob.desc}: #{toob.url}"
343 }.then do |iq|
344 next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
345
346 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
347 end
348 end
349
350 class Activate
351 def initialize(customer, payment_method, tel, finish: Finish)
352 @customer = customer
353 @payment_method = payment_method
354 @tel = tel
355 @finish = finish
356 end
357
358 def write
359 CreditCardSale.create(
360 @customer,
361 amount: CONFIG[:activation_amount],
362 payment_method: @payment_method
363 ).then(
364 ->(_) { sold },
365 ->(_) { declined }
366 )
367 end
368
369 protected
370
371 def sold
372 BillPlan.new(@customer, @tel, finish: @finish).write
373 end
374
375 DECLINE_MESSAGE =
376 "Your bank declined the transaction. " \
377 "Often this happens when a person's credit card " \
378 "is a US card that does not support international " \
379 "transactions, as JMP is not based in the USA, though " \
380 "we do support transactions in USD.\n\n" \
381 "You may add another card"
382
383 def decline_oob(reply)
384 oob = OOB.find_or_create(reply.command)
385 oob.url = CONFIG[:credit_card_url].call(
386 reply.to.stripped.to_s.gsub("\\", "%5C"),
387 @customer.customer_id
388 ) + "&amount=#{CONFIG[:activation_amount]}"
389 oob.desc = DECLINE_MESSAGE
390 oob
391 end
392
393 def declined
394 Command.reply { |reply|
395 reply_oob = decline_oob(reply)
396 reply.allowed_actions = [:next]
397 reply.note_type = :error
398 reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
399 }.then do
400 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
401 end
402 end
403 end
404 end
405
406 class InviteCode
407 Payment.kinds[:code] = method(:new)
408
409 FIELDS = [{
410 var: "code",
411 type: "text-single",
412 label: "Your referral code",
413 required: true
414 }].freeze
415
416 def initialize(customer, tel, error: nil, **)
417 @customer = customer
418 @tel = tel
419 @error = error
420 end
421
422 def add_form(reply)
423 form = reply.form
424 form.type = :form
425 form.title = "Enter Referral Code"
426 form.instructions = @error if @error
427 form.fields = FIELDS
428 end
429
430 def write
431 Command.reply { |reply|
432 reply.allowed_actions = [:next, :prev]
433 add_form(reply)
434 }.then(&method(:parse))
435 end
436
437 def parse(iq)
438 return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
439
440 verify(iq.form.field("code")&.value&.to_s).then {
441 Finish.new(@customer, @tel)
442 }.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
443 end
444
445 protected
446
447 def invalid_code(e)
448 InviteCode.new(@customer, @tel, error: e.message)
449 end
450
451 def customer_id
452 @customer.customer_id
453 end
454
455 def verify(code)
456 InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) do
457 @customer.activate_plan_starting_now
458 end
459 end
460 end
461
462 class Mail
463 Payment.kinds[:mail] = method(:new)
464
465 def initialize(customer, tel, final_message: nil, **)
466 @customer = customer
467 @tel = tel
468 @final_message = final_message
469 end
470
471 def form
472 FormTemplate.render(
473 "registration/mail",
474 currency: @customer.currency,
475 final_message: @final_message,
476 **onboarding_extras
477 )
478 end
479
480 def onboarding_extras
481 jid = ProxiedJID.new(@customer.jid).unproxied
482 return {} unless jid.domain == CONFIG[:onboarding_domain]
483
484 {
485 customer_id: @customer.customer_id,
486 in_note: "Customer ID"
487 }
488 end
489
490 def write
491 Command.reply { |reply|
492 reply.allowed_actions = [:prev]
493 reply.status = :canceled
494 reply.command << form
495 }.then { |iq|
496 raise "Action not allowed" unless iq.prev?
497
498 Activation.for(@customer, nil, @tel).then(&:write)
499 }
500 end
501 end
502 end
503
504 class BillPlan
505 def initialize(customer, tel, finish: Finish)
506 @customer = customer
507 @tel = tel
508 @finish = finish
509 end
510
511 def write
512 @customer.bill_plan(note: "Bill #{@tel} for first month").then do
513 @finish.new(@customer, @tel).write
514 end
515 end
516 end
517
518 class Finish
519 def initialize(customer, tel)
520 @customer = customer
521 @tel = tel
522 end
523
524 def write
525 BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
526 BandwidthTNOrder.create(
527 @tel,
528 customer_order_id: @customer.customer_id,
529 reservation_id: rid
530 ).then(&:poll).then(
531 ->(_) { customer_active_tel_purchased },
532 method(:number_purchase_error)
533 )
534 end
535 end
536
537 protected
538
539 def number_purchase_error(e)
540 Command.log.error "number_purchase_error", e
541 TEL_SELECTIONS.delete(@customer.jid).then {
542 TelSelections::ChooseTel.new.choose_tel(
543 error: "The JMP number #{@tel} is no longer available."
544 )
545 }.then { |tel| Finish.new(@customer, tel).write }
546 end
547
548 def raise_setup_error(e)
549 Command.log.error "@customer.register! failed", e
550 Command.finish(
551 "There was an error setting up your number, " \
552 "please contact JMP support.",
553 type: :error
554 )
555 end
556
557 def put_default_fwd
558 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
559 uri: "xmpp:#{@customer.jid}",
560 voicemail_enabled: true
561 ))
562 end
563
564 def customer_active_tel_purchased
565 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
566 EMPromise.all([
567 REDIS.del("pending_tel_for-#{@customer.jid}"),
568 put_default_fwd
569 ])
570 }.then do
571 FinishOnboarding.for(@customer, @tel).then(&:write)
572 end
573 end
574 end
575
576 module FinishOnboarding
577 def self.for(customer, tel, db: LazyObject.new { DB })
578 jid = ProxiedJID.new(customer.jid).unproxied
579 if jid.domain == CONFIG[:onboarding_domain]
580 Snikket.for(customer, tel, db: db)
581 else
582 NotOnboarding.new(customer, tel)
583 end
584 end
585
586 class Snikket
587 def self.for(customer, tel, db:)
588 ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
589 if is.empty?
590 new(customer, tel, db: db)
591 else
592 GetInvite.for(is[0])
593 end
594 end
595 end
596
597 def initialize(customer, tel, error: nil, db:)
598 @customer = customer
599 @tel = tel
600 @error = error
601 @db = db
602 end
603
604 ACTION_VAR = "http://jabber.org/protocol/commands#actions"
605
606 def write
607 Command.reply { |reply|
608 reply.allowed_actions = [:next]
609 reply.command << form
610 }.then do |iq|
611 if iq.form.field(ACTION_VAR)&.value == "custom_domain"
612 CustomDomain.new(@tel).write
613 else
614 launch("#{iq.form.field('subdomain')&.value}.snikket.chat")
615 end
616 end
617 end
618
619 def form
620 FormTemplate.render(
621 "registration/snikket",
622 tel: @tel,
623 error: @error
624 )
625 end
626
627 def launch(domain)
628 IQ_MANAGER.write(::Snikket::Launch.new(
629 nil, CONFIG[:snikket_hosting_api], domain: domain
630 )).then { |launched|
631 save_instance_and_wait(domain, launched)
632 }.catch { |e|
633 next EMPromise.reject(e) unless e.respond_to?(:text)
634
635 Snikket.new(@customer, @tel, error: e.text, db: @db).write
636 }
637 end
638
639 def save_instance_and_wait(domain, launched)
640 instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
641 ::Snikket::Repo.new(db: @db).put(instance).then do
642 GetInvite.for(instance).then(&:write)
643 end
644 end
645
646 class GetInvite
647 def self.for(instance)
648 instance.fetch_invite.then do |xmpp_uri|
649 if xmpp_uri
650 GoToInvite.new(xmpp_uri)
651 else
652 new(instance)
653 end
654 end
655 end
656
657 def initialize(instance)
658 @instance = instance
659 end
660
661 def write
662 Command.reply { |reply|
663 reply.allowed_actions = [:next]
664 reply.command << FormTemplate.render(
665 "registration/snikket_wait",
666 domain: @instance.domain
667 )
668 }.then { GetInvite.for(@instance).then(&:write) }
669 end
670 end
671
672 class GoToInvite
673 def initialize(xmpp_uri)
674 @xmpp_uri = xmpp_uri
675 end
676
677 def write
678 Command.finish do |reply|
679 oob = OOB.find_or_create(reply.command)
680 oob.url = @xmpp_uri
681 end
682 end
683 end
684 end
685
686 class CustomDomain
687 def initialize(tel)
688 @tel = tel
689 end
690
691 CONTACT_SUPPORT =
692 "Please contact JMP support to set up " \
693 "an instance on an existing domain."
694
695 def write
696 Command.reply { |reply|
697 reply.allowed_actions = [:prev]
698 reply.status = :canceled
699 reply.note_type = :info
700 reply.note_text = CONTACT_SUPPORT
701 }.then do |iq|
702 raise "Action not allowed" unless iq.prev?
703
704 Snikket.new(@tel).write
705 end
706 end
707 end
708
709 class NotOnboarding
710 def initialize(customer, tel)
711 @customer = customer
712 @tel = tel
713 end
714
715 def write
716 WelcomeMessage.new(@customer, @tel).welcome
717 Command.finish("Your JMP account has been activated as #{@tel}")
718 end
719 end
720 end
721end