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