mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
|
||||
-- Rules table (one per association)
|
||||
CREATE TABLE public.bill_approval_rules (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
association_id uuid NOT NULL UNIQUE REFERENCES public.associations(id) ON DELETE CASCADE,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
required_approvals integer NOT NULL DEFAULT 2 CHECK (required_approvals >= 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TRIGGER trg_bill_approval_rules_updated_at
|
||||
BEFORE UPDATE ON public.bill_approval_rules
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||||
|
||||
-- Amount thresholds
|
||||
CREATE TABLE public.bill_approval_rule_thresholds (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
rule_id uuid NOT NULL REFERENCES public.bill_approval_rules(id) ON DELETE CASCADE,
|
||||
min_amount numeric(12,2) NOT NULL DEFAULT 0,
|
||||
max_amount numeric(12,2),
|
||||
required_approvals integer NOT NULL CHECK (required_approvals >= 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_batr_rule ON public.bill_approval_rule_thresholds(rule_id);
|
||||
|
||||
-- Vendor exceptions
|
||||
CREATE TABLE public.bill_approval_rule_vendor_exceptions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
rule_id uuid NOT NULL REFERENCES public.bill_approval_rules(id) ON DELETE CASCADE,
|
||||
vendor_name text NOT NULL,
|
||||
required_approvals integer NOT NULL CHECK (required_approvals >= 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_barve_rule ON public.bill_approval_rule_vendor_exceptions(rule_id);
|
||||
CREATE INDEX idx_barve_vendor ON public.bill_approval_rule_vendor_exceptions(rule_id, lower(vendor_name));
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE public.bill_approval_rules ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.bill_approval_rule_thresholds ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.bill_approval_rule_vendor_exceptions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Staff manage approval rules" ON public.bill_approval_rules
|
||||
FOR ALL TO authenticated
|
||||
USING (public.has_role(auth.uid(),'admin'::app_role) OR public.has_role(auth.uid(),'manager'::app_role))
|
||||
WITH CHECK (public.has_role(auth.uid(),'admin'::app_role) OR public.has_role(auth.uid(),'manager'::app_role));
|
||||
|
||||
CREATE POLICY "Board can view their approval rules" ON public.bill_approval_rules
|
||||
FOR SELECT TO authenticated
|
||||
USING (association_id IN (SELECT bm.association_id FROM public.board_members bm WHERE bm.user_id = auth.uid()));
|
||||
|
||||
CREATE POLICY "Staff manage thresholds" ON public.bill_approval_rule_thresholds
|
||||
FOR ALL TO authenticated
|
||||
USING (public.has_role(auth.uid(),'admin'::app_role) OR public.has_role(auth.uid(),'manager'::app_role))
|
||||
WITH CHECK (public.has_role(auth.uid(),'admin'::app_role) OR public.has_role(auth.uid(),'manager'::app_role));
|
||||
|
||||
CREATE POLICY "Board view thresholds" ON public.bill_approval_rule_thresholds
|
||||
FOR SELECT TO authenticated
|
||||
USING (rule_id IN (SELECT r.id FROM public.bill_approval_rules r WHERE r.association_id IN (SELECT bm.association_id FROM public.board_members bm WHERE bm.user_id = auth.uid())));
|
||||
|
||||
CREATE POLICY "Staff manage vendor exceptions" ON public.bill_approval_rule_vendor_exceptions
|
||||
FOR ALL TO authenticated
|
||||
USING (public.has_role(auth.uid(),'admin'::app_role) OR public.has_role(auth.uid(),'manager'::app_role))
|
||||
WITH CHECK (public.has_role(auth.uid(),'admin'::app_role) OR public.has_role(auth.uid(),'manager'::app_role));
|
||||
|
||||
CREATE POLICY "Board view vendor exceptions" ON public.bill_approval_rule_vendor_exceptions
|
||||
FOR SELECT TO authenticated
|
||||
USING (rule_id IN (SELECT r.id FROM public.bill_approval_rules r WHERE r.association_id IN (SELECT bm.association_id FROM public.board_members bm WHERE bm.user_id = auth.uid())));
|
||||
|
||||
-- Helper: required approvals for a bill
|
||||
CREATE OR REPLACE FUNCTION public.get_required_approvals_for_bill(p_bill_id uuid)
|
||||
RETURNS integer
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public'
|
||||
AS $$
|
||||
DECLARE
|
||||
v_assoc uuid;
|
||||
v_amount numeric;
|
||||
v_vendor text;
|
||||
v_rule public.bill_approval_rules%ROWTYPE;
|
||||
v_required integer;
|
||||
BEGIN
|
||||
SELECT b.association_id, b.amount, COALESCE(b.vendor_name, '')
|
||||
INTO v_assoc, v_amount, v_vendor
|
||||
FROM public.bills b WHERE b.id = p_bill_id;
|
||||
|
||||
IF v_assoc IS NULL THEN RETURN NULL; END IF;
|
||||
|
||||
SELECT * INTO v_rule FROM public.bill_approval_rules WHERE association_id = v_assoc;
|
||||
IF v_rule.id IS NULL OR v_rule.enabled = false THEN
|
||||
RETURN NULL; -- no rule: legacy behavior
|
||||
END IF;
|
||||
|
||||
-- Vendor exception wins
|
||||
SELECT required_approvals INTO v_required
|
||||
FROM public.bill_approval_rule_vendor_exceptions
|
||||
WHERE rule_id = v_rule.id AND lower(vendor_name) = lower(v_vendor)
|
||||
LIMIT 1;
|
||||
IF v_required IS NOT NULL THEN RETURN v_required; END IF;
|
||||
|
||||
-- Threshold match (most specific: highest min_amount that fits)
|
||||
SELECT required_approvals INTO v_required
|
||||
FROM public.bill_approval_rule_thresholds
|
||||
WHERE rule_id = v_rule.id
|
||||
AND COALESCE(v_amount,0) >= min_amount
|
||||
AND (max_amount IS NULL OR COALESCE(v_amount,0) <= max_amount)
|
||||
ORDER BY min_amount DESC
|
||||
LIMIT 1;
|
||||
IF v_required IS NOT NULL THEN RETURN v_required; END IF;
|
||||
|
||||
RETURN v_rule.required_approvals;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Update recompute trigger function to use rule
|
||||
CREATE OR REPLACE FUNCTION public.recompute_bill_status_from_approvals()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
|
||||
AS $$
|
||||
DECLARE
|
||||
v_bill_id uuid;
|
||||
v_total int;
|
||||
v_approved int;
|
||||
v_denied int;
|
||||
v_required int;
|
||||
v_current_status text;
|
||||
v_new_status text;
|
||||
BEGIN
|
||||
v_bill_id := COALESCE(NEW.bill_id, OLD.bill_id);
|
||||
IF v_bill_id IS NULL THEN
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*),
|
||||
COUNT(*) FILTER (WHERE status = 'approved'),
|
||||
COUNT(*) FILTER (WHERE status = 'denied')
|
||||
INTO v_total, v_approved, v_denied
|
||||
FROM public.bill_approvals
|
||||
WHERE bill_id = v_bill_id;
|
||||
|
||||
IF v_total = 0 THEN
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END IF;
|
||||
|
||||
SELECT status INTO v_current_status FROM public.bills WHERE id = v_bill_id;
|
||||
v_required := public.get_required_approvals_for_bill(v_bill_id);
|
||||
|
||||
IF v_denied > 0 THEN
|
||||
v_new_status := 'denied';
|
||||
ELSIF v_required IS NOT NULL AND v_approved >= v_required THEN
|
||||
v_new_status := 'approved';
|
||||
ELSIF v_required IS NULL AND v_approved = v_total THEN
|
||||
-- legacy: no rule configured, all recorded votes must approve
|
||||
v_new_status := 'approved';
|
||||
ELSE
|
||||
v_new_status := NULL;
|
||||
END IF;
|
||||
|
||||
IF v_new_status IS NOT NULL
|
||||
AND v_current_status IS DISTINCT FROM v_new_status
|
||||
AND COALESCE(v_current_status, '') NOT IN ('paid', 'cancelled') THEN
|
||||
UPDATE public.bills
|
||||
SET status = v_new_status,
|
||||
updated_at = now(),
|
||||
approved_date = CASE WHEN v_new_status = 'approved'
|
||||
THEN COALESCE(approved_date, CURRENT_DATE)
|
||||
ELSE approved_date END
|
||||
WHERE id = v_bill_id;
|
||||
END IF;
|
||||
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user