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