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