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