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