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