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 false && 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(
263 iq, customer, tel,
264 finish: Finish, maybe_bill: MaybeBill,
265 price: CONFIG[:activation_amount] + tel.price
266 )
267 kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
268 raise "Invalid activation method"
269 }.call(
270 customer, tel, finish: finish, maybe_bill: maybe_bill, price: price
271 )
272 end
273
274 class CryptoPaymentMethod
275 def crypto_addrs
276 raise NotImplementedError, "Subclass must implement"
277 end
278
279 def reg_form_name
280 raise NotImplementedError, "Subclass must implement"
281 end
282
283 def sell_prices
284 raise NotImplementedError, "Subclass must implement"
285 end
286
287 def initialize(
288 customer, tel,
289 price: CONFIG[:activation_amount] + tel.price, **
290 )
291 @customer = customer
292 @customer_id = customer.customer_id
293 @tel = tel
294 @price = price
295 end
296
297 def save
298 TEL_SELECTIONS.set(@customer.jid, @tel)
299 end
300
301 attr_reader :customer_id, :tel
302
303 def form(rate, addr)
304 FormTemplate.render(
305 reg_form_name,
306 amount: @price / rate,
307 addr: addr
308 )
309 end
310
311 def write
312 EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
313 Command.reply { |reply|
314 reply.allowed_actions = [:prev]
315 reply.status = :canceled
316 reply.command << form(rate, addr)
317 }.then(&method(:handle_possible_prev))
318 end
319 end
320
321 protected
322
323 def handle_possible_prev(iq)
324 raise "Action not allowed" unless iq.prev?
325
326 Activation.for(@customer, nil, @tel).then(&:write)
327 end
328
329 def addr_and_rate
330 EMPromise.all([
331 crypto_addrs.then { |addrs|
332 addrs.first || add_crypto_addr
333 },
334
335 sell_prices.public_send(@customer.currency.to_s.downcase)
336 ])
337 end
338 end
339
340 class Bitcoin < CryptoPaymentMethod
341 Payment.kinds[:bitcoin] = method(:new)
342
343 def reg_form_name
344 "registration/btc"
345 end
346
347 def sell_prices
348 BTC_SELL_PRICES
349 end
350
351 def crypto_addrs
352 @customer.btc_addresses
353 end
354
355 def add_crypto_addr
356 @customer.add_btc_address
357 end
358 end
359
360 ## Like Bitcoin
361 class BCH < CryptoPaymentMethod
362 Payment.kinds[:bch] = method(:new)
363
364 def reg_form_name
365 "registration/bch"
366 end
367
368 def sell_prices
369 BCH_SELL_PRICES
370 end
371
372 def crypto_addrs
373 @customer.bch_addresses
374 end
375
376 def add_crypto_addr
377 @customer.add_bch_address
378 end
379 end
380
381 class MaybeBill
382 def initialize(customer, tel, finish: Finish)
383 @customer = customer
384 @tel = tel
385 @finish = finish
386 end
387
388 def call
389 reload_customer(@customer).then do |customer|
390 if customer.balance >= CONFIG[:activation_amount_accept]
391 next BillPlan.new(customer, @tel, finish: @finish)
392 end
393
394 yield customer
395 end
396 end
397 end
398
399 class JustCharge
400 def initialize(customer, *, **)
401 @customer = customer
402 end
403
404 def call; end
405 end
406
407 class CreditCard
408 Payment.kinds[:credit_card] = method(:new)
409
410 def initialize(
411 customer, tel,
412 finish: Finish, maybe_bill: MaybeBill,
413 price: CONFIG[:activation_amount] + tel.price
414 )
415 @customer = customer
416 @tel = tel
417 @finish = finish
418 @maybe_bill = maybe_bill.new(customer, tel, finish: finish)
419 @price = price
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=#{@price.ceil}"
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 @maybe_bill.call { self }&.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(
526 customer, tel,
527 price: CONFIG[:activation_amount] + tel.price, **
528 )
529 @customer = customer
530 @tel = tel
531 @price = price
532 end
533
534 def form
535 FormTemplate.render(
536 "registration/mail",
537 currency: @customer.currency,
538 price: @price,
539 **onboarding_extras
540 )
541 end
542
543 def onboarding_extras
544 jid = ProxiedJID.new(@customer.jid).unproxied
545 return {} unless jid.domain == CONFIG[:onboarding_domain]
546
547 {
548 customer_id: @customer.customer_id,
549 in_note: "Customer ID"
550 }
551 end
552
553 def write
554 Command.reply { |reply|
555 reply.allowed_actions = [:prev]
556 reply.status = :canceled
557 reply.command << form
558 }.then { |iq|
559 raise "Action not allowed" unless iq.prev?
560
561 Activation.for(@customer, nil, @tel).then(&:write)
562 }
563 end
564 end
565 end
566
567 class BillPlan
568 def initialize(customer, tel, finish: Finish)
569 @customer = customer
570 @tel = tel
571 @finish = finish
572 end
573
574 def write
575 @customer.bill_plan(note: "Bill #{@tel} for first month").then do
576 updated_customer =
577 @customer.with_balance(@customer.balance - @customer.monthly_price)
578 @finish.new(updated_customer, @tel).write
579 end
580 end
581 end
582
583 class BuyNumber
584 def initialize(customer, tel)
585 @customer = customer
586 @tel = tel
587 end
588
589 def write
590 Command.reply { |reply|
591 reply.command << FormTemplate.render(
592 "registration/buy_number",
593 tel: @tel
594 )
595 }.then(&method(:parse)).then(&:write)
596 end
597
598 protected
599
600 def parse(iq)
601 Payment.for(
602 iq, @customer, @tel,
603 maybe_bill: ::Registration::Payment::JustCharge,
604 price: @tel.price
605 )
606 end
607 end
608
609 class Finish
610 def initialize(customer, tel)
611 @customer = customer
612 @tel = tel
613 @invites = InvitesRepo.new(DB, REDIS)
614 end
615
616 def write
617 return buy_number if @customer.balance < @tel.price
618
619 @tel.order(DB, @customer).then(
620 ->(_) { customer_active_tel_purchased },
621 method(:number_purchase_error)
622 )
623 end
624
625 protected
626
627 def buy_number
628 BuyNumber.new(@customer, @tel).write.then do
629 reload_customer(@customer).then { |customer|
630 Finish.new(customer, @tel)
631 }.then(&:write)
632 end
633 end
634
635 def number_purchase_error(e)
636 Command.log.error "number_purchase_error", e
637 TEL_SELECTIONS.delete(@customer.jid).then {
638 TEL_SELECTIONS[@customer.jid]
639 }.then { |choose|
640 choose.choose_tel(
641 error: "The JMP number #{@tel} is no longer available."
642 )
643 }.then { |tel| Finish.new(@customer, tel).write }
644 end
645
646 def raise_setup_error(e)
647 Command.log.error "@customer.register! failed", e
648 Command.finish(
649 "There was an error setting up your number, " \
650 "please contact JMP support.",
651 type: :error
652 )
653 end
654
655 def put_default_fwd
656 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel.tel, CustomerFwd.for(
657 uri: "xmpp:#{@customer.jid}",
658 voicemail_enabled: true
659 ))
660 end
661
662 def use_referral_code
663 @invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
664 next unless credit_to
665
666 Transaction.new(
667 customer_id: @customer.customer_id,
668 transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
669 amount: @customer.monthly_price,
670 note: "Referral Bonus",
671 bonus_eligible?: false
672 ).insert
673 end
674 end
675
676 def customer_active_tel_purchased
677 @customer.register!(@tel.tel).catch(&method(:raise_setup_error)).then {
678 EMPromise.all([
679 TEL_SELECTIONS.delete(@customer.jid),
680 put_default_fwd,
681 use_referral_code,
682 @tel.charge(@customer)
683 ])
684 }.then do
685 FinishOnboarding.for(@customer, @tel).then(&:write)
686 end
687 end
688 end
689
690 module FinishOnboarding
691 def self.for(customer, tel, db: LazyObject.new { DB })
692 jid = ProxiedJID.new(customer.jid).unproxied
693 if jid.domain == CONFIG[:onboarding_domain]
694 Snikket.for(customer, tel, db: db)
695 else
696 NotOnboarding.new(customer, tel)
697 end
698 end
699
700 class Snikket
701 def self.for(customer, tel, db:)
702 ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
703 if is.empty?
704 new(customer, tel, db: db)
705 elsif is[0].bootstrap_token.empty?
706 # This is a need_dns one, try the launch again
707 new(customer, tel, db: db).launch(is[0].domain)
708 else
709 GetInvite.for(customer, is[0], tel, db: db)
710 end
711 end
712 end
713
714 def initialize(customer, tel, error: nil, old: nil, db:)
715 @customer = customer
716 @tel = tel
717 @error = error
718 @db = db
719 @old = old
720 end
721
722 ACTION_VAR = "http://jabber.org/protocol/commands#actions"
723
724 def form
725 FormTemplate.render(
726 "registration/snikket",
727 tel: @tel,
728 error: @error
729 )
730 end
731
732 def write
733 Command.reply { |reply|
734 reply.allowed_actions = [:next]
735 reply.command << form
736 }.then(&method(:next_step))
737 end
738
739 def next_step(iq)
740 subdomain = empty_nil(iq.form.field("subdomain")&.value)
741 domain = "#{subdomain}.snikket.chat"
742 if iq.form.field(ACTION_VAR)&.value == "custom_domain"
743 CustomDomain.new(@customer, @tel, old: @old).write
744 elsif @old && (!subdomain || domain == @old.domain)
745 GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
746 else
747 launch(domain)
748 end
749 end
750
751 def launch(domain)
752 IQ_MANAGER.write(::Snikket::Launch.new(
753 nil, CONFIG[:snikket_hosting_api], domain: domain
754 )).then { |launched|
755 save_instance_and_wait(domain, launched)
756 }.catch { |e|
757 next EMPromise.reject(e) unless e.respond_to?(:text)
758
759 Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
760 }
761 end
762
763 def save_instance_and_wait(domain, launched)
764 instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
765 repo = ::Snikket::Repo.new(db: @db)
766 (@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
767 .then { repo.put(instance) }.then do
768 if launched.status == :needs_dns
769 NeedsDNS.new(@customer, instance, @tel, launched.records).write
770 else
771 GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
772 end
773 end
774 end
775
776 def empty_nil(s)
777 s.nil? || s.empty? ? nil : s
778 end
779
780 class NeedsDNS < Snikket
781 def initialize(customer, instance, tel, records, db: DB)
782 @customer = customer
783 @instance = instance
784 @tel = tel
785 @records = records
786 @db = db
787 end
788
789 def form
790 FormTemplate.render(
791 "registration/snikket_needs_dns",
792 records: @records
793 )
794 end
795
796 def write
797 Command.reply { |reply|
798 reply.allowed_actions = [:prev, :next]
799 reply.command << form
800 }.then do |iq|
801 if iq.prev?
802 CustomDomain.new(@customer, @tel, old: @instance).write
803 else
804 launch(@instance.domain)
805 end
806 end
807 end
808 end
809
810 class GetInvite
811 def self.for(customer, instance, tel, db: DB)
812 instance.fetch_invite.then do |xmpp_uri|
813 if xmpp_uri
814 GoToInvite.new(xmpp_uri)
815 else
816 new(customer, instance, tel, db: db)
817 end
818 end
819 end
820
821 def initialize(customer, instance, tel, db: DB)
822 @customer = customer
823 @instance = instance
824 @tel = tel
825 @db = db
826 end
827
828 def form
829 FormTemplate.render(
830 "registration/snikket_wait",
831 domain: @instance.domain
832 )
833 end
834
835 def write
836 Command.reply { |reply|
837 reply.allowed_actions = [:prev, :next]
838 reply.command << form
839 }.then do |iq|
840 if iq.prev?
841 Snikket.new(@customer, @tel, old: @instance, db: @db).write
842 else
843 GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
844 end
845 end
846 end
847 end
848
849 class GoToInvite
850 def initialize(xmpp_uri)
851 @xmpp_uri = xmpp_uri
852 end
853
854 def write
855 Command.finish do |reply|
856 oob = OOB.find_or_create(reply.command)
857 oob.url = @xmpp_uri
858 end
859 end
860 end
861 end
862
863 class CustomDomain < Snikket
864 def initialize(customer, tel, old: nil, error: nil, db: DB)
865 @customer = customer
866 @tel = tel
867 @error = error
868 @old = old
869 @db = db
870 end
871
872 def form
873 FormTemplate.render(
874 "registration/snikket_custom",
875 tel: @tel,
876 error: @error
877 )
878 end
879
880 def write
881 Command.reply { |reply|
882 reply.allowed_actions = [:prev, :next]
883 reply.command << form
884 }.then do |iq|
885 if iq.prev?
886 Snikket.new(@customer, @tel, db: @db, old: @old).write
887 else
888 launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
889 end
890 end
891 end
892 end
893
894 class NotOnboarding
895 def initialize(customer, tel)
896 @customer = customer
897 @tel = tel
898 end
899
900 def write
901 WelcomeMessage.new(@customer, @tel).welcome
902 Command.finish("Your JMP account has been activated as #{@tel}")
903 end
904 end
905 end
906end