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