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