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