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