CREATE TABLE public.board_vote_email_tokens ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), board_vote_id uuid NOT NULL REFERENCES public.board_votes(id) ON DELETE CASCADE, board_member_id uuid NOT NULL REFERENCES public.board_members(id) ON DELETE CASCADE, token uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(), email text NOT NULL, member_name text, sent_at timestamp with time zone, voted_at timestamp with time zone, vote_option text, created_at timestamp with time zone NOT NULL DEFAULT now(), UNIQUE (board_vote_id, board_member_id) ); CREATE INDEX idx_bvet_token ON public.board_vote_email_tokens(token); CREATE INDEX idx_bvet_vote ON public.board_vote_email_tokens(board_vote_id); ALTER TABLE public.board_vote_email_tokens ENABLE ROW LEVEL SECURITY; CREATE POLICY "Staff manage board vote tokens" ON public.board_vote_email_tokens FOR ALL TO authenticated USING (public.has_role(auth.uid(),'admin'::public.app_role) OR public.has_role(auth.uid(),'manager'::public.app_role)) WITH CHECK (public.has_role(auth.uid(),'admin'::public.app_role) OR public.has_role(auth.uid(),'manager'::public.app_role)); CREATE OR REPLACE FUNCTION public.lookup_board_vote_by_token(p_token uuid) RETURNS TABLE( token_id uuid, board_vote_id uuid, vote_title text, vote_description text, vote_options text[], vote_status text, association_name text, member_name text, email text, voted_at timestamp with time zone, vote_option text ) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT t.id, bv.id, bv.title, bv.description, bv.vote_options, bv.status, a.name, t.member_name, t.email, t.voted_at, t.vote_option FROM public.board_vote_email_tokens t JOIN public.board_votes bv ON bv.id = t.board_vote_id LEFT JOIN public.associations a ON a.id = bv.association_id WHERE t.token = p_token LIMIT 1; $$; CREATE OR REPLACE FUNCTION public.record_board_vote_by_token(p_token uuid, p_option text) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_token RECORD; v_vote RECORD; BEGIN SELECT * INTO v_token FROM public.board_vote_email_tokens WHERE token = p_token; IF v_token IS NULL THEN RETURN jsonb_build_object('ok', false, 'error', 'Invalid token'); END IF; IF v_token.voted_at IS NOT NULL THEN RETURN jsonb_build_object('ok', false, 'error', 'Vote already recorded', 'already_voted', true); END IF; SELECT * INTO v_vote FROM public.board_votes WHERE id = v_token.board_vote_id; IF v_vote.status <> 'open' THEN RETURN jsonb_build_object('ok', false, 'error', 'Voting is closed for this item'); END IF; IF NOT (p_option = ANY(v_vote.vote_options)) THEN RETURN jsonb_build_object('ok', false, 'error', 'Invalid vote option'); END IF; UPDATE public.board_vote_email_tokens SET voted_at = now(), vote_option = p_option WHERE id = v_token.id; RETURN jsonb_build_object('ok', true); END; $$; GRANT EXECUTE ON FUNCTION public.lookup_board_vote_by_token(uuid) TO anon, authenticated; GRANT EXECUTE ON FUNCTION public.record_board_vote_by_token(uuid, text) TO anon, authenticated;