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