-- Per-bill, per-board-member secure approval tokens for email approve/deny. -- Updated to use approver_name (post bill_approvals.vendor_name rename) and -- populate approved_by/created_by from the board member's user_id. CREATE TABLE IF NOT EXISTS public.bill_approval_email_tokens ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), bill_id uuid NOT NULL REFERENCES public.bills(id) ON DELETE CASCADE, board_member_id uuid NOT NULL REFERENCES public.board_members(id) ON DELETE CASCADE, token uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE, email text NOT NULL, member_name text, sent_at timestamptz, acted_at timestamptz, action text CHECK (action IN ('approved','denied')), notes text, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (bill_id, board_member_id) ); CREATE INDEX IF NOT EXISTS idx_baet_token ON public.bill_approval_email_tokens(token); CREATE INDEX IF NOT EXISTS idx_baet_bill ON public.bill_approval_email_tokens(bill_id); ALTER TABLE public.bill_approval_email_tokens ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Staff manage bill approval tokens" ON public.bill_approval_email_tokens; CREATE POLICY "Staff manage bill approval tokens" ON public.bill_approval_email_tokens TO authenticated USING (has_role(auth.uid(),'admin') OR has_role(auth.uid(),'manager')) WITH CHECK (has_role(auth.uid(),'admin') OR has_role(auth.uid(),'manager')); -- Lookup RPC for the public page (anonymous-friendly, security definer). -- The "vendor_name" returned here is the bill's vendor (from public.vendors), -- not the approver. CREATE OR REPLACE FUNCTION public.lookup_bill_approval_by_token(p_token uuid) RETURNS TABLE ( bill_id uuid, member_name text, member_email text, acted_at timestamptz, action text, vendor_name text, invoice_number text, bill_date date, due_date date, amount numeric, description text, attachment_url text, bill_status text, association_name text ) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT t.bill_id, t.member_name, t.email AS member_email, t.acted_at, t.action, v.name AS vendor_name, b.invoice_number, b.bill_date, b.due_date, b.amount, b.description, b.attachment_url, b.status AS bill_status, a.name AS association_name FROM bill_approval_email_tokens t JOIN bills b ON b.id = t.bill_id LEFT JOIN vendors v ON v.id = b.vendor_id LEFT JOIN associations a ON a.id = b.association_id WHERE t.token = p_token; $$; GRANT EXECUTE ON FUNCTION public.lookup_bill_approval_by_token(uuid) TO anon, authenticated; -- Record action via token. Anonymous callers identify via the secret token; -- the linked board_member.user_id (if any) is the audit identity we record. CREATE OR REPLACE FUNCTION public.record_bill_approval_by_token( p_token uuid, p_action text, p_notes text DEFAULT NULL ) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE tok bill_approval_email_tokens%ROWTYPE; bill bills%ROWTYPE; v_user_id uuid; BEGIN IF p_action NOT IN ('approved','denied') THEN RETURN jsonb_build_object('ok', false, 'error', 'Invalid action'); END IF; SELECT * INTO tok FROM bill_approval_email_tokens WHERE token = p_token; IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'error', 'Invalid token'); END IF; IF tok.acted_at IS NOT NULL THEN RETURN jsonb_build_object( 'ok', false, 'already_voted', true, 'error', 'You have already responded', 'action', tok.action ); END IF; SELECT * INTO bill FROM bills WHERE id = tok.bill_id; IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'error', 'Bill not found'); END IF; SELECT user_id INTO v_user_id FROM board_members WHERE id = tok.board_member_id; INSERT INTO bill_approvals ( bill_id, association_id, approver_name, amount, status, notes, approved_date, approved_by, created_by ) VALUES ( tok.bill_id, bill.association_id, COALESCE(tok.member_name, 'Board Member'), bill.amount, p_action, p_notes, CURRENT_DATE, v_user_id, v_user_id ); -- public.bills.status is derived by the recompute_bill_status_from_approvals -- trigger from the votes — don't force-update here. UPDATE bill_approval_email_tokens SET acted_at = now(), action = p_action, notes = p_notes WHERE token = p_token; RETURN jsonb_build_object('ok', true, 'action', p_action); END; $$; GRANT EXECUTE ON FUNCTION public.record_bill_approval_by_token(uuid, text, text) TO anon, authenticated;