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