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 "./parent_code_repo"
15require_relative "./proxied_jid"
16require_relative "./tel_selections"
17require_relative "./welcome_message"
18
19class Registration
20 def self.for(customer, google_play_userid, tel_selections)
21 if (reg = customer.registered?)
22 Registered.for(customer, reg.phone)
23 else
24 tel_selections[customer.jid].then(&:choose_tel).then do |tel|
25 reserve_and_continue(tel_selections, customer, tel).then do
26 FinishOrStartActivation.for(customer, google_play_userid, tel)
27 end
28 end
29 end
30 end
31
32 def self.reserve_and_continue(tel_selections, customer, tel)
33 tel.reserve(customer).catch do
34 tel_selections.delete(customer.jid).then {
35 tel_selections[customer.jid]
36 }.then { |choose|
37 choose.choose_tel(
38 error: "The JMP number #{tel} is no longer available."
39 )
40 }.then { |n_tel| reserve_and_continue(tel_selections, customer, n_tel) }
41 end
42 end
43
44 def self.guard_onboarding_subaccounts(customer)
45 customer_domain = ProxiedJID.new(customer.jid).domain
46 return unless customer_domain == CONFIG[:onboarding_domain]
47
48 raise "Please create a new Jabber ID before creating a subaccount."
49 end
50
51 class Registered
52 def self.for(customer, tel)
53 jid = ProxiedJID.new(customer.jid).unproxied
54 if jid.domain == CONFIG[:onboarding_domain]
55 FinishOnboarding.for(customer, tel)
56 else
57 new(tel)
58 end
59 end
60
61 def initialize(tel)
62 @tel = tel
63 end
64
65 def write
66 Command.finish("You are already registered with JMP number #{@tel}")
67 end
68 end
69
70 class FinishOrStartActivation
71 def self.for(customer, google_play_userid, tel)
72 if customer.active?
73 Finish.new(customer, tel)
74 elsif customer.balance >= CONFIG[:activation_amount_accept]
75 BillPlan.new(customer, tel)
76 else
77 new(customer, google_play_userid, tel)
78 end
79 end
80
81 def initialize(customer, google_play_userid, tel)
82 @customer = customer
83 @tel = tel
84 @google_play_userid = google_play_userid
85 end
86
87 def write
88 Command.reply { |reply|
89 reply.allowed_actions = [:next]
90 reply.note_type = :info
91 reply.note_text = File.read("#{__dir__}/../fup.txt")
92 }.then { Activation.for(@customer, @google_play_userid, @tel).write }
93 end
94 end
95
96 class Activation
97 def self.for(customer, google_play_userid, tel)
98 jid = ProxiedJID.new(customer.jid).unproxied
99 if CONFIG[:approved_domains].key?(jid.domain.to_sym)
100 Allow.for(customer, tel, jid)
101 elsif google_play_userid
102 GooglePlay.new(customer, google_play_userid, tel)
103 else
104 new(customer, tel)
105 end
106 end
107
108 def initialize(customer, tel)
109 @customer = customer
110 @tel = tel
111 @invites = InvitesRepo.new(DB, REDIS)
112 end
113
114 attr_reader :customer, :tel
115
116 def form
117 FormTemplate.render("registration/activate", tel: tel)
118 end
119
120 def write
121 Command.reply { |reply|
122 reply.allowed_actions = [:next]
123 reply.command << form
124 }.then(&method(:next_step))
125 end
126
127 def next_step(iq)
128 code = iq.form.field("code")&.value&.to_s
129 save_customer_plan(iq, code).then {
130 finish_if_valid_invite(code)
131 }.catch_only(InvitesRepo::Invalid) do
132 @invites.stash_code(customer.customer_id, code).then do
133 Payment.for(iq, @customer, @tel).then(&:write)
134 end
135 end
136 end
137
138 protected
139
140 def finish_if_valid_invite(code)
141 @invites.claim_code(@customer.customer_id, code) {
142 @customer.activate_plan_starting_now
143 }.then do
144 Finish.new(@customer, @tel).write
145 end
146 end
147
148 def save_customer_plan(iq, code)
149 Registration.guard_onboarding_subaccounts(@customer)
150
151 ParentCodeRepo.new(redis: REDIS, db: DB).find(code).then do |parent|
152 plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
153 @customer = @customer.with_plan(plan.name, parent_customer_id: parent)
154 @customer.save_plan!
155 end
156 end
157
158 class GooglePlay
159 def initialize(customer, google_play_userid, tel)
160 @customer = customer
161 @google_play_userid = google_play_userid
162 @tel = tel
163 @invites = InvitesRepo.new(DB, REDIS)
164 @parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB)
165 end
166
167 def used
168 REDIS.sismember("google_play_userids", @google_play_userid)
169 end
170
171 def form
172 FormTemplate.render(
173 "registration/google_play",
174 tel: @tel
175 )
176 end
177
178 def write
179 used.then do |u|
180 next Activation.for(@customer, nil, @tel).write if u.to_s == "1"
181
182 Command.reply { |reply|
183 reply.allowed_actions = [:next]
184 reply.command << form
185 }.then(&method(:activate)).then do
186 Finish.new(@customer, @tel).write
187 end
188 end
189 end
190
191 def activate(iq)
192 plan = Plan.for_registration(iq.form.field("plan_name").value)
193 code = iq.form.field("code")&.value
194 EMPromise.all([
195 @parent_code_repo.find(code),
196 REDIS.sadd("google_play_userids", @google_play_userid)
197 ]).then { |(parent, _)|
198 save_active_plan(plan, parent)
199 }.then do
200 use_referral_code(code)
201 end
202 end
203
204 protected
205
206 def save_bogus_transaction
207 Transaction.new(
208 customer_id: @customer.customer_id,
209 transaction_id: "google_play_#{@customer.customer_id}",
210 amount: 0,
211 note: "Activated via Google Play",
212 bonus_eligible?: false
213 ).insert
214 end
215
216 def save_active_plan(plan, parent)
217 @customer = @customer.with_plan(plan.name, parent_customer_id: parent)
218 save_bogus_transaction.then do
219 @customer.activate_plan_starting_now
220 end
221 end
222
223 def use_referral_code(code)
224 EMPromise.resolve(nil).then {
225 @invites.claim_code(@customer.customer_id, code) {
226 @customer.extend_plan
227 }
228 }.catch_only(InvitesRepo::Invalid) do
229 @invites.stash_code(@customer.customer_id, code)
230 end
231 end
232 end
233
234 class Allow < Activation
235 def self.for(customer, tel, jid)
236 credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
237 new(customer, tel, credit_to)
238 end
239
240 def initialize(customer, tel, credit_to)
241 super(customer, tel)
242 @credit_to = credit_to
243 end
244
245 def form
246 FormTemplate.render(
247 "registration/allow",
248 tel: tel,
249 domain: customer.jid.domain
250 )
251 end
252
253 def next_step(iq)
254 plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
255 @customer = customer.with_plan(plan.name)
256 EMPromise.resolve(nil).then { activate }.then do
257 Finish.new(customer, tel).write
258 end
259 end
260
261 protected
262
263 def activate
264 DB.transaction do
265 if @credit_to
266 InvitesRepo.new(DB, REDIS).create_claimed_code(
267 @credit_to,
268 customer.customer_id
269 )
270 end
271 @customer.activate_plan_starting_now
272 end
273 end
274 end
275 end
276
277 module Payment
278 def self.kinds
279 @kinds ||= {}
280 end
281
282 def self.for(iq, customer, tel, finish: Finish)
283 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
284 raise "Invalid activation method"
285 }.call(customer, tel, finish: finish)
286 end
287
288 class CryptoPaymentMethod
289 def crypto_addrs
290 raise NotImplementedError, "Subclass must implement"
291 end
292
293 def reg_form_name
294 raise NotImplementedError, "Subclass must implement"
295 end
296
297 def sell_prices
298 raise NotImplementedError, "Subclass must implement"
299 end
300
301 def initialize(customer, tel, **)
302 @customer = customer
303 @customer_id = customer.customer_id
304 @tel = tel
305 end
306
307 def save
308 TEL_SELECTIONS.set(@customer.jid, @tel)
309 end
310
311 attr_reader :customer_id, :tel
312
313 def form(rate, addr)
314 amount = CONFIG[:activation_amount] / rate
315
316 FormTemplate.render(
317 reg_form_name,
318 amount: amount,
319 addr: addr
320 )
321 end
322
323 def write
324 EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
325 Command.reply { |reply|
326 reply.allowed_actions = [:prev]
327 reply.status = :canceled
328 reply.command << form(rate, addr)
329 }.then(&method(:handle_possible_prev))
330 end
331 end
332
333 protected
334
335 def handle_possible_prev(iq)
336 raise "Action not allowed" unless iq.prev?
337
338 Activation.for(@customer, nil, @tel).then(&:write)
339 end
340
341 def addr_and_rate
342 EMPromise.all([
343 crypto_addrs.then { |addrs|
344 addrs.first || add_crypto_addr
345 },
346
347 sell_prices.public_send(@customer.currency.to_s.downcase)
348 ])
349 end
350 end
351
352 class Bitcoin < CryptoPaymentMethod
353 Payment.kinds[:bitcoin] = method(:new)
354
355 def reg_form_name
356 "registration/btc"
357 end
358
359 def sell_prices
360 BTC_SELL_PRICES
361 end
362
363 def crypto_addrs
364 @customer.btc_addresses
365 end
366
367 def add_crypto_addr
368 @customer.add_btc_address
369 end
370 end
371
372 ## Like Bitcoin
373 class BCH < CryptoPaymentMethod
374 Payment.kinds[:bch] = method(:new)
375
376 def reg_form_name
377 "registration/bch"
378 end
379
380 def sell_prices
381 BCH_SELL_PRICES
382 end
383
384 def crypto_addrs
385 @customer.bch_addresses
386 end
387
388 def add_crypto_addr
389 @customer.add_bch_address
390 end
391 end
392
393 class CreditCard
394 Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
395
396 def self.for(in_customer, tel, finish: Finish, **)
397 reload_customer(in_customer).then do |customer|
398 if customer.balance >= CONFIG[:activation_amount_accept]
399 next BillPlan.new(customer, tel, finish: finish)
400 end
401
402 new(customer, tel, finish: finish)
403 end
404 end
405
406 def self.reload_customer(customer)
407 EMPromise.resolve(nil).then do
408 Command.execution.customer_repo.find(customer.customer_id)
409 end
410 end
411
412 def initialize(customer, tel, finish: Finish)
413 @customer = customer
414 @tel = tel
415 @finish = finish
416 end
417
418 def oob(reply)
419 oob = OOB.find_or_create(reply.command)
420 oob.url = CONFIG[:credit_card_url].call(
421 reply.to.stripped.to_s.gsub("\\", "%5C"),
422 @customer.customer_id
423 ) + "&amount=#{CONFIG[:activation_amount]}"
424 oob.desc = "Pay by credit card, save, then next here to continue"
425 oob
426 end
427
428 def write
429 Command.reply { |reply|
430 reply.allowed_actions = [:next, :prev]
431 toob = oob(reply)
432 reply.note_type = :info
433 reply.note_text = "#{toob.desc}: #{toob.url}"
434 }.then do |iq|
435 next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
436
437 CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
438 end
439 end
440 end
441
442 class InviteCode
443 Payment.kinds[:code] = ->(*args, **kw) { self.for(*args, **kw) }
444
445 def self.for(in_customer, tel, finish: Finish, **)
446 reload_customer(in_customer).then do |customer|
447 if customer.balance >= CONFIG[:activation_amount_accept]
448 next BillPlan.new(customer, tel, finish: finish)
449 end
450
451 msg = if customer.balance.positive?
452 "Account balance not enough to cover the activation"
453 end
454 new(customer, tel, error: msg, finish: Finish)
455 end
456 end
457
458 def self.reload_customer(customer)
459 Command.execution.customer_repo.find(customer.customer_id)
460 end
461
462 FIELDS = [{
463 var: "code",
464 type: "text-single",
465 label: "Your referral code",
466 required: true
467 }].freeze
468
469 def initialize(customer, tel, error: nil, finish: Finish, **)
470 @customer = customer
471 @tel = tel
472 @error = error
473 @finish = finish
474 @parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB)
475 end
476
477 def add_form(reply)
478 form = reply.form
479 form.type = :form
480 form.title = "Enter Referral Code"
481 form.instructions = @error if @error
482 form.fields = FIELDS
483 end
484
485 def write
486 Command.reply { |reply|
487 reply.allowed_actions = [:next, :prev]
488 add_form(reply)
489 }.then(&method(:parse))
490 end
491
492 def parse(iq)
493 return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
494
495 verify(iq.form.field("code")&.value&.to_s)
496 .catch_only(InvitesRepo::Invalid, &method(:invalid_code))
497 .then(&:write)
498 end
499
500 protected
501
502 def invalid_code(e)
503 InviteCode.new(@customer, @tel, error: e.message)
504 end
505
506 def customer_id
507 @customer.customer_id
508 end
509
510 def verify(code)
511 @parent_code_repo.find(code).then do |parent_customer_id|
512 if parent_customer_id
513 set_parent(parent_customer_id)
514 else
515 InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) {
516 @customer.activate_plan_starting_now
517 }.then { Finish.new(@customer, @tel) }
518 end
519 end
520 end
521
522 def set_parent(parent_customer_id)
523 Registration.guard_onboarding_subaccounts(@customer)
524
525 @customer = @customer.with_plan(
526 @customer.plan_name,
527 parent_customer_id: parent_customer_id
528 )
529 @customer.save_plan!.then do
530 self.class.for(@customer, @tel, finish: @finish)
531 end
532 end
533 end
534
535 class Mail
536 Payment.kinds[:mail] = method(:new)
537
538 def initialize(customer, tel, **)
539 @customer = customer
540 @tel = tel
541 end
542
543 def form
544 FormTemplate.render(
545 "registration/mail",
546 currency: @customer.currency,
547 **onboarding_extras
548 )
549 end
550
551 def onboarding_extras
552 jid = ProxiedJID.new(@customer.jid).unproxied
553 return {} unless jid.domain == CONFIG[:onboarding_domain]
554
555 {
556 customer_id: @customer.customer_id,
557 in_note: "Customer ID"
558 }
559 end
560
561 def write
562 Command.reply { |reply|
563 reply.allowed_actions = [:prev]
564 reply.status = :canceled
565 reply.command << form
566 }.then { |iq|
567 raise "Action not allowed" unless iq.prev?
568
569 Activation.for(@customer, nil, @tel).then(&:write)
570 }
571 end
572 end
573 end
574
575 class BillPlan
576 def initialize(customer, tel, finish: Finish)
577 @customer = customer
578 @tel = tel
579 @finish = finish
580 end
581
582 def write
583 @customer.bill_plan(note: "Bill #{@tel} for first month").then do
584 @finish.new(@customer, @tel).write
585 end
586 end
587 end
588
589 class Finish
590 def initialize(customer, tel)
591 @customer = customer
592 @tel = tel
593 @invites = InvitesRepo.new(DB, REDIS)
594 end
595
596 def write
597 @tel.order(DB, @customer).then(
598 ->(_) { customer_active_tel_purchased },
599 method(:number_purchase_error)
600 )
601 end
602
603 protected
604
605 def number_purchase_error(e)
606 Command.log.error "number_purchase_error", e
607 TEL_SELECTIONS.delete(@customer.jid).then {
608 TEL_SELECTIONS[@customer.jid]
609 }.then { |choose|
610 choose.choose_tel(
611 error: "The JMP number #{@tel} is no longer available."
612 )
613 }.then { |tel| Finish.new(@customer, tel).write }
614 end
615
616 def raise_setup_error(e)
617 Command.log.error "@customer.register! failed", e
618 Command.finish(
619 "There was an error setting up your number, " \
620 "please contact JMP support.",
621 type: :error
622 )
623 end
624
625 def put_default_fwd
626 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel.tel, CustomerFwd.for(
627 uri: "xmpp:#{@customer.jid}",
628 voicemail_enabled: true
629 ))
630 end
631
632 def use_referral_code
633 @invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
634 next unless credit_to
635
636 Transaction.new(
637 customer_id: @customer.customer_id,
638 transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
639 amount: @customer.monthly_price,
640 note: "Referral Bonus",
641 bonus_eligible?: false
642 ).insert
643 end
644 end
645
646 def customer_active_tel_purchased
647 @customer.register!(@tel.tel).catch(&method(:raise_setup_error)).then {
648 EMPromise.all([
649 TEL_SELECTIONS.delete(@customer.jid),
650 put_default_fwd,
651 use_referral_code
652 ])
653 }.then do
654 FinishOnboarding.for(@customer, @tel).then(&:write)
655 end
656 end
657 end
658
659 module FinishOnboarding
660 def self.for(customer, tel, db: LazyObject.new { DB })
661 jid = ProxiedJID.new(customer.jid).unproxied
662 if jid.domain == CONFIG[:onboarding_domain]
663 Snikket.for(customer, tel, db: db)
664 else
665 NotOnboarding.new(customer, tel)
666 end
667 end
668
669 class Snikket
670 def self.for(customer, tel, db:)
671 ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
672 if is.empty?
673 new(customer, tel, db: db)
674 elsif is[0].bootstrap_token.empty?
675 # This is a need_dns one, try the launch again
676 new(customer, tel, db: db).launch(is[0].domain)
677 else
678 GetInvite.for(customer, is[0], tel, db: db)
679 end
680 end
681 end
682
683 def initialize(customer, tel, error: nil, old: nil, db:)
684 @customer = customer
685 @tel = tel
686 @error = error
687 @db = db
688 @old = old
689 end
690
691 ACTION_VAR = "http://jabber.org/protocol/commands#actions"
692
693 def form
694 FormTemplate.render(
695 "registration/snikket",
696 tel: @tel,
697 error: @error
698 )
699 end
700
701 def write
702 Command.reply { |reply|
703 reply.allowed_actions = [:next]
704 reply.command << form
705 }.then(&method(:next_step))
706 end
707
708 def next_step(iq)
709 subdomain = empty_nil(iq.form.field("subdomain")&.value)
710 domain = "#{subdomain}.snikket.chat"
711 if iq.form.field(ACTION_VAR)&.value == "custom_domain"
712 CustomDomain.new(@customer, @tel, old: @old).write
713 elsif @old && (!subdomain || domain == @old.domain)
714 GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
715 else
716 launch(domain)
717 end
718 end
719
720 def launch(domain)
721 IQ_MANAGER.write(::Snikket::Launch.new(
722 nil, CONFIG[:snikket_hosting_api], domain: domain
723 )).then { |launched|
724 save_instance_and_wait(domain, launched)
725 }.catch { |e|
726 next EMPromise.reject(e) unless e.respond_to?(:text)
727
728 Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
729 }
730 end
731
732 def save_instance_and_wait(domain, launched)
733 instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
734 repo = ::Snikket::Repo.new(db: @db)
735 (@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
736 .then { repo.put(instance) }.then do
737 if launched.status == :needs_dns
738 NeedsDNS.new(@customer, instance, @tel, launched.records).write
739 else
740 GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
741 end
742 end
743 end
744
745 def empty_nil(s)
746 s.nil? || s.empty? ? nil : s
747 end
748
749 class NeedsDNS < Snikket
750 def initialize(customer, instance, tel, records, db: DB)
751 @customer = customer
752 @instance = instance
753 @tel = tel
754 @records = records
755 @db = db
756 end
757
758 def form
759 FormTemplate.render(
760 "registration/snikket_needs_dns",
761 records: @records
762 )
763 end
764
765 def write
766 Command.reply { |reply|
767 reply.allowed_actions = [:prev, :next]
768 reply.command << form
769 }.then do |iq|
770 if iq.prev?
771 CustomDomain.new(@customer, @tel, old: @instance).write
772 else
773 launch(@instance.domain)
774 end
775 end
776 end
777 end
778
779 class GetInvite
780 def self.for(customer, instance, tel, db: DB)
781 instance.fetch_invite.then do |xmpp_uri|
782 if xmpp_uri
783 GoToInvite.new(xmpp_uri)
784 else
785 new(customer, instance, tel, db: db)
786 end
787 end
788 end
789
790 def initialize(customer, instance, tel, db: DB)
791 @customer = customer
792 @instance = instance
793 @tel = tel
794 @db = db
795 end
796
797 def form
798 FormTemplate.render(
799 "registration/snikket_wait",
800 domain: @instance.domain
801 )
802 end
803
804 def write
805 Command.reply { |reply|
806 reply.allowed_actions = [:prev, :next]
807 reply.command << form
808 }.then do |iq|
809 if iq.prev?
810 Snikket.new(@customer, @tel, old: @instance, db: @db).write
811 else
812 GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
813 end
814 end
815 end
816 end
817
818 class GoToInvite
819 def initialize(xmpp_uri)
820 @xmpp_uri = xmpp_uri
821 end
822
823 def write
824 Command.finish do |reply|
825 oob = OOB.find_or_create(reply.command)
826 oob.url = @xmpp_uri
827 end
828 end
829 end
830 end
831
832 class CustomDomain < Snikket
833 def initialize(customer, tel, old: nil, error: nil, db: DB)
834 @customer = customer
835 @tel = tel
836 @error = error
837 @old = old
838 @db = db
839 end
840
841 def form
842 FormTemplate.render(
843 "registration/snikket_custom",
844 tel: @tel,
845 error: @error
846 )
847 end
848
849 def write
850 Command.reply { |reply|
851 reply.allowed_actions = [:prev, :next]
852 reply.command << form
853 }.then do |iq|
854 if iq.prev?
855 Snikket.new(@customer, @tel, db: @db, old: @old).write
856 else
857 launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
858 end
859 end
860 end
861 end
862
863 class NotOnboarding
864 def initialize(customer, tel)
865 @customer = customer
866 @tel = tel
867 end
868
869 def write
870 WelcomeMessage.new(@customer, @tel).welcome
871 Command.finish("Your JMP account has been activated as #{@tel}")
872 end
873 end
874 end
875end