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