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 { |i|
114					i << txt
115				}
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 { |addrs|
206					addrs.first || @customer.add_btc_address
207				}
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
384						@customer.activate_plan_starting_now
385					end
386				end
387			end
388		end
389
390		class Mail
391			Payment.kinds[:mail] = method(:new)
392
393			def initialize(_customer, _tel, final_message: nil, **)
394				@final_message = final_message
395			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					"#{@final_message}"
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				->(_) { number_purchase_error }
436			)
437		end
438
439	protected
440
441		def number_purchase_error
442			TEL_SELECTIONS.delete(@customer.jid).then {
443				TelSelections::ChooseTel.new.choose_tel(
444					error: "The JMP number #{@tel} is no longer available."
445				)
446			}.then { |tel| Finish.new(@customer, tel).write }
447		end
448
449		def raise_setup_error(e)
450			Command.log.error "@customer.register! failed", e
451			Command.finish(
452				"There was an error setting up your number, " \
453				"please contact JMP support.",
454				type: :error
455			)
456		end
457
458		def customer_active_tel_purchased
459			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
460				EMPromise.all([
461					REDIS.del("pending_tel_for-#{@customer.jid}"),
462					Bwmsgsv2Repo.new.put_fwd(@customer.customer_id, @tel, CustomerFwd.for(
463						uri: "xmpp:#{@customer.jid}", timeout: 25 # ~5s / ring, 5 rings
464					))
465				])
466			}.then do
467				Command.finish("Your JMP account has been activated as #{@tel}")
468			end
469		end
470	end
471end