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