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