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"
 16
 17class Registration
 18	def self.for(customer, tel_selections)
 19		if (reg = customer.registered?)
 20			Registered.new(reg.phone)
 21		else
 22			tel_selections[customer.jid].then(&:choose_tel).then do |tel|
 23				BandwidthTnReservationRepo.new.ensure(customer, tel)
 24				FinishOrStartActivation.for(customer, tel)
 25			end
 26		end
 27	end
 28
 29	class Registered
 30		def initialize(tel)
 31			@tel = tel
 32		end
 33
 34		def write
 35			Command.finish("You are already registered with JMP number #{@tel}")
 36		end
 37	end
 38
 39	class FinishOrStartActivation
 40		def self.for(customer, tel)
 41			if customer.active?
 42				Finish.new(customer, tel)
 43			elsif customer.balance >= CONFIG[:activation_amount_accept]
 44				BillPlan.new(customer, tel)
 45			else
 46				new(customer, tel)
 47			end
 48		end
 49
 50		def initialize(customer, tel)
 51			@customer = customer
 52			@tel = tel
 53		end
 54
 55		def write
 56			Command.reply { |reply|
 57				reply.allowed_actions = [:next]
 58				reply.note_type = :info
 59				reply.note_text = File.read("#{__dir__}/../fup.txt")
 60			}.then { Activation.for(@customer, @tel).write }
 61		end
 62	end
 63
 64	class Activation
 65		def self.for(customer, tel)
 66			jid = ProxiedJID.new(customer.jid).unproxied
 67			if CONFIG[:approved_domains].key?(jid.domain.to_sym)
 68				Allow.for(customer, tel, jid)
 69			else
 70				new(customer, tel)
 71			end
 72		end
 73
 74		def initialize(customer, tel)
 75			@customer = customer
 76			@tel = tel
 77		end
 78
 79		attr_reader :customer, :tel
 80
 81		def form(center)
 82			FormTemplate.render(
 83				"registration/activate",
 84				tel: tel,
 85				rate_center: center
 86			)
 87		end
 88
 89		def write
 90			rate_center.then { |center|
 91				Command.reply do |reply|
 92					reply.allowed_actions = [:next]
 93					reply.command << form(center)
 94				end
 95			}.then(&method(:next_step))
 96		end
 97
 98		def next_step(iq)
 99			EMPromise.resolve(nil).then {
100				Payment.for(iq, customer, tel)
101			}.then(&:write)
102		end
103
104	protected
105
106		def rate_center
107			EM.promise_fiber {
108				center = BandwidthIris::Tn.get(tel).get_rate_center
109				"#{center[:rate_center]}, #{center[:state]}"
110			}.catch { nil }
111		end
112
113		class Allow < Activation
114			def self.for(customer, tel, jid)
115				credit_to = CONFIG[:approved_domains][jid.domain.to_sym]
116				new(customer, tel, credit_to)
117			end
118
119			def initialize(customer, tel, credit_to)
120				super(customer, tel)
121				@credit_to = credit_to
122			end
123
124			def form(center)
125				FormTemplate.render(
126					"registration/allow",
127					tel: tel,
128					rate_center: center,
129					domain: customer.jid.domain
130				)
131			end
132
133			def next_step(iq)
134				plan_name = iq.form.field("plan_name").value.to_s
135				@customer = customer.with_plan(plan_name)
136				EMPromise.resolve(nil).then { activate }.then do
137					Finish.new(customer, tel).write
138				end
139			end
140
141		protected
142
143			def activate
144				DB.transaction do
145					if @credit_to
146						DB.exec(<<~SQL, [@credit_to, customer.customer_id])
147							INSERT INTO invites (creator_id, used_by_id, used_at)
148							VALUES ($1, $2, LOCALTIMESTAMP)
149						SQL
150					end
151					@customer.activate_plan_starting_now
152				end
153			end
154		end
155	end
156
157	module Payment
158		def self.kinds
159			@kinds ||= {}
160		end
161
162		def self.for(iq, customer, tel, final_message: nil, finish: Finish)
163			plan_name = iq.form.field("plan_name").value.to_s
164			customer = customer.with_plan(plan_name)
165			customer.save_plan!.then do
166				kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
167					raise "Invalid activation method"
168				}.call(customer, tel, final_message: final_message, finish: finish)
169			end
170		end
171
172		class Bitcoin
173			Payment.kinds[:bitcoin] = method(:new)
174
175			THIRTY_DAYS = 60 * 60 * 24 * 30
176
177			def initialize(customer, tel, final_message: nil, **)
178				@customer = customer
179				@customer_id = customer.customer_id
180				@tel = tel
181				@final_message = final_message
182			end
183
184			attr_reader :customer_id, :tel
185
186			def save
187				REDIS.setex("pending_tel_for-#{@customer.jid}", THIRTY_DAYS, tel)
188			end
189
190			def form(rate, addr)
191				amount = CONFIG[:activation_amount] / rate
192
193				FormTemplate.render(
194					"registration/btc",
195					amount: amount,
196					addr: addr,
197					final_message: @final_message
198				)
199			end
200
201			def write
202				EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
203					Command.reply { |reply|
204						reply.allowed_actions = [:prev]
205						reply.status = :canceled
206						reply.command << form(rate, addr)
207					}.then(&method(:handle_possible_prev))
208				end
209			end
210
211		protected
212
213			def handle_possible_prev(iq)
214				raise "Action not allowed" unless iq.prev?
215
216				Activation.for(@customer, @tel).then(&:write)
217			end
218
219			def addr_and_rate
220				EMPromise.all([
221					@customer.btc_addresses.then { |addrs|
222						addrs.first || @customer.add_btc_address
223					},
224					BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
225				])
226			end
227		end
228
229		class CreditCard
230			Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) }
231
232			def self.for(in_customer, tel, finish: Finish, **)
233				reload_customer(in_customer).then do |(customer, payment_methods)|
234					if customer.balance >= CONFIG[:activation_amount_accept]
235						next BillPlan.new(customer, tel, finish: finish)
236					end
237
238					if (method = payment_methods.default_payment_method)
239						next Activate.new(customer, method, tel, finish: finish)
240					end
241
242					new(customer, tel, finish: finish)
243				end
244			end
245
246			def self.reload_customer(customer)
247				EMPromise.all([
248					Command.execution.customer_repo.find(customer.customer_id),
249					customer.payment_methods
250				])
251			end
252
253			def initialize(customer, tel, finish: Finish)
254				@customer = customer
255				@tel = tel
256				@finish = finish
257			end
258
259			def oob(reply)
260				oob = OOB.find_or_create(reply.command)
261				oob.url = CONFIG[:credit_card_url].call(
262					reply.to.stripped.to_s.gsub("\\", "%5C"),
263					@customer.customer_id
264				) + "&amount=#{CONFIG[:activation_amount]}"
265				oob.desc = "Add credit card, save, then next here to continue"
266				oob
267			end
268
269			def write
270				Command.reply { |reply|
271					reply.allowed_actions = [:next, :prev]
272					toob = oob(reply)
273					reply.note_type = :info
274					reply.note_text = "#{toob.desc}: #{toob.url}"
275				}.then do |iq|
276					next Activation.for(@customer, @tel).then(&:write) if iq.prev?
277
278					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
279				end
280			end
281
282			class Activate
283				def initialize(customer, payment_method, tel, finish: Finish)
284					@customer = customer
285					@payment_method = payment_method
286					@tel = tel
287					@finish = finish
288				end
289
290				def write
291					CreditCardSale.create(
292						@customer,
293						amount: CONFIG[:activation_amount],
294						payment_method: @payment_method
295					).then(
296						->(_) { sold },
297						->(_) { declined }
298					)
299				end
300
301			protected
302
303				def sold
304					BillPlan.new(@customer, @tel, finish: @finish).write
305				end
306
307				DECLINE_MESSAGE =
308					"Your bank declined the transaction. " \
309					"Often this happens when a person's credit card " \
310					"is a US card that does not support international " \
311					"transactions, as JMP is not based in the USA, though " \
312					"we do support transactions in USD.\n\n" \
313					"You may add another card"
314
315				def decline_oob(reply)
316					oob = OOB.find_or_create(reply.command)
317					oob.url = CONFIG[:credit_card_url].call(
318						reply.to.stripped.to_s.gsub("\\", "%5C"),
319						@customer.customer_id
320					) + "&amount=#{CONFIG[:activation_amount]}"
321					oob.desc = DECLINE_MESSAGE
322					oob
323				end
324
325				def declined
326					Command.reply { |reply|
327						reply_oob = decline_oob(reply)
328						reply.allowed_actions = [:next]
329						reply.note_type = :error
330						reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
331					}.then do
332						CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
333					end
334				end
335			end
336		end
337
338		class InviteCode
339			Payment.kinds[:code] = method(:new)
340
341			FIELDS = [{
342				var: "code",
343				type: "text-single",
344				label: "Your referral code",
345				required: true
346			}].freeze
347
348			def initialize(customer, tel, error: nil, **)
349				@customer = customer
350				@tel = tel
351				@error = error
352			end
353
354			def add_form(reply)
355				form = reply.form
356				form.type = :form
357				form.title = "Enter Referral Code"
358				form.instructions = @error if @error
359				form.fields = FIELDS
360			end
361
362			def write
363				Command.reply { |reply|
364					reply.allowed_actions = [:next, :prev]
365					add_form(reply)
366				}.then(&method(:parse))
367			end
368
369			def parse(iq)
370				return Activation.for(@customer, @tel).then(&:write) if iq.prev?
371
372				guard_too_many_tries.then {
373					verify(iq.form.field("code")&.value&.to_s)
374				}.then {
375					Finish.new(@customer, @tel)
376				}.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
377			end
378
379		protected
380
381			def guard_too_many_tries
382				REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
383					raise InvitesRepo::Invalid, "Too many wrong attempts" if t.to_i > 10
384				end
385			end
386
387			def invalid_code(e)
388				EMPromise.all([
389					REDIS.incr("jmp_invite_tries-#{customer_id}").then do
390						REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
391					end,
392					InviteCode.new(@customer, @tel, error: e.message)
393				]).then(&:last)
394			end
395
396			def customer_id
397				@customer.customer_id
398			end
399
400			def verify(code)
401				InvitesRepo.new(DB).claim_code(customer_id, code) do
402					@customer.activate_plan_starting_now
403				end
404			end
405		end
406
407		class Mail
408			Payment.kinds[:mail] = method(:new)
409
410			def initialize(customer, tel, final_message: nil, **)
411				@customer = customer
412				@tel = tel
413				@final_message = final_message
414			end
415
416			def form
417				FormTemplate.render(
418					"registration/mail",
419					currency: @customer.currency,
420					final_message: @final_message,
421					**onboarding_extras
422				)
423			end
424
425			def onboarding_extras
426				jid = ProxiedJID.new(@customer.jid).unproxied
427				return {} unless jid.domain == CONFIG[:onboarding_domain]
428
429				{
430					customer_id: @customer.customer_id,
431					in_note: "Customer ID"
432				}
433			end
434
435			def write
436				Command.reply { |reply|
437					reply.allowed_actions = [:prev]
438					reply.status = :canceled
439					reply.command << form
440				}.then { |iq|
441					raise "Action not allowed" unless iq.prev?
442
443					Activation.for(@customer, @tel).then(&:write)
444				}
445			end
446		end
447	end
448
449	class BillPlan
450		def initialize(customer, tel, finish: Finish)
451			@customer = customer
452			@tel = tel
453			@finish = finish
454		end
455
456		def write
457			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
458				@finish.new(@customer, @tel).write
459			end
460		end
461	end
462
463	class Finish
464		def initialize(customer, tel)
465			@customer = customer
466			@tel = tel
467		end
468
469		def write
470			BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
471				BandwidthTNOrder.create(
472					@tel,
473					customer_order_id: @customer.customer_id,
474					reservation_id: rid
475				).then(&:poll).then(
476					->(_) { customer_active_tel_purchased },
477					method(:number_purchase_error)
478				)
479			end
480		end
481
482	protected
483
484		def number_purchase_error(e)
485			Command.log.error "number_purchase_error", e
486			TEL_SELECTIONS.delete(@customer.jid).then {
487				TelSelections::ChooseTel.new.choose_tel(
488					error: "The JMP number #{@tel} is no longer available."
489				)
490			}.then { |tel| Finish.new(@customer, tel).write }
491		end
492
493		def raise_setup_error(e)
494			Command.log.error "@customer.register! failed", e
495			Command.finish(
496				"There was an error setting up your number, " \
497				"please contact JMP support.",
498				type: :error
499			)
500		end
501
502		def customer_active_tel_purchased
503			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
504				EMPromise.all([
505					REDIS.del("pending_tel_for-#{@customer.jid}"),
506					Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
507						uri: "xmpp:#{@customer.jid}", voicemail_enabled: true
508					))
509				])
510			}.then do
511				Command.finish("Your JMP account has been activated as #{@tel}")
512			end
513		end
514	end
515end