registration.rb

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