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