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