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