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