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