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 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