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