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