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