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