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