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