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