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