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