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