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)
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, **)
448 @customer = customer
449 @tel = tel
450 @error = error
451 end
452
453 def add_form(reply)
454 form = reply.form
455 form.type = :form
456 form.title = "Enter Referral Code"
457 form.instructions = @error if @error
458 form.fields = FIELDS
459 end
460
461 def write
462 Command.reply { |reply|
463 reply.allowed_actions = [:next, :prev]
464 add_form(reply)
465 }.then(&method(:parse))
466 end
467
468 def parse(iq)
469 return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
470
471 verify(iq.form.field("code")&.value&.to_s).then {
472 Finish.new(@customer, @tel)
473 }.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
474 end
475
476 protected
477
478 def invalid_code(e)
479 InviteCode.new(@customer, @tel, error: e.message)
480 end
481
482 def customer_id
483 @customer.customer_id
484 end
485
486 def verify(code)
487 InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) do
488 @customer.activate_plan_starting_now
489 end
490 end
491 end
492
493 class Mail
494 Payment.kinds[:mail] = method(:new)
495
496 def initialize(customer, tel, final_message: nil, **)
497 @customer = customer
498 @tel = tel
499 @final_message = final_message
500 end
501
502 def form
503 FormTemplate.render(
504 "registration/mail",
505 currency: @customer.currency,
506 final_message: @final_message,
507 **onboarding_extras
508 )
509 end
510
511 def onboarding_extras
512 jid = ProxiedJID.new(@customer.jid).unproxied
513 return {} unless jid.domain == CONFIG[:onboarding_domain]
514
515 {
516 customer_id: @customer.customer_id,
517 in_note: "Customer ID"
518 }
519 end
520
521 def write
522 Command.reply { |reply|
523 reply.allowed_actions = [:prev]
524 reply.status = :canceled
525 reply.command << form
526 }.then { |iq|
527 raise "Action not allowed" unless iq.prev?
528
529 Activation.for(@customer, nil, @tel).then(&:write)
530 }
531 end
532 end
533 end
534
535 class BillPlan
536 def initialize(customer, tel, finish: Finish)
537 @customer = customer
538 @tel = tel
539 @finish = finish
540 end
541
542 def write
543 @customer.bill_plan(note: "Bill #{@tel} for first month").then do
544 @finish.new(@customer, @tel).write
545 end
546 end
547 end
548
549 class Finish
550 def initialize(customer, tel)
551 @customer = customer
552 @tel = tel
553 @invites = InvitesRepo.new(DB, REDIS)
554 end
555
556 def write
557 BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
558 BandwidthTNOrder.create(
559 @tel,
560 customer_order_id: @customer.customer_id,
561 reservation_id: rid
562 ).then(&:poll).then(
563 ->(_) { customer_active_tel_purchased },
564 method(:number_purchase_error)
565 )
566 end
567 end
568
569 protected
570
571 def number_purchase_error(e)
572 Command.log.error "number_purchase_error", e
573 TEL_SELECTIONS.delete(@customer.jid).then {
574 TEL_SELECTIONS[@customer.jid]
575 }.then { |choose|
576 choose.choose_tel(
577 error: "The JMP number #{@tel} is no longer available."
578 )
579 }.then { |tel| Finish.new(@customer, tel).write }
580 end
581
582 def raise_setup_error(e)
583 Command.log.error "@customer.register! failed", e
584 Command.finish(
585 "There was an error setting up your number, " \
586 "please contact JMP support.",
587 type: :error
588 )
589 end
590
591 def put_default_fwd
592 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
593 uri: "xmpp:#{@customer.jid}",
594 voicemail_enabled: true
595 ))
596 end
597
598 def use_referral_code
599 @invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
600 next unless credit_to
601
602 Transaction.new(
603 customer_id: @customer.customer_id,
604 transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
605 amount: @customer.monthly_price,
606 note: "Referral Bonus",
607 bonus_eligible?: false
608 ).insert
609 end
610 end
611
612 def customer_active_tel_purchased
613 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
614 EMPromise.all([
615 REDIS.del("pending_tel_for-#{@customer.jid}"),
616 put_default_fwd,
617 use_referral_code
618 ])
619 }.then do
620 FinishOnboarding.for(@customer, @tel).then(&:write)
621 end
622 end
623 end
624
625 module FinishOnboarding
626 def self.for(customer, tel, db: LazyObject.new { DB })
627 jid = ProxiedJID.new(customer.jid).unproxied
628 if jid.domain == CONFIG[:onboarding_domain]
629 Snikket.for(customer, tel, db: db)
630 else
631 NotOnboarding.new(customer, tel)
632 end
633 end
634
635 class Snikket
636 def self.for(customer, tel, db:)
637 ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
638 if is.empty?
639 new(customer, tel, db: db)
640 elsif is[0].bootstrap_token.empty?
641 # This is a need_dns one, try the launch again
642 new(customer, tel, db: db).launch(is[0].domain)
643 else
644 GetInvite.for(customer, is[0], tel, db: db)
645 end
646 end
647 end
648
649 def initialize(customer, tel, error: nil, old: nil, db:)
650 @customer = customer
651 @tel = tel
652 @error = error
653 @db = db
654 @old = old
655 end
656
657 ACTION_VAR = "http://jabber.org/protocol/commands#actions"
658
659 def form
660 FormTemplate.render(
661 "registration/snikket",
662 tel: @tel,
663 error: @error
664 )
665 end
666
667 def write
668 Command.reply { |reply|
669 reply.allowed_actions = [:next]
670 reply.command << form
671 }.then(&method(:next_step))
672 end
673
674 def next_step(iq)
675 subdomain = empty_nil(iq.form.field("subdomain")&.value)
676 domain = "#{subdomain}.snikket.chat"
677 if iq.form.field(ACTION_VAR)&.value == "custom_domain"
678 CustomDomain.new(@customer, @tel, old: @old).write
679 elsif @old && (!subdomain || domain == @old.domain)
680 GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
681 else
682 launch(domain)
683 end
684 end
685
686 def launch(domain)
687 IQ_MANAGER.write(::Snikket::Launch.new(
688 nil, CONFIG[:snikket_hosting_api], domain: domain
689 )).then { |launched|
690 save_instance_and_wait(domain, launched)
691 }.catch { |e|
692 next EMPromise.reject(e) unless e.respond_to?(:text)
693
694 Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
695 }
696 end
697
698 def save_instance_and_wait(domain, launched)
699 instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
700 repo = ::Snikket::Repo.new(db: @db)
701 (@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
702 .then { repo.put(instance) }.then do
703 if launched.status == :needs_dns
704 NeedsDNS.new(@customer, instance, @tel, launched.records).write
705 else
706 GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
707 end
708 end
709 end
710
711 def empty_nil(s)
712 s.nil? || s.empty? ? nil : s
713 end
714
715 class NeedsDNS < Snikket
716 def initialize(customer, instance, tel, records, db: DB)
717 @customer = customer
718 @instance = instance
719 @tel = tel
720 @records = records
721 @db = db
722 end
723
724 def form
725 FormTemplate.render(
726 "registration/snikket_needs_dns",
727 records: @records
728 )
729 end
730
731 def write
732 Command.reply { |reply|
733 reply.allowed_actions = [:prev, :next]
734 reply.command << form
735 }.then do |iq|
736 if iq.prev?
737 CustomDomain.new(@customer, @tel, old: @instance).write
738 else
739 launch(@instance.domain)
740 end
741 end
742 end
743 end
744
745 class GetInvite
746 def self.for(customer, instance, tel, db: DB)
747 instance.fetch_invite.then do |xmpp_uri|
748 if xmpp_uri
749 GoToInvite.new(xmpp_uri)
750 else
751 new(customer, instance, tel, db: db)
752 end
753 end
754 end
755
756 def initialize(customer, instance, tel, db: DB)
757 @customer = customer
758 @instance = instance
759 @tel = tel
760 @db = db
761 end
762
763 def form
764 FormTemplate.render(
765 "registration/snikket_wait",
766 domain: @instance.domain
767 )
768 end
769
770 def write
771 Command.reply { |reply|
772 reply.allowed_actions = [:prev, :next]
773 reply.command << form
774 }.then do |iq|
775 if iq.prev?
776 Snikket.new(@customer, @tel, old: @instance, db: @db).write
777 else
778 GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
779 end
780 end
781 end
782 end
783
784 class GoToInvite
785 def initialize(xmpp_uri)
786 @xmpp_uri = xmpp_uri
787 end
788
789 def write
790 Command.finish do |reply|
791 oob = OOB.find_or_create(reply.command)
792 oob.url = @xmpp_uri
793 end
794 end
795 end
796 end
797
798 class CustomDomain < Snikket
799 def initialize(customer, tel, old: nil, error: nil, db: DB)
800 @customer = customer
801 @tel = tel
802 @error = error
803 @old = old
804 @db = db
805 end
806
807 def form
808 FormTemplate.render(
809 "registration/snikket_custom",
810 tel: @tel,
811 error: @error
812 )
813 end
814
815 def write
816 Command.reply { |reply|
817 reply.allowed_actions = [:prev, :next]
818 reply.command << form
819 }.then do |iq|
820 if iq.prev?
821 Snikket.new(@customer, @tel, db: @db, old: @old).write
822 else
823 launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
824 end
825 end
826 end
827 end
828
829 class NotOnboarding
830 def initialize(customer, tel)
831 @customer = customer
832 @tel = tel
833 end
834
835 def write
836 WelcomeMessage.new(@customer, @tel).welcome
837 Command.finish("Your JMP account has been activated as #{@tel}")
838 end
839 end
840 end
841end