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