registration.rb

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