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