registration.rb

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