invites_repo.rb

 1# frozen_string_literal: true
 2
 3class InvitesRepo
 4	class Invalid < StandardError; end
 5
 6	def initialize(db=DB, redis=REDIS)
 7		@db = db
 8		@redis = redis
 9	end
10
11	def unused_invites(customer_id)
12		promise = @db.query_defer(<<~SQL, [customer_id])
13			SELECT code FROM unused_invites WHERE creator_id=$1
14		SQL
15		promise.then { |result| result.map { |row| row["code"] } }
16	end
17
18	def claim_code(customer_id, code, &blk)
19		guard_too_many_tries(customer_id).then do
20			@db.transaction do
21				valid = @db.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive?
22					UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP
23					WHERE code=$2 AND used_by_id IS NULL
24				SQL
25				invalid_code(customer_id, code).sync unless valid
26
27				blk.call
28			end
29		end
30	end
31
32	CREATE_N_SQL = <<~SQL
33		INSERT INTO invites
34			SELECT unnest(array_fill($1::text, array[$2::int]))
35		RETURNING code
36	SQL
37
38	def create_n_codes(customer_id, num)
39		EMPromise.resolve(nil).then {
40			codes = @db.exec(CREATE_N_SQL, [customer_id, num])
41			raise Invalid, "Failed to fetch codes" unless codes.cmd_tuples.positive?
42
43			codes.map { |row| row["code"] }
44		}
45	end
46
47	def any_existing?(codes)
48		promise = @db.query_one(<<~SQL, [codes])
49			SELECT count(1) FROM invites WHERE code = ANY($1)
50		SQL
51		promise.then { |result| result[:count].positive? }
52	end
53
54	def any_claimed?(codes)
55		promise = @db.query_one(<<~SQL, [codes])
56			SELECT count(1) FROM invites WHERE code = ANY($1) AND used_by_id IS NOT NULL
57		SQL
58		promise.then { |result| result[:count].positive? }
59	end
60
61	def create_codes(customer_id, codes)
62		custs = [customer_id] * codes.length
63		EMPromise.resolve(nil).then {
64			@db.transaction do
65				valid = @db.exec(<<~SQL, [custs, codes]).cmd_tuples.positive?
66					INSERT INTO invites(creator_id, code) SELECT unnest($1), unnest($2)
67				SQL
68				raise Invalid, "Failed to insert one of: #{codes}" unless valid
69			end
70		}
71	end
72
73	def delete_codes(codes)
74		EMPromise.resolve(nil).then {
75			@db.exec(<<~SQL, [codes])
76				DELETE FROM invites WHERE code = ANY($1)
77			SQL
78		}
79	end
80
81protected
82
83	def guard_too_many_tries(customer_id)
84		@redis.get("jmp_invite_tries-#{customer_id}").then do |t|
85			raise Invalid, "Too many wrong attempts" if t.to_i > 10
86		end
87	end
88
89	def invalid_code(customer_id, code)
90		@redis.incr("jmp_invite_tries-#{customer_id}").then {
91			@redis.expire("jmp_invite_tries-#{customer_id}", 60 * 60)
92		}.then do
93			raise Invalid, "Not a valid invite code: #{code}"
94		end
95	end
96end