mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Bill approvals: surface approvers, fix email path, schema cleanup
UI - Dashboard BillApprovalsCard: render approver name chips (color-coded by vote status) per pending bill instead of leaving the approver identity invisible. - BillDetailPage: collapse the duplicate "Requested Approvers" card into the existing "Approvers" table. Approve/deny handler now stamps approved_by = auth.uid() for audit trail. - MasterBoardDashboardPage: the "pending approvals for me" count was filtering on a non-existent bill_approvals.approver_user_id column. Replaced with a board_members.member_name -> bill_approvals.approver_name join (matches the RLS policy). - BillApprovalRequestDialog + AIInvoiceParserPage: bill_approvals inserts now set created_by. Database - Rename public.bill_approvals.vendor_name -> approver_name. RLS policies auto-rewritten by ALTER TABLE RENAME COLUMN; the column was misnamed (it stores the approver's board-member name, never a vendor). - Restore the bill_approval_email_tokens table + lookup_/record_ bill_approval_by_token RPCs. The original 20260520153409 migration was never applied successfully; rewrote it to use approver_name and to populate approved_by/created_by from board_members.user_id on token-driven votes. Added the v2 migration that matches the live DB state. - accounting trigger: void on accounting.bills cascades to public.bills.status='cancelled' (existing forward sync then drops the accounting row per accounting.bill_should_mirror). Edge function - send-transactional-email: add bill-approval-request and bill-approval-vote-invite templates (caller paths in BillApprovalsPage + send-bill-approval-invites referenced templates that weren't in the registry, so every email 404'd). Restored the local copies of election-invite, board-vote-invite, and the missing registry.ts so the repo matches what's deployed. Deployed to send-transactional-email v35. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
|
||||
-- Per-bill, per-board-member secure approval tokens for email approve/deny
|
||||
-- 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,
|
||||
@@ -18,12 +20,15 @@ CREATE INDEX IF NOT EXISTS idx_baet_token ON public.bill_approval_email_tokens(t
|
||||
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)
|
||||
-- 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,
|
||||
@@ -54,14 +59,20 @@ AS $$
|
||||
$$;
|
||||
GRANT EXECUTE ON FUNCTION public.lookup_bill_approval_by_token(uuid) TO anon, authenticated;
|
||||
|
||||
-- Record action via token
|
||||
CREATE OR REPLACE FUNCTION public.record_bill_approval_by_token(p_token uuid, p_action text, p_notes text DEFAULT NULL)
|
||||
-- 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 record;
|
||||
bill record;
|
||||
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');
|
||||
@@ -72,7 +83,10 @@ BEGIN
|
||||
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);
|
||||
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;
|
||||
@@ -80,15 +94,22 @@ BEGIN
|
||||
RETURN jsonb_build_object('ok', false, 'error', 'Bill not found');
|
||||
END IF;
|
||||
|
||||
INSERT INTO bill_approvals (bill_id, association_id, vendor_name, amount, status, notes, approved_date)
|
||||
VALUES (tok.bill_id, bill.association_id, COALESCE(tok.member_name,'Board Member'), bill.amount,
|
||||
p_action, p_notes, CURRENT_DATE);
|
||||
SELECT user_id INTO v_user_id FROM board_members WHERE id = tok.board_member_id;
|
||||
|
||||
UPDATE bills SET status = p_action, updated_at = now() WHERE id = tok.bill_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;
|
||||
SET acted_at = now(), action = p_action, notes = p_notes
|
||||
WHERE token = p_token;
|
||||
|
||||
RETURN jsonb_build_object('ok', true, 'action', p_action);
|
||||
END;
|
||||
|
||||
Reference in New Issue
Block a user