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