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 note_text(rate, addr)
191				amount = CONFIG[:activation_amount] / rate
192				<<~NOTE
193					Activate your account by sending at least #{'%.6f' % amount} BTC to
194					#{addr}
195
196					You will receive a notification when your payment is complete.
197				NOTE
198			end
199
200			def write
201				EMPromise.all([addr_and_rate, save]).then do |((addr, rate), _)|
202					Command.reply { |reply|
203						reply.allowed_actions = [:prev]
204						reply.status = :canceled
205						reply.note_type = :info
206						reply.note_text = note_text(rate, addr) + @final_message.to_s
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(customer, tel, finish: Finish, **)
233				customer.payment_methods.then do |payment_methods|
234					if (method = payment_methods.default_payment_method)
235						Activate.new(customer, method, tel, finish: finish)
236					else
237						new(customer, tel, finish: finish)
238					end
239				end
240			end
241
242			def initialize(customer, tel, finish: Finish)
243				@customer = customer
244				@tel = tel
245				@finish = finish
246			end
247
248			def oob(reply)
249				oob = OOB.find_or_create(reply.command)
250				oob.url = CONFIG[:credit_card_url].call(
251					reply.to.stripped.to_s.gsub("\\", "%5C"),
252					@customer.customer_id
253				)
254				oob.desc = "Add credit card, save, then next here to continue"
255				oob
256			end
257
258			def write
259				Command.reply { |reply|
260					reply.allowed_actions = [:next, :prev]
261					toob = oob(reply)
262					reply.note_type = :info
263					reply.note_text = "#{toob.desc}: #{toob.url}"
264				}.then do |iq|
265					next Activation.for(@customer, @tel).then(&:write) if iq.prev?
266
267					CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
268				end
269			end
270
271			class Activate
272				def initialize(customer, payment_method, tel, finish: Finish)
273					@customer = customer
274					@payment_method = payment_method
275					@tel = tel
276					@finish = finish
277				end
278
279				def write
280					CreditCardSale.create(
281						@customer,
282						amount: CONFIG[:activation_amount],
283						payment_method: @payment_method
284					).then(
285						->(_) { sold },
286						->(_) { declined }
287					)
288				end
289
290			protected
291
292				def sold
293					BillPlan.new(@customer, @tel, finish: @finish).write
294				end
295
296				DECLINE_MESSAGE =
297					"Your bank declined the transaction. " \
298					"Often this happens when a person's credit card " \
299					"is a US card that does not support international " \
300					"transactions, as JMP is not based in the USA, though " \
301					"we do support transactions in USD.\n\n" \
302					"If you were trying a prepaid card, you may wish to use "\
303					"Privacy.com instead, as they do support international " \
304					"transactions.\n\n " \
305					"You may add another card"
306
307				def decline_oob(reply)
308					oob = OOB.find_or_create(reply.command)
309					oob.url = CONFIG[:credit_card_url].call(
310						reply.to.stripped.to_s.gsub("\\", "%5C"),
311						@customer.customer_id
312					)
313					oob.desc = DECLINE_MESSAGE
314					oob
315				end
316
317				def declined
318					Command.reply { |reply|
319						reply_oob = decline_oob(reply)
320						reply.allowed_actions = [:next]
321						reply.note_type = :error
322						reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
323					}.then do
324						CreditCard.for(@customer, @tel, finish: @finish).then(&:write)
325					end
326				end
327			end
328		end
329
330		class InviteCode
331			Payment.kinds[:code] = method(:new)
332
333			FIELDS = [{
334				var: "code",
335				type: "text-single",
336				label: "Your referral code",
337				required: true
338			}].freeze
339
340			def initialize(customer, tel, error: nil, **)
341				@customer = customer
342				@tel = tel
343				@error = error
344			end
345
346			def add_form(reply)
347				form = reply.form
348				form.type = :form
349				form.title = "Enter Referral Code"
350				form.instructions = @error if @error
351				form.fields = FIELDS
352			end
353
354			def write
355				Command.reply { |reply|
356					reply.allowed_actions = [:next, :prev]
357					add_form(reply)
358				}.then(&method(:parse))
359			end
360
361			def parse(iq)
362				return Activation.for(@customer, @tel).then(&:write) if iq.prev?
363
364				guard_too_many_tries.then {
365					verify(iq.form.field("code")&.value&.to_s)
366				}.then {
367					Finish.new(@customer, @tel)
368				}.catch_only(InvitesRepo::Invalid, &method(:invalid_code)).then(&:write)
369			end
370
371		protected
372
373			def guard_too_many_tries
374				REDIS.get("jmp_invite_tries-#{customer_id}").then do |t|
375					raise InvitesRepo::Invalid, "Too many wrong attempts" if t.to_i > 10
376				end
377			end
378
379			def invalid_code(e)
380				EMPromise.all([
381					REDIS.incr("jmp_invite_tries-#{customer_id}").then do
382						REDIS.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
383					end,
384					InviteCode.new(@customer, @tel, error: e.message)
385				]).then(&:last)
386			end
387
388			def customer_id
389				@customer.customer_id
390			end
391
392			def verify(code)
393				InvitesRepo.new(DB).claim_code(customer_id, code) do
394					@customer.activate_plan_starting_now
395				end
396			end
397		end
398
399		class Mail
400			Payment.kinds[:mail] = method(:new)
401
402			def initialize(customer, tel, final_message: nil, **)
403				@customer = customer
404				@tel = tel
405				@final_message = final_message
406			end
407
408			def form
409				FormTemplate.render(
410					"registration/mail",
411					currency: @customer.currency,
412					final_message: @final_message
413				)
414			end
415
416			def write
417				Command.reply { |reply|
418					reply.allowed_actions = [:prev]
419					reply.status = :canceled
420					reply.command << form
421				}.then { |iq|
422					raise "Action not allowed" unless iq.prev?
423
424					Activation.for(@customer, @tel).then(&:write)
425				}
426			end
427		end
428	end
429
430	class BillPlan
431		def initialize(customer, tel, finish: Finish)
432			@customer = customer
433			@tel = tel
434			@finish = finish
435		end
436
437		def write
438			@customer.bill_plan(note: "Bill #{@tel} for first month").then do
439				@finish.new(@customer, @tel).write
440			end
441		end
442	end
443
444	class Finish
445		def initialize(customer, tel)
446			@customer = customer
447			@tel = tel
448		end
449
450		def write
451			BandwidthTnReservationRepo.new.get(@customer, @tel).then do |rid|
452				BandwidthTNOrder.create(
453					@tel,
454					customer_order_id: @customer.customer_id,
455					reservation_id: rid
456				).then(&:poll).then(
457					->(_) { customer_active_tel_purchased },
458					method(:number_purchase_error)
459				)
460			end
461		end
462
463	protected
464
465		def number_purchase_error(e)
466			Command.log.error "number_purchase_error", e
467			TEL_SELECTIONS.delete(@customer.jid).then {
468				TelSelections::ChooseTel.new.choose_tel(
469					error: "The JMP number #{@tel} is no longer available."
470				)
471			}.then { |tel| Finish.new(@customer, tel).write }
472		end
473
474		def raise_setup_error(e)
475			Command.log.error "@customer.register! failed", e
476			Command.finish(
477				"There was an error setting up your number, " \
478				"please contact JMP support.",
479				type: :error
480			)
481		end
482
483		def customer_active_tel_purchased
484			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
485				EMPromise.all([
486					REDIS.del("pending_tel_for-#{@customer.jid}"),
487					Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
488						uri: "xmpp:#{@customer.jid}", voicemail_enabled: true
489					))
490				])
491			}.then do
492				Command.finish("Your JMP account has been activated as #{@tel}")
493			end
494		end
495	end
496end