registration.rb

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