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