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, 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, 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, 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, tel)
 57			end
 58		end
 59
 60		def initialize(customer, tel)
 61			@customer = customer
 62			@tel = tel
 63		end
 64
 65		def write
 66			Command.reply { |reply|
 67				reply.allowed_actions = [:next]
 68				reply.note_type = :info
 69				reply.note_text = File.read("#{__dir__}/../fup.txt")
 70			}.then { Activation.for(@customer, @tel).write }
 71		end
 72	end
 73
 74	class Activation
 75		def self.for(customer, tel)
 76			jid = ProxiedJID.new(customer.jid).unproxied
 77			if CONFIG[:approved_domains].key?(jid.domain.to_sym)
 78				Allow.for(customer, tel, jid)
 79			else
 80				new(customer, tel)
 81			end
 82		end
 83
 84		def initialize(customer, tel)
 85			@customer = customer
 86			@tel = tel
 87		end
 88
 89		attr_reader :customer, :tel
 90
 91		def form(center)
 92			FormTemplate.render(
 93				"registration/activate",
 94				tel: tel,
 95				rate_center: center
 96			)
 97		end
 98
 99		def write
100			rate_center.then { |center|
101				Command.reply do |reply|
102					reply.allowed_actions = [:next]
103					reply.command << form(center)
104				end
105			}.then(&method(:next_step))
106		end
107
108		def next_step(iq)
109			EMPromise.resolve(nil).then {
110				Payment.for(iq, customer, tel)
111			}.then(&:write)
112		end
113
114	protected
115
116		def rate_center
117			EM.promise_fiber {
118				center = BandwidthIris::Tn.get(tel).get_rate_center
119				"#{center[:rate_center]}, #{center[:state]}"
120			}.catch { nil }
121		end
122
123		class Allow < Activation
124			def self.for(customer, tel, jid)
125				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
126				new(customer, tel, credit_to)
127			end
128
129			def initialize(customer, tel, credit_to)
130				super(customer, tel)
131				@credit_to = credit_to
132			end
133
134			def form(center)
135				FormTemplate.render(
136					"registration/allow",
137					tel: tel,
138					rate_center: center,
139					domain: customer.jid.domain
140				)
141			end
142
143			def next_step(iq)
144				plan_name = iq.form.field("plan_name").value.to_s
145				@customer = customer.with_plan(plan_name)
146				EMPromise.resolve(nil).then { activate }.then do
147					Finish.new(customer, tel).write
148				end
149			end
150
151		protected
152
153			def activate
154				DB.transaction do
155					if @credit_to
156						DB.exec(<<~SQL, [@credit_to, customer.customer_id])
157							INSERT INTO invites (creator_id, used_by_id, used_at)
158							VALUES ($1, $2, LOCALTIMESTAMP)
159						SQL
160					end
161					@customer.activate_plan_starting_now
162				end
163			end
164		end
165	end
166
167	module Payment
168		def self.kinds
169			@kinds ||= {}
170		end
171
172		def self.for(iq, customer, tel, final_message: nil, finish: Finish)
173			plan_name = iq.form.field("plan_name").value.to_s
174			customer = customer.with_plan(plan_name)
175			customer.save_plan!.then do
176				kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
177					raise "Invalid activation method"
178				}.call(customer, tel, final_message: final_message, finish: finish)
179			end
180		end
181
182		class Bitcoin
183			Payment.kinds[:bitcoin] = method(:new)
184
185			THIRTY_DAYS = 60 * 60 * 24 * 30
186
187			def initialize(customer, tel, final_message: nil, **)
188				@customer = customer
189				@customer_id = customer.customer_id
190				@tel = tel
191				@final_message = final_message
192			end
193
194			attr_reader :customer_id, :tel
195
196			def save
197				REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel)
198			end
199
200			def form(rate, addr)
201				amount = CONFIG[:activation_amount] / rate
202
203				FormTemplate.render(
204					"registration/btc",
205					amount: amount,
206					addr: addr,
207					final_message: @final_message
208				)
209			end
210
211			def write
212				EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
213					Command.reply { |reply|
214						reply.allowed_actions = [:prev]
215						reply.status = :canceled
216						reply.command << form(rate, addr)
217					}.then(&method(:handle_possible_prev))
218				end
219			end
220
221		protected
222
223			def handle_possible_prev(iq)
224				raise "Action not allowed" unless iq.prev?
225
226				Activation.for(@customer, @tel).then(&:write)
227			end
228
229			def addr_and_rate
230				EMPromise.all([
231					@customer.btc_addresses.then { |addrs|
232						addrs.first || @customer.add_btc_address
233					},
234					BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
235				])
236			end
237		end
238
239		class CreditCard
240			Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
241
242			def self.for(in_customer, tel, finish: Finish, **)
243				reload_customer(in_customer).then do |(customer, payment_methods)|
244					if customer.balance >= CONFIG[:activation_amount_accept]
245						next BillPlan.new(customer, tel, finish: finish)
246					end
247
248					if (method = payment_methods.default_payment_method)
249						next Activate.new(customer, method, tel, finish: finish)
250					end
251
252					new(customer, tel, finish: finish)
253				end
254			end
255
256			def self.reload_customer(customer)
257				EMPromise.all([
258					Command.execution.customer_repo.find(customer.customer_id),
259					customer.payment_methods
260				])
261			end
262
263			def initialize(customer, tel, finish: Finish)
264				@customer = customer
265				@tel = tel
266				@finish = finish
267			end
268
269			def oob(reply)
270				oob = OOB.find_or_create(reply.command)
271				oob.url = CONFIG[:credit_card_url].call(
272					reply.to.stripped.to_s.gsub("\\", "%5C"),
273					@customer.customer_id
274				) + "&amount=#{CONFIG[:activation_amount]}"
275				oob.desc = "Add credit card, save, then next here to continue"
276				oob
277			end
278
279			def write
280				Command.reply { |reply|
281					reply.allowed_actions = [:next, :prev]
282					toob = oob(reply)
283					reply.note_type = :info
284					reply.note_text = "#{toob.desc}: #{toob.url}"
285				}.then do |iq|
286					next Activation.for(@customer, @tel).then(&:write) if iq.prev?
287
288					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
289				end
290			end
291
292			class Activate
293				def initialize(customer, payment_method, tel, finish: Finish)
294					@customer = customer
295					@payment_method = payment_method
296					@tel = tel
297					@finish = finish
298				end
299
300				def write
301					CreditCardSale.create(
302						@customer,
303						amount: CONFIG[:activation_amount],
304						payment_method: @payment_method
305					).then(
306						->(_) { sold },
307						->(_) { declined }
308					)
309				end
310
311			protected
312
313				def sold
314					BillPlan.new(@customer, @tel, finish: @finish).write
315				end
316
317				DECLINE_MESSAGE =
318					"Your bank declined the transaction. " \
319					"Often this happens when a person's credit card " \
320					"is a US card that does not support international " \
321					"transactions, as JMP is not based in the USA, though " \
322					"we do support transactions in USD.\n\n" \
323					"You may add another card"
324
325				def decline_oob(reply)
326					oob = OOB.find_or_create(reply.command)
327					oob.url = CONFIG[:credit_card_url].call(
328						reply.to.stripped.to_s.gsub("\\", "%5C"),
329						@customer.customer_id
330					) + "&amount=#{CONFIG[:activation_amount]}"
331					oob.desc = DECLINE_MESSAGE
332					oob
333				end
334
335				def declined
336					Command.reply { |reply|
337						reply_oob = decline_oob(reply)
338						reply.allowed_actions = [:next]
339						reply.note_type = :error
340						reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
341					}.then do
342						CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
343					end
344				end
345			end
346		end
347
348		class InviteCode
349			Payment.kinds[:code] = method(:new)
350
351			FIELDS = [{
352				var: "code",
353				type: "text-single",
354				label: "Your referral code",
355				required: true
356			}].freeze
357
358			def initialize(customer, tel, error: nil, **)
359				@customer = customer
360				@tel = tel
361				@error = error
362			end
363
364			def add_form(reply)
365				form = reply.form
366				form.type = :form
367				form.title = "Enter Referral Code"
368				form.instructions = @error if @error
369				form.fields = FIELDS
370			end
371
372			def write
373				Command.reply { |reply|
374					reply.allowed_actions = [:next, :prev]
375					add_form(reply)
376				}.then(&method(:parse))
377			end
378
379			def parse(iq)
380				return Activation.for(@customer, @tel).then(&:write) if iq.prev?
381
382				guard_too_many_tries.then {
383					verify(iq.form.field("code")&.value&.to_s)
384				}.then {
385					Finish.new(@customer, @tel)
386				}.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
387			end
388
389		protected
390
391			def guard_too_many_tries
392				REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
393					raise InvitesRepo::Invalid, "Too many wrong attempts" if t.to_i > 10
394				end
395			end
396
397			def invalid_code(e)
398				EMPromise.all([
399					REDIS.incr("jmp_invite_tries-#{customer_id}").then do
400						REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
401					end,
402					InviteCode.new(@customer, @tel, error: e.message)
403				]).then(&:last)
404			end
405
406			def customer_id
407				@customer.customer_id
408			end
409
410			def verify(code)
411				InvitesRepo.new(DB).claim_code(customer_id, code) do
412					@customer.activate_plan_starting_now
413				end
414			end
415		end
416
417		class Mail
418			Payment.kinds[:mail] = method(:new)
419
420			def initialize(customer, tel, final_message: nil, **)
421				@customer = customer
422				@tel = tel
423				@final_message = final_message
424			end
425
426			def form
427				FormTemplate.render(
428					"registration/mail",
429					currency: @customer.currency,
430					final_message: @final_message,
431					**onboarding_extras
432				)
433			end
434
435			def onboarding_extras
436				jid = ProxiedJID.new(@customer.jid).unproxied
437				return {} unless jid.domain == CONFIG[:onboarding_domain]
438
439				{
440					customer_id: @customer.customer_id,
441					in_note: "Customer ID"
442				}
443			end
444
445			def write
446				Command.reply { |reply|
447					reply.allowed_actions = [:prev]
448					reply.status = :canceled
449					reply.command << form
450				}.then { |iq|
451					raise "Action not allowed" unless iq.prev?
452
453					Activation.for(@customer, @tel).then(&:write)
454				}
455			end
456		end
457	end
458
459	class BillPlan
460		def initialize(customer, tel, finish: Finish)
461			@customer = customer
462			@tel = tel
463			@finish = finish
464		end
465
466		def write
467			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
468				@finish.new(@customer, @tel).write
469			end
470		end
471	end
472
473	class Finish
474		def initialize(customer, tel)
475			@customer = customer
476			@tel = tel
477		end
478
479		def write
480			BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
481				BandwidthTNOrder.create(
482					@tel,
483					customer_order_id: @customer.customer_id,
484					reservation_id: rid
485				).then(&:poll).then(
486					->(_) { customer_active_tel_purchased },
487					method(:number_purchase_error)
488				)
489			end
490		end
491
492	protected
493
494		def number_purchase_error(e)
495			Command.log.error "number_purchase_error", e
496			TEL_SELECTIONS.delete(@customer.jid).then {
497				TelSelections::ChooseTel.new.choose_tel(
498					error: "The JMP number #{@tel} is no longer available."
499				)
500			}.then { |tel| Finish.new(@customer, tel).write }
501		end
502
503		def raise_setup_error(e)
504			Command.log.error "@customer.register! failed", e
505			Command.finish(
506				"There was an error setting up your number, " \
507				"please contact JMP support.",
508				type: :error
509			)
510		end
511
512		def put_default_fwd
513			Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
514				uri: "xmpp:#{@customer.jid}",
515				voicemail_enabled: true
516			))
517		end
518
519		def customer_active_tel_purchased
520			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
521				EMPromise.all([
522					REDIS.del("pending_tel_for-#{@customer.jid}"),
523					put_default_fwd
524				])
525			}.then do
526				FinishOnboarding.for(@customer, @tel).then(&:write)
527			end
528		end
529	end
530
531	module FinishOnboarding
532		def self.for(customer, tel, db: LazyObject.new { DB })
533			jid = ProxiedJID.new(customer.jid).unproxied
534			if jid.domain == CONFIG[:onboarding_domain]
535				Snikket.for(customer, tel, db: db)
536			else
537				NotOnboarding.new(customer, tel)
538			end
539		end
540
541		class Snikket
542			def self.for(customer, tel, db:)
543				::Snikket::Repo.new(db: db).find_by_customer(customer).then do |is|
544					if is.empty?
545						new(customer, tel, db: db)
546					else
547						GetInvite.for(is[0])
548					end
549				end
550			end
551
552			def initialize(customer, tel, error: nil, db:)
553				@customer = customer
554				@tel = tel
555				@error = error
556				@db = db
557			end
558
559			ACTION_VAR = "http://jabber.org/protocol/commands#actions"
560
561			def write
562				Command.reply { |reply|
563					reply.allowed_actions = [:next]
564					reply.command << form
565				}.then do |iq|
566					if iq.form.field(ACTION_VAR)&.value == "custom_domain"
567						CustomDomain.new(@tel).write
568					else
569						launch("#{iq.form.field('subdomain')&.value}.snikket.chat")
570					end
571				end
572			end
573
574			def form
575				FormTemplate.render(
576					"registration/snikket",
577					tel: @tel,
578					error: @error
579				)
580			end
581
582			def launch(domain)
583				IQ_MANAGER.write(::Snikket::Launch.new(
584					nil, CONFIG[:snikket_hosting_api], domain: domain
585				)).then { |launched|
586					save_instance_and_wait(domain, launched)
587				}.catch { |e|
588					next EMPromise.reject(e) unless e.respond_to?(:text)
589
590					Snikket.new(@customer, @tel, error: e.text, db: @db).write
591				}
592			end
593
594			def save_instance_and_wait(domain, launched)
595				instance = ::Snikket::CustomerInstance.for(@customer, domain, launched)
596				::Snikket::Repo.new(db: @db).put(instance).then do
597					GetInvite.for(instance).then(&:write)
598				end
599			end
600
601			class GetInvite
602				def self.for(instance)
603					instance.fetch_invite.then do |xmpp_uri|
604						if xmpp_uri
605							GoToInvite.new(xmpp_uri)
606						else
607							new(instance)
608						end
609					end
610				end
611
612				def initialize(instance)
613					@instance = instance
614				end
615
616				def write
617					Command.reply { |reply|
618						reply.allowed_actions = [:next]
619						reply.note_type = :info
620						reply.note_text =
621							"Your instance #{@instance.domain} is starting up. " \
622							"This may take several minutes. " \
623							"Press next to check if it is ready."
624					}.then { GetInvite.for(@instance).then(&:write) }
625				end
626			end
627
628			class GoToInvite
629				def initialize(xmpp_uri)
630					@xmpp_uri = xmpp_uri
631				end
632
633				def write
634					Command.finish do |reply|
635						oob = OOB.find_or_create(reply.command)
636						oob.url = @xmpp_uri
637					end
638				end
639			end
640		end
641
642		class CustomDomain
643			def initialize(tel)
644				@tel = tel
645			end
646
647			CONTACT_SUPPORT =
648				"Please contact JMP support to set up " \
649				"an instance on an existing domain."
650
651			def write
652				Command.reply { |reply|
653					reply.allowed_actions = [:prev]
654					reply.status = :canceled
655					reply.note_type = :info
656					reply.note_text = CONTACT_SUPPORT
657				}.then do |iq|
658					raise "Action not allowed" unless iq.prev?
659
660					Snikket.new(@tel).write
661				end
662			end
663		end
664
665		class NotOnboarding
666			def initialize(customer, tel)
667				@customer = customer
668				@tel = tel
669			end
670
671			def write
672				WelcomeMessage.new(@customer, @tel).welcome
673				Command.finish("Your JMP account has been activated as #{@tel}")
674			end
675		end
676	end
677end