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