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