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 TEL_SELECTIONS[@customer.jid]
546 }.then { |choose|
547 choose.choose_tel(
548 error: "The JMP number #{@tel} is no longer available."
549 )
550 }.then { |tel| Finish.new(@customer, tel).write }
551 end
552
553 def raise_setup_error(e)
554 Command.log.error "@customer.register! failed", e
555 Command.finish(
556 "There was an error setting up your number, " \
557 "please contact JMP support.",
558 type: :error
559 )
560 end
561
562 def put_default_fwd
563 Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
564 uri: "xmpp:#{@customer.jid}",
565 voicemail_enabled: true
566 ))
567 end
568
569 def use_referral_code
570 @invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
571 next unless credit_to
572
573 Transaction.new(
574 customer_id: @customer.customer_id,
575 transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
576 amount: @customer.monthly_price,
577 note: "Referral Bonus",
578 bonus_eligible?: false
579 ).insert
580 end
581 end
582
583 def customer_active_tel_purchased
584 @customer.register!(@tel).catch(&method(:raise_setup_error)).then {
585 EMPromise.all([
586 REDIS.del("pending_tel_for-#{@customer.jid}"),
587 put_default_fwd,
588 use_referral_code
589 ])
590 }.then do
591 FinishOnboarding.for(@customer, @tel).then(&:write)
592 end
593 end
594 end
595
596 module FinishOnboarding
597 def self.for(customer, tel, db: LazyObject.new { DB })
598 jid = ProxiedJID.new(customer.jid).unproxied
599 if jid.domain == CONFIG[:onboarding_domain]
600 Snikket.for(customer, tel, db: db)
601 else
602 NotOnboarding.new(customer, tel)
603 end
604 end
605
606 class Snikket
607 def self.for(customer, tel, db:)
608 ::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
609 if is.empty?
610 new(customer, tel, db: db)
611 elsif is[0].bootstrap_token.empty?
612 # This is a need_dns one, try the launch again
613 new(customer, tel, db: db).launch(is[0].domain)
614 else
615 GetInvite.for(customer, is[0], tel, db: db)
616 end
617 end
618 end
619
620 def initialize(customer, tel, error: nil, old: nil, db:)
621 @customer = customer
622 @tel = tel
623 @error = error
624 @db = db
625 @old = old
626 end
627
628 ACTION_VAR = "http://jabber.org/protocol/commands#actions"
629
630 def form
631 FormTemplate.render(
632 "registration/snikket",
633 tel: @tel,
634 error: @error
635 )
636 end
637
638 def write
639 Command.reply { |reply|
640 reply.allowed_actions = [:next]
641 reply.command << form
642 }.then(&method(:next_step))
643 end
644
645 def next_step(iq)
646 subdomain = empty_nil(iq.form.field("subdomain")&.value)
647 domain = "#{subdomain}.snikket.chat"
648 if iq.form.field(ACTION_VAR)&.value == "custom_domain"
649 CustomDomain.new(@customer, @tel, old: @old).write
650 elsif @old && (!subdomain || domain == @old.domain)
651 GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
652 else
653 launch(domain)
654 end
655 end
656
657 def launch(domain)
658 IQ_MANAGER.write(::Snikket::Launch.new(
659 nil, CONFIG[:snikket_hosting_api], domain: domain
660 )).then { |launched|
661 save_instance_and_wait(domain, launched)
662 }.catch { |e|
663 next EMPromise.reject(e) unless e.respond_to?(:text)
664
665 Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
666 }
667 end
668
669 def save_instance_and_wait(domain, launched)
670 instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
671 repo = ::Snikket::Repo.new(db: @db)
672 (@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
673 .then { repo.put(instance) }.then do
674 if launched.status == :needs_dns
675 NeedsDNS.new(@customer, instance, @tel, launched.records).write
676 else
677 GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
678 end
679 end
680 end
681
682 def empty_nil(s)
683 s.nil? || s.empty? ? nil : s
684 end
685
686 class NeedsDNS < Snikket
687 def initialize(customer, instance, tel, records, db: DB)
688 @customer = customer
689 @instance = instance
690 @tel = tel
691 @records = records
692 @db = db
693 end
694
695 def form
696 FormTemplate.render(
697 "registration/snikket_needs_dns",
698 records: @records
699 )
700 end
701
702 def write
703 Command.reply { |reply|
704 reply.allowed_actions = [:prev, :next]
705 reply.command << form
706 }.then do |iq|
707 if iq.prev?
708 CustomDomain.new(@customer, @tel, old: @instance).write
709 else
710 launch(@instance.domain)
711 end
712 end
713 end
714 end
715
716 class GetInvite
717 def self.for(customer, instance, tel, db: DB)
718 instance.fetch_invite.then do |xmpp_uri|
719 if xmpp_uri
720 GoToInvite.new(xmpp_uri)
721 else
722 new(customer, instance, tel, db: db)
723 end
724 end
725 end
726
727 def initialize(customer, instance, tel, db: DB)
728 @customer = customer
729 @instance = instance
730 @tel = tel
731 @db = db
732 end
733
734 def form
735 FormTemplate.render(
736 "registration/snikket_wait",
737 domain: @instance.domain
738 )
739 end
740
741 def write
742 Command.reply { |reply|
743 reply.allowed_actions = [:prev, :next]
744 reply.command << form
745 }.then do |iq|
746 if iq.prev?
747 Snikket.new(@customer, @tel, old: @instance, db: @db).write
748 else
749 GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
750 end
751 end
752 end
753 end
754
755 class GoToInvite
756 def initialize(xmpp_uri)
757 @xmpp_uri = xmpp_uri
758 end
759
760 def write
761 Command.finish do |reply|
762 oob = OOB.find_or_create(reply.command)
763 oob.url = @xmpp_uri
764 end
765 end
766 end
767 end
768
769 class CustomDomain < Snikket
770 def initialize(customer, tel, old: nil, error: nil, db: DB)
771 @customer = customer
772 @tel = tel
773 @error = error
774 @old = old
775 @db = db
776 end
777
778 def form
779 FormTemplate.render(
780 "registration/snikket_custom",
781 tel: @tel,
782 error: @error
783 )
784 end
785
786 def write
787 Command.reply { |reply|
788 reply.allowed_actions = [:prev, :next]
789 reply.command << form
790 }.then do |iq|
791 if iq.prev?
792 Snikket.new(@customer, @tel, db: @db, old: @old).write
793 else
794 launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
795 end
796 end
797 end
798 end
799
800 class NotOnboarding
801 def initialize(customer, tel)
802 @customer = customer
803 @tel = tel
804 end
805
806 def write
807 WelcomeMessage.new(@customer, @tel).welcome
808 Command.finish("Your JMP account has been activated as #{@tel}")
809 end
810 end
811 end
812end