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 "./proxied_jid"
 15require_relative "./tel_selections"
 16require_relative "./welcome_message"
 17
 18class Registration
 19	def self.for(customer, google_play_userid, tel_selections)
 20		if (reg = customer.registered?)
 21			Registered.for(customer, reg.phone)
 22		else
 23			tel_selections[customer.jid].then(&:choose_tel).then do |tel|
 24				BandwidthTnReservationRepo.new.ensure(customer, tel)
 25				FinishOrStartActivation.for(customer, google_play_userid, tel)
 26			end
 27		end
 28	end
 29
 30	class Registered
 31		def self.for(customer, tel)
 32			jid = ProxiedJID.new(customer.jid).unproxied
 33			if jid.domain == CONFIG[:onboarding_domain]
 34				FinishOnboarding.for(customer, tel)
 35			else
 36				new(tel)
 37			end
 38		end
 39
 40		def initialize(tel)
 41			@tel = tel
 42		end
 43
 44		def write
 45			Command.finish("You are already registered with JMP number #{@tel}")
 46		end
 47	end
 48
 49	class FinishOrStartActivation
 50		def self.for(customer, google_play_userid, tel)
 51			if customer.active?
 52				Finish.new(customer, tel)
 53			elsif customer.balance >= CONFIG[:activation_amount_accept]
 54				BillPlan.new(customer, tel)
 55			else
 56				new(customer, google_play_userid, tel)
 57			end
 58		end
 59
 60		def initialize(customer, google_play_userid, tel)
 61			@customer = customer
 62			@tel = tel
 63			@google_play_userid = google_play_userid
 64		end
 65
 66		def write
 67			Command.reply { |reply|
 68				reply.allowed_actions = [:next]
 69				reply.note_type = :info
 70				reply.note_text = File.read("#{__dir__}/../fup.txt")
 71			}.then { Activation.for(@customer, @google_play_userid, @tel).write }
 72		end
 73	end
 74
 75	class Activation
 76		def self.for(customer, google_play_userid, tel)
 77			jid = ProxiedJID.new(customer.jid).unproxied
 78			if CONFIG[:approved_domains].key?(jid.domain.to_sym)
 79				Allow.for(customer, tel, jid)
 80			elsif google_play_userid
 81				GooglePlay.new(customer, google_play_userid, tel)
 82			else
 83				new(customer, tel)
 84			end
 85		end
 86
 87		def initialize(customer, tel)
 88			@customer = customer
 89			@tel = tel
 90			@invites = InvitesRepo.new(DB, REDIS)
 91		end
 92
 93		attr_reader :customer, :tel
 94
 95		def form(center)
 96			FormTemplate.render(
 97				"registration/activate",
 98				tel: tel,
 99				rate_center: center
100			)
101		end
102
103		def write
104			rate_center.then { |center|
105				Command.reply do |reply|
106					reply.allowed_actions = [:next]
107					reply.command << form(center)
108				end
109			}.then(&method(:next_step))
110		end
111
112		def next_step(iq)
113			code = iq.form.field("code")&.value&.to_s
114			save_customer_plan(iq).then {
115				finish_if_valid_invite(code)
116			}.catch_only(InvitesRepo::Invalid) do
117				@invites.stash_code(customer.customer_id, code).then do
118					Payment.for(iq, @customer, @tel).then(&:write)
119				end
120			end
121		end
122
123	protected
124
125		def finish_if_valid_invite(code)
126			@invites.claim_code(@customer.customer_id, code) {
127				@customer.activate_plan_starting_now
128			}.then do
129				Finish.new(@customer, @tel).write
130			end
131		end
132
133		def save_customer_plan(iq)
134			plan_name = iq.form.field("plan_name").value.to_s
135			@customer = @customer.with_plan(plan_name)
136			@customer.save_plan!
137		end
138
139		def rate_center
140			EM.promise_fiber {
141				center = BandwidthIris::Tn.get(tel).get_rate_center
142				"#{center[:rate_center]}, #{center[:state]}"
143			}.catch { nil }
144		end
145
146		class GooglePlay
147			def initialize(customer, google_play_userid, tel)
148				@customer = customer
149				@google_play_userid = google_play_userid
150				@tel = tel
151				@invites = InvitesRepo.new(DB, REDIS)
152			end
153
154			def used
155				REDIS.sismember("google_play_userids", @google_play_userid)
156			end
157
158			def form
159				FormTemplate.render(
160					"registration/google_play",
161					tel: @tel
162				)
163			end
164
165			def write
166				used.then do |u|
167					next Activation.for(@customer, nil, @tel).write if u.to_s == "1"
168
169					Command.reply { |reply|
170						reply.allowed_actions = [:next]
171						reply.command << form
172					}.then(&method(:activate)).then do
173						Finish.new(@customer, @tel).write
174					end
175				end
176			end
177
178			def activate(iq)
179				REDIS.sadd("google_play_userids", @google_play_userid).then {
180					plan_name = iq.form.field("plan_name").value.to_s
181					@customer = @customer.with_plan(plan_name)
182					@customer.activate_plan_starting_now
183				}.then do
184					use_referral_code(iq.form.field("code")&.value&.to_s)
185				end
186			end
187
188		protected
189
190			def use_referral_code(code)
191				@invites.claim_code(@customer.customer_id, code) {
192					@customer.extend_plan
193				}.catch_only(InvitesRepo::Invalid) do
194					@invites.stash_code(customer.customer_id, code)
195				end
196			end
197		end
198
199		class Allow < Activation
200			def self.for(customer, tel, jid)
201				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
202				new(customer, tel, credit_to)
203			end
204
205			def initialize(customer, tel, credit_to)
206				super(customer, tel)
207				@credit_to = credit_to
208			end
209
210			def form(center)
211				FormTemplate.render(
212					"registration/allow",
213					tel: tel,
214					rate_center: center,
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						DB.exec(<<~SQL, [@credit_to, customer.customer_id])
233							INSERT INTO invites (creator_id, used_by_id, used_at)
234							VALUES ($1, $2, LOCALTIMESTAMP)
235						SQL
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] = method(:new)
422
423			FIELDS = [{
424				var: "code",
425				type: "text-single",
426				label: "Your referral code",
427				required: true
428			}].freeze
429
430			def initialize(customer, tel, error: nil, **)
431				@customer = customer
432				@tel = tel
433				@error = error
434			end
435
436			def add_form(reply)
437				form = reply.form
438				form.type = :form
439				form.title = "Enter Referral Code"
440				form.instructions = @error if @error
441				form.fields = FIELDS
442			end
443
444			def write
445				Command.reply { |reply|
446					reply.allowed_actions = [:next, :prev]
447					add_form(reply)
448				}.then(&method(:parse))
449			end
450
451			def parse(iq)
452				return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
453
454				verify(iq.form.field("code")&.value&.to_s).then {
455					Finish.new(@customer, @tel)
456				}.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
457			end
458
459		protected
460
461			def invalid_code(e)
462				InviteCode.new(@customer, @tel, error: e.message)
463			end
464
465			def customer_id
466				@customer.customer_id
467			end
468
469			def verify(code)
470				InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) do
471					@customer.activate_plan_starting_now
472				end
473			end
474		end
475
476		class Mail
477			Payment.kinds[:mail] = method(:new)
478
479			def initialize(customer, tel, final_message: nil, **)
480				@customer = customer
481				@tel = tel
482				@final_message = final_message
483			end
484
485			def form
486				FormTemplate.render(
487					"registration/mail",
488					currency: @customer.currency,
489					final_message: @final_message,
490					**onboarding_extras
491				)
492			end
493
494			def onboarding_extras
495				jid = ProxiedJID.new(@customer.jid).unproxied
496				return {} unless jid.domain == CONFIG[:onboarding_domain]
497
498				{
499					customer_id: @customer.customer_id,
500					in_note: "Customer ID"
501				}
502			end
503
504			def write
505				Command.reply { |reply|
506					reply.allowed_actions = [:prev]
507					reply.status = :canceled
508					reply.command << form
509				}.then { |iq|
510					raise "Action not allowed" unless iq.prev?
511
512					Activation.for(@customer, nil, @tel).then(&:write)
513				}
514			end
515		end
516	end
517
518	class BillPlan
519		def initialize(customer, tel, finish: Finish)
520			@customer = customer
521			@tel = tel
522			@finish = finish
523		end
524
525		def write
526			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
527				@finish.new(@customer, @tel).write
528			end
529		end
530	end
531
532	class Finish
533		def initialize(customer, tel)
534			@customer = customer
535			@tel = tel
536		end
537
538		def write
539			BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
540				BandwidthTNOrder.create(
541					@tel,
542					customer_order_id: @customer.customer_id,
543					reservation_id: rid
544				).then(&:poll).then(
545					->(_) { customer_active_tel_purchased },
546					method(:number_purchase_error)
547				)
548			end
549		end
550
551	protected
552
553		def number_purchase_error(e)
554			Command.log.error "number_purchase_error", e
555			TEL_SELECTIONS.delete(@customer.jid).then {
556				TelSelections::ChooseTel.new.choose_tel(
557					error: "The JMP number #{@tel} is no longer available."
558				)
559			}.then { |tel| Finish.new(@customer, tel).write }
560		end
561
562		def raise_setup_error(e)
563			Command.log.error "@customer.register! failed", e
564			Command.finish(
565				"There was an error setting up your number, " \
566				"please contact JMP support.",
567				type: :error
568			)
569		end
570
571		def put_default_fwd
572			Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
573				uri: "xmpp:#{@customer.jid}",
574				voicemail_enabled: true
575			))
576		end
577
578		def customer_active_tel_purchased
579			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
580				EMPromise.all([
581					REDIS.del("pending_tel_for-#{@customer.jid}"),
582					put_default_fwd
583				])
584			}.then do
585				FinishOnboarding.for(@customer, @tel).then(&:write)
586			end
587		end
588	end
589
590	module FinishOnboarding
591		def self.for(customer, tel, db: LazyObject.new { DB })
592			jid = ProxiedJID.new(customer.jid).unproxied
593			if jid.domain == CONFIG[:onboarding_domain]
594				Snikket.for(customer, tel, db: db)
595			else
596				NotOnboarding.new(customer, tel)
597			end
598		end
599
600		class Snikket
601			def self.for(customer, tel, db:)
602				::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
603					if is.empty?
604						new(customer, tel, db: db)
605					else
606						GetInvite.for(is[0])
607					end
608				end
609			end
610
611			def initialize(customer, tel, error: nil, db:)
612				@customer = customer
613				@tel = tel
614				@error = error
615				@db = db
616			end
617
618			ACTION_VAR = "http://jabber.org/protocol/commands#actions"
619
620			def write
621				Command.reply { |reply|
622					reply.allowed_actions = [:next]
623					reply.command << form
624				}.then do |iq|
625					if iq.form.field(ACTION_VAR)&.value == "custom_domain"
626						CustomDomain.new(@tel).write
627					else
628						launch("#{iq.form.field('subdomain')&.value}.snikket.chat")
629					end
630				end
631			end
632
633			def form
634				FormTemplate.render(
635					"registration/snikket",
636					tel: @tel,
637					error: @error
638				)
639			end
640
641			def launch(domain)
642				IQ_MANAGER.write(::Snikket::Launch.new(
643					nil, CONFIG[:snikket_hosting_api], domain: domain
644				)).then { |launched|
645					save_instance_and_wait(domain, launched)
646				}.catch { |e|
647					next EMPromise.reject(e) unless e.respond_to?(:text)
648
649					Snikket.new(@customer, @tel, error: e.text, db: @db).write
650				}
651			end
652
653			def save_instance_and_wait(domain, launched)
654				instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
655				::Snikket::Repo.new(db: @db).put(instance).then do
656					GetInvite.for(instance).then(&:write)
657				end
658			end
659
660			class GetInvite
661				def self.for(instance)
662					instance.fetch_invite.then do |xmpp_uri|
663						if xmpp_uri
664							GoToInvite.new(xmpp_uri)
665						else
666							new(instance)
667						end
668					end
669				end
670
671				def initialize(instance)
672					@instance = instance
673				end
674
675				def write
676					Command.reply { |reply|
677						reply.allowed_actions = [:next]
678						reply.command << FormTemplate.render(
679							"registration/snikket_wait",
680							domain: @instance.domain
681						)
682					}.then { GetInvite.for(@instance).then(&:write) }
683				end
684			end
685
686			class GoToInvite
687				def initialize(xmpp_uri)
688					@xmpp_uri = xmpp_uri
689				end
690
691				def write
692					Command.finish do |reply|
693						oob = OOB.find_or_create(reply.command)
694						oob.url = @xmpp_uri
695					end
696				end
697			end
698		end
699
700		class CustomDomain
701			def initialize(tel)
702				@tel = tel
703			end
704
705			CONTACT_SUPPORT =
706				"Please contact JMP support to set up " \
707				"an instance on an existing domain."
708
709			def write
710				Command.reply { |reply|
711					reply.allowed_actions = [:prev]
712					reply.status = :canceled
713					reply.note_type = :info
714					reply.note_text = CONTACT_SUPPORT
715				}.then do |iq|
716					raise "Action not allowed" unless iq.prev?
717
718					Snikket.new(@tel).write
719				end
720			end
721		end
722
723		class NotOnboarding
724			def initialize(customer, tel)
725				@customer = customer
726				@tel = tel
727			end
728
729			def write
730				WelcomeMessage.new(@customer, @tel).welcome
731				Command.finish("Your JMP account has been activated as #{@tel}")
732			end
733		end
734	end
735end