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, maybe_bill: MaybeBill)
257			kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
258				raise "Invalid activation method"
259			}.call(customer, tel, finish: finish, maybe_bill: maybe_bill)
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 MaybeBill
368			def initialize(customer, tel, finish: Finish)
369				@customer = customer
370				@tel = tel
371				@finish = finish
372			end
373
374			def call
375				reload_customer.then do |customer|
376					if customer.balance >= CONFIG[:activation_amount_accept]
377						next BillPlan.new(customer, @tel, finish: @finish)
378					end
379
380					yield customer
381				end
382			end
383
384			def reload_customer
385				EMPromise.resolve(nil).then do
386					Command.execution.customer_repo.find(@customer.customer_id)
387				end
388			end
389
390			def self.bill?
391				true
392			end
393		end
394
395		class JustCharge
396			def initialize(customer)
397				@customer = customer
398			end
399
400			def call
401				yield @customer
402			end
403
404			def self.bill?
405				false
406			end
407		end
408
409		class CreditCard
410			Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
411
412			def self.for(in_customer, tel, finish: Finish, maybe_bill: MaybeBill, **)
413				maybe_bill.new(in_customer, tel, finish: finish).call do |customer|
414					new(customer, tel, finish: finish)
415				end
416			end
417
418			def self.reload_customer(customer)
419				EMPromise.resolve(nil).then do
420					Command.execution.customer_repo.find(customer.customer_id)
421				end
422			end
423
424			def initialize(customer, tel, finish: Finish)
425				@customer = customer
426				@tel = tel
427				@finish = finish
428			end
429
430			def oob(reply)
431				oob = OOB.find_or_create(reply.command)
432				oob.url = CONFIG[:credit_card_url].call(
433					reply.to.stripped.to_s.gsub("\\", "%5C"),
434					@customer.customer_id
435				) + "&amount=#{CONFIG[:activation_amount]}"
436				oob.desc = "Pay by credit card, save, then next here to continue"
437				oob
438			end
439
440			def write
441				Command.reply { |reply|
442					reply.allowed_actions = [:next, :prev]
443					toob = oob(reply)
444					reply.note_type = :info
445					reply.note_text = "#{toob.desc}: #{toob.url}"
446				}.then do |iq|
447					next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
448
449					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
450				end
451			end
452		end
453
454		class InviteCode
455			Payment.kinds[:code] = method(:new)
456
457			FIELDS = [{
458				var: "code",
459				type: "text-single",
460				label: "Your referral code",
461				required: true
462			}].freeze
463
464			def initialize(
465				customer, tel,
466				error: nil, finish: Finish, db: DB, redis: REDIS, **
467			)
468				@customer = customer
469				@tel = tel
470				@error = error
471				@finish = finish
472				@invites_repo = InvitesRepo.new(db, redis)
473				@parent_code_repo = ParentCodeRepo.new(db: db, redis: redis)
474			end
475
476			def add_form(reply)
477				form = reply.form
478				form.type = :form
479				form.title = "Enter Referral Code"
480				form.instructions = @error if @error
481				form.fields = FIELDS
482			end
483
484			def write
485				Command.reply { |reply|
486					reply.allowed_actions = [:next, :prev]
487					add_form(reply)
488				}.then(&method(:parse)).catch_only(InvitesRepo::Invalid) { |e|
489					invalid_code(e).write
490				}
491			end
492
493			def parse(iq, force_save_plan: false)
494				return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
495
496				verify(iq.form.field("code")&.value&.to_s, force_save_plan)
497					.then(&:write)
498			end
499
500		protected
501
502			def invalid_code(e)
503				self.class.new(@customer, @tel, error: e.message, finish: @finish)
504			end
505
506			def customer_id
507				@customer.customer_id
508			end
509
510			def verify(code, force_save_plan)
511				@parent_code_repo.claim_code(@customer, code) {
512					check_parent_balance
513				}.catch_only(ParentCodeRepo::Invalid) {
514					(@customer.save_plan! if force_save_plan).then do
515						@invites_repo.claim_code(customer_id, code) {
516							@customer.activate_plan_starting_now
517						}.then { @finish.new(@customer, @tel) }
518					end
519				}
520			end
521
522			def check_parent_balance
523				MaybeBill.new(@customer, @tel, finish: @finish).call do
524					msg = "Account balance not enough to cover the activation"
525					invalid_code(RuntimeError.new(msg))
526				end
527			end
528		end
529
530		class Mail
531			Payment.kinds[:mail] = method(:new)
532
533			def initialize(customer, tel, maybe_bill: MaybeBill, **)
534				@customer = customer
535				@tel = tel
536				@maybe_bill = maybe_bill
537			end
538
539			def form
540				price = @maybe_bill.bill? ? CONFIG[:activation_amount] + @tel.price : @tel.price
541				FormTemplate.render(
542					"registration/mail",
543					currency: @customer.currency,
544					price: price,
545					**onboarding_extras
546				)
547			end
548
549			def onboarding_extras
550				jid = ProxiedJID.new(@customer.jid).unproxied
551				return {} unless jid.domain == CONFIG[:onboarding_domain]
552
553				{
554					customer_id: @customer.customer_id,
555					in_note: "Customer ID"
556				}
557			end
558
559			def write
560				Command.reply { |reply|
561					reply.allowed_actions = [:prev]
562					reply.status = :canceled
563					reply.command << form
564				}.then { |iq|
565					raise "Action not allowed" unless iq.prev?
566
567					Activation.for(@customer, nil, @tel).then(&:write)
568				}
569			end
570		end
571	end
572
573	class BillPlan
574		def initialize(customer, tel, finish: Finish)
575			@customer = customer
576			@tel = tel
577			@finish = finish
578		end
579
580		def write
581			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
582				@finish.new(@customer, @tel).write
583			end
584		end
585	end
586
587	class Finish
588		def initialize(customer, tel)
589			@customer = customer
590			@tel = tel
591			@invites = InvitesRepo.new(DB, REDIS)
592		end
593
594		def write
595			@tel.order(DB, @customer).then(
596				->(_) { customer_active_tel_purchased },
597				method(:number_purchase_error)
598			)
599		end
600
601	protected
602
603		def number_purchase_error(e)
604			Command.log.error "number_purchase_error", e
605			TEL_SELECTIONS.delete(@customer.jid).then {
606				TEL_SELECTIONS[@customer.jid]
607			}.then { |choose|
608				choose.choose_tel(
609					error: "The JMP number #{@tel} is no longer available."
610				)
611			}.then { |tel| Finish.new(@customer, tel).write }
612		end
613
614		def raise_setup_error(e)
615			Command.log.error "@customer.register! failed", e
616			Command.finish(
617				"There was an error setting up your number, " \
618				"please contact JMP support.",
619				type: :error
620			)
621		end
622
623		def put_default_fwd
624			Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel.tel, CustomerFwd.for(
625				uri: "xmpp:#{@customer.jid}",
626				voicemail_enabled: true
627			))
628		end
629
630		def use_referral_code
631			@invites.use_pending_group_code(@customer.customer_id).then do |credit_to|
632				next unless credit_to
633
634				Transaction.new(
635					customer_id: @customer.customer_id,
636					transaction_id: "referral_#{@customer.customer_id}_#{credit_to}",
637					amount: @customer.monthly_price,
638					note: "Referral Bonus",
639					bonus_eligible?: false
640				).insert
641			end
642		end
643
644		def customer_active_tel_purchased
645			@customer.register!(@tel.tel).catch(&method(:raise_setup_error)).then {
646				EMPromise.all([
647					TEL_SELECTIONS.delete(@customer.jid),
648					put_default_fwd,
649					use_referral_code
650				])
651			}.then do
652				FinishOnboarding.for(@customer, @tel).then(&:write)
653			end
654		end
655	end
656
657	module FinishOnboarding
658		def self.for(customer, tel, db: LazyObject.new { DB })
659			jid = ProxiedJID.new(customer.jid).unproxied
660			if jid.domain == CONFIG[:onboarding_domain]
661				Snikket.for(customer, tel, db: db)
662			else
663				NotOnboarding.new(customer, tel)
664			end
665		end
666
667		class Snikket
668			def self.for(customer, tel, db:)
669				::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
670					if is.empty?
671						new(customer, tel, db: db)
672					elsif is[0].bootstrap_token.empty?
673						# This is a need_dns one, try the launch again
674						new(customer, tel, db: db).launch(is[0].domain)
675					else
676						GetInvite.for(customer, is[0], tel, db: db)
677					end
678				end
679			end
680
681			def initialize(customer, tel, error: nil, old: nil, db:)
682				@customer = customer
683				@tel = tel
684				@error = error
685				@db = db
686				@old = old
687			end
688
689			ACTION_VAR = "http://jabber.org/protocol/commands#actions"
690
691			def form
692				FormTemplate.render(
693					"registration/snikket",
694					tel: @tel,
695					error: @error
696				)
697			end
698
699			def write
700				Command.reply { |reply|
701					reply.allowed_actions = [:next]
702					reply.command << form
703				}.then(&method(:next_step))
704			end
705
706			def next_step(iq)
707				subdomain = empty_nil(iq.form.field("subdomain")&.value)
708				domain = "#{subdomain}.snikket.chat"
709				if iq.form.field(ACTION_VAR)&.value == "custom_domain"
710					CustomDomain.new(@customer, @tel, old: @old).write
711				elsif @old && (!subdomain || domain == @old.domain)
712					GetInvite.for(@customer, @old, @tel, db: @db).then(&:write)
713				else
714					launch(domain)
715				end
716			end
717
718			def launch(domain)
719				IQ_MANAGER.write(::Snikket::Launch.new(
720					nil, CONFIG[:snikket_hosting_api], domain: domain
721				)).then { |launched|
722					save_instance_and_wait(domain, launched)
723				}.catch { |e|
724					next EMPromise.reject(e) unless e.respond_to?(:text)
725
726					Snikket.new(@customer, @tel, old: @old, error: e.text, db: @db).write
727				}
728			end
729
730			def save_instance_and_wait(domain, launched)
731				instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
732				repo = ::Snikket::Repo.new(db: @db)
733				(@old&.domain == domain ? EMPromise.resolve(nil) : repo.del(@old))
734					.then { repo.put(instance) }.then do
735						if launched.status == :needs_dns
736							NeedsDNS.new(@customer, instance, @tel, launched.records).write
737						else
738							GetInvite.for(@customer, instance, @tel, db: @db).then(&:write)
739						end
740					end
741			end
742
743			def empty_nil(s)
744				s.nil? || s.empty? ? nil : s
745			end
746
747			class NeedsDNS < Snikket
748				def initialize(customer, instance, tel, records, db: DB)
749					@customer = customer
750					@instance = instance
751					@tel = tel
752					@records = records
753					@db = db
754				end
755
756				def form
757					FormTemplate.render(
758						"registration/snikket_needs_dns",
759						records: @records
760					)
761				end
762
763				def write
764					Command.reply { |reply|
765						reply.allowed_actions = [:prev, :next]
766						reply.command << form
767					}.then do |iq|
768						if iq.prev?
769							CustomDomain.new(@customer, @tel, old: @instance).write
770						else
771							launch(@instance.domain)
772						end
773					end
774				end
775			end
776
777			class GetInvite
778				def self.for(customer, instance, tel, db: DB)
779					instance.fetch_invite.then do |xmpp_uri|
780						if xmpp_uri
781							GoToInvite.new(xmpp_uri)
782						else
783							new(customer, instance, tel, db: db)
784						end
785					end
786				end
787
788				def initialize(customer, instance, tel, db: DB)
789					@customer = customer
790					@instance = instance
791					@tel = tel
792					@db = db
793				end
794
795				def form
796					FormTemplate.render(
797						"registration/snikket_wait",
798						domain: @instance.domain
799					)
800				end
801
802				def write
803					Command.reply { |reply|
804						reply.allowed_actions = [:prev, :next]
805						reply.command << form
806					}.then do |iq|
807						if iq.prev?
808							Snikket.new(@customer, @tel, old: @instance, db: @db).write
809						else
810							GetInvite.for(@customer, @instance, @tel, db: @db).then(&:write)
811						end
812					end
813				end
814			end
815
816			class GoToInvite
817				def initialize(xmpp_uri)
818					@xmpp_uri = xmpp_uri
819				end
820
821				def write
822					Command.finish do |reply|
823						oob = OOB.find_or_create(reply.command)
824						oob.url = @xmpp_uri
825					end
826				end
827			end
828		end
829
830		class CustomDomain < Snikket
831			def initialize(customer, tel, old: nil, error: nil, db: DB)
832				@customer = customer
833				@tel = tel
834				@error = error
835				@old = old
836				@db = db
837			end
838
839			def form
840				FormTemplate.render(
841					"registration/snikket_custom",
842					tel: @tel,
843					error: @error
844				)
845			end
846
847			def write
848				Command.reply { |reply|
849					reply.allowed_actions = [:prev, :next]
850					reply.command << form
851				}.then do |iq|
852					if iq.prev?
853						Snikket.new(@customer, @tel, db: @db, old: @old).write
854					else
855						launch(empty_nil(iq.form.field("domain")&.value) || @old&.domain)
856					end
857				end
858			end
859		end
860
861		class NotOnboarding
862			def initialize(customer, tel)
863				@customer = customer
864				@tel = tel
865			end
866
867			def write
868				WelcomeMessage.new(@customer, @tel).welcome
869				Command.finish("Your JMP account has been activated as #{@tel}")
870			end
871		end
872	end
873end