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