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				@addr = ELECTRUM.createnewaddress
174			end
175
176			attr_reader :reply, :customer_id, :tel
177
178			def legacy_session_save
179				sid = SecureRandom.hex
180				REDIS.mset(
181					"reg-sid_for-#{customer_id}", sid,
182					"reg-session_tel-#{sid}", tel
183				)
184			end
185
186			def save
187				EMPromise.all([
188					legacy_session_save,
189					REDIS.mset(
190						"pending_tel_for-#{customer_id}", tel,
191						"pending_plan_for-#{customer_id}", @customer.plan_name
192					),
193					@addr.then do |addr|
194						REDIS.sadd("jmp_customer_btc_addresses-#{customer_id}", addr)
195					end
196				])
197			end
198
199			def note_text(amount, addr)
200				<<~NOTE
201					Activate your account by sending at least #{'%.6f' % amount} BTC to
202					#{addr}
203
204					You will receive a notification when your payment is complete.
205				NOTE
206			end
207
208			def write
209				EMPromise.all([
210					@addr,
211					save,
212					BTC_SELL_PRICES.public_send(@customer.currency.to_s.downcase)
213				]).then do |(addr, _, rate)|
214					min = CONFIG[:activation_amount] / rate
215					reply.note_text = note_text(min, addr)
216					BLATHER << reply
217					nil
218				end
219			end
220		end
221
222		class CreditCard
223			Payment.kinds[:credit_card] = ->(*args) { self.for(*args) }
224
225			def self.for(iq, customer, tel)
226				customer.payment_methods.then do |payment_methods|
227					if (method = payment_methods.default_payment_method)
228						Activate.new(iq, customer, method, tel)
229					else
230						new(iq, customer, tel)
231					end
232				end
233			end
234
235			def initialize(iq, customer, tel)
236				@customer = customer
237				@tel = tel
238
239				@reply = iq.reply
240				@reply.status = :executing
241				@reply.allowed_actions = [:next]
242				@reply.note_type = :info
243				@reply.note_text = "#{oob.desc}: #{oob.url}"
244			end
245
246			attr_reader :reply
247
248			def oob
249				oob = OOB.find_or_create(@reply.command)
250				oob.url = CONFIG[:credit_card_url].call(
251					reply.to.stripped.to_s.gsub("\\", "%5C"),
252					@customer.customer_id
253				)
254				oob.desc = "Add credit card, then return here and choose next"
255				oob
256			end
257
258			def write
259				COMMAND_MANAGER.write(@reply).then do |riq|
260					CreditCard.for(riq, @customer, @tel).write
261				end
262			end
263
264			class Activate
265				def initialize(iq, customer, payment_method, tel)
266					@iq = iq
267					@customer = customer
268					@payment_method = payment_method
269					@tel = tel
270				end
271
272				def write
273					Transaction.sale(
274						@customer,
275						amount: CONFIG[:activation_amount],
276						payment_method: @payment_method
277					).then(
278						method(:sold),
279						->(_) { declined }
280					)
281				end
282
283			protected
284
285				def sold(tx)
286					tx.insert.then {
287						@customer.bill_plan
288					}.then do
289						Finish.new(@iq, @customer, @tel).write
290					end
291				end
292
293				DECLINE_MESSAGE =
294					"Your bank declined the transaction. " \
295					"Often this happens when a person's credit card " \
296					"is a US card that does not support international " \
297					"transactions, as JMP is not based in the USA, though " \
298					"we do support transactions in USD.\n\n" \
299					"If you were trying a prepaid card, you may wish to use "\
300					"Privacy.com instead, as they do support international " \
301					"transactions.\n\n " \
302					"You may add another card and then choose next"
303
304				def decline_oob(reply)
305					oob = OOB.find_or_create(reply.command)
306					oob.url = CONFIG[:credit_card_url].call(
307						reply.to.stripped.to_s.gsub("\\", "%5C"),
308						@customer.customer_id
309					)
310					oob.desc = DECLINE_MESSAGE
311					oob
312				end
313
314				def declined
315					reply = @iq.reply
316					reply_oob = decline_oob(reply)
317					reply.status = :executing
318					reply.allowed_actions = [:next]
319					reply.note_type = :error
320					reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
321					COMMAND_MANAGER.write(reply).then do |riq|
322						CreditCard.for(riq, @customer, @tel).write
323					end
324				end
325			end
326		end
327
328		class InviteCode
329			Payment.kinds[:code] = method(:new)
330
331			class Invalid < StandardError; end
332
333			FIELDS = [{
334				var: "code",
335				type: "text-single",
336				label: "Your invite code",
337				required: true
338			}].freeze
339
340			def initialize(iq, customer, tel, error: nil)
341				@customer = customer
342				@tel = tel
343				@reply = iq.reply
344				@reply.status = :executing
345				@reply.allowed_actions = [:next]
346				@form = @reply.form
347				@form.type = :form
348				@form.title = "Enter Invite Code"
349				@form.instructions = error
350				@form.fields = FIELDS
351			end
352
353			def write
354				COMMAND_MANAGER.write(@reply).then do |iq|
355					guard_too_many_tries.then {
356						verify(iq.form.field("code")&.value&.to_s)
357					}.then {
358						Finish.new(iq, @customer, @tel)
359					}.catch_only(Invalid) { |e|
360						invalid_code(iq, e)
361					}.then(&:write)
362				end
363			end
364
365		protected
366
367			def guard_too_many_tries
368				REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t|
369					raise Invalid, "Too many wrong attempts" if t > 10
370				end
371			end
372
373			def invalid_code(iq, e)
374				EMPromise.all([
375					REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do
376						REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60)
377					end,
378					InviteCode.new(iq, @customer, @tel, error: e.message)
379				]).then(&:last)
380			end
381
382			def customer_id
383				@customer.customer_id
384			end
385
386			def verify(code)
387				EM.promise_fiber do
388					DB.transaction do
389						valid = DB.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive?
390							UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
391							WHERE code=$2 AND used_by_id IS NULL
392						SQL
393						raise Invalid, "Not a valid invite code: #{code}" unless valid
394						@customer.activate_plan_starting_now
395					end
396				end
397			end
398		end
399	end
400
401	class Finish
402		def initialize(iq, customer, tel)
403			@reply = iq.reply
404			@reply.status = :completed
405			@reply.note_type = :info
406			@reply.note_text = "Your JMP account has been activated as #{tel}"
407			@customer = customer
408			@tel = tel
409		end
410
411		def write
412			BandwidthTNOrder.create(@tel).then(&:poll).then(
413				->(_) { customer_active_tel_purchased },
414				lambda do |_|
415					@reply.note_type = :error
416					@reply.note_text =
417						"The JMP number #{@tel} is no longer available, " \
418						"please visit https://jmp.chat and choose another."
419					BLATHER << @reply
420				end
421			)
422		end
423
424	protected
425
426		def cheogram_sip_addr
427			"sip:#{ERB::Util.url_encode(@reply.to.stripped.to_s)}@sip.cheogram.com"
428		end
429
430		def customer_active_tel_purchased
431			@customer.register!(@tel).then {
432				EMPromise.all([
433					REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
434					REDIS.set(
435						"catapult_fwd_timeout-#{@reply.to.stripped}",
436						25 # ~5 seconds / ring, 5 rings
437					)
438				])
439			}.then { BLATHER << @reply }
440		end
441	end
442end