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