Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 02:33:34 +00:00
co-authored by renee-png
parent 68d86e08ea
commit 3374695d1b
2 changed files with 290 additions and 0 deletions
@@ -0,0 +1,149 @@
-- Invoice line items: allow grouping by case + manual editing
CREATE TABLE public.invoice_line_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id uuid NOT NULL REFERENCES public.invoices(id) ON DELETE CASCADE,
case_id uuid REFERENCES public.cases(id) ON DELETE SET NULL,
kind text NOT NULL DEFAULT 'manual', -- 'time' | 'expense' | 'manual'
description text NOT NULL DEFAULT '',
work_date date,
quantity numeric NOT NULL DEFAULT 1,
rate numeric NOT NULL DEFAULT 0,
amount numeric NOT NULL DEFAULT 0,
time_entry_id uuid REFERENCES public.time_entries(id) ON DELETE SET NULL,
expense_id uuid REFERENCES public.expenses(id) ON DELETE SET NULL,
user_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
sort_order integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_invoice_line_items_invoice ON public.invoice_line_items(invoice_id);
CREATE INDEX idx_invoice_line_items_case ON public.invoice_line_items(case_id);
ALTER TABLE public.invoice_line_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY "ili_select" ON public.invoice_line_items FOR SELECT TO authenticated
USING (EXISTS (
SELECT 1 FROM public.invoices i
WHERE i.id = invoice_line_items.invoice_id
AND (public.is_admin(auth.uid()) OR i.created_by = auth.uid()
OR (i.case_id IS NOT NULL AND public.can_access_case(i.case_id, auth.uid())))
));
CREATE POLICY "ili_insert" ON public.invoice_line_items FOR INSERT TO authenticated
WITH CHECK (EXISTS (
SELECT 1 FROM public.invoices i
WHERE i.id = invoice_line_items.invoice_id
AND (public.is_admin(auth.uid()) OR i.created_by = auth.uid()
OR (i.case_id IS NOT NULL AND public.can_access_case(i.case_id, auth.uid())))
));
CREATE POLICY "ili_update" ON public.invoice_line_items FOR UPDATE TO authenticated
USING (EXISTS (
SELECT 1 FROM public.invoices i
WHERE i.id = invoice_line_items.invoice_id
AND (public.is_admin(auth.uid()) OR i.created_by = auth.uid()
OR (i.case_id IS NOT NULL AND public.can_access_case(i.case_id, auth.uid())))
));
CREATE POLICY "ili_delete" ON public.invoice_line_items FOR DELETE TO authenticated
USING (EXISTS (
SELECT 1 FROM public.invoices i
WHERE i.id = invoice_line_items.invoice_id
AND (public.is_admin(auth.uid()) OR i.created_by = auth.uid()
OR (i.case_id IS NOT NULL AND public.can_access_case(i.case_id, auth.uid())))
));
CREATE TRIGGER trg_ili_updated BEFORE UPDATE ON public.invoice_line_items
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- Invoice payments
CREATE TABLE public.invoice_payments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id uuid NOT NULL REFERENCES public.invoices(id) ON DELETE CASCADE,
amount numeric NOT NULL DEFAULT 0,
paid_on date NOT NULL DEFAULT CURRENT_DATE,
method text,
reference text,
notes text,
created_by uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_invoice_payments_invoice ON public.invoice_payments(invoice_id);
ALTER TABLE public.invoice_payments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "ip_select" ON public.invoice_payments FOR SELECT TO authenticated
USING (EXISTS (
SELECT 1 FROM public.invoices i
WHERE i.id = invoice_payments.invoice_id
AND (public.is_admin(auth.uid()) OR i.created_by = auth.uid()
OR (i.case_id IS NOT NULL AND public.can_access_case(i.case_id, auth.uid())))
));
CREATE POLICY "ip_insert" ON public.invoice_payments FOR INSERT TO authenticated
WITH CHECK (EXISTS (
SELECT 1 FROM public.invoices i
WHERE i.id = invoice_payments.invoice_id
AND (public.is_admin(auth.uid()) OR i.created_by = auth.uid()
OR (i.case_id IS NOT NULL AND public.can_access_case(i.case_id, auth.uid())))
));
CREATE POLICY "ip_delete" ON public.invoice_payments FOR DELETE TO authenticated
USING (EXISTS (
SELECT 1 FROM public.invoices i
WHERE i.id = invoice_payments.invoice_id
AND (public.is_admin(auth.uid()) OR i.created_by = auth.uid()
OR (i.case_id IS NOT NULL AND public.can_access_case(i.case_id, auth.uid())))
));
-- Trigger: recompute invoice amount_paid / paid_at / status from payments
CREATE OR REPLACE FUNCTION public.tg_recompute_invoice_payments()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = public
AS $$
DECLARE
v_invoice_id uuid;
v_total numeric;
v_paid numeric;
v_due date;
v_current_status invoice_status;
v_new_status invoice_status;
v_last_paid timestamptz;
BEGIN
v_invoice_id := COALESCE(NEW.invoice_id, OLD.invoice_id);
SELECT total, due_date, status INTO v_total, v_due, v_current_status
FROM public.invoices WHERE id = v_invoice_id;
SELECT COALESCE(SUM(amount), 0), MAX(created_at)
INTO v_paid, v_last_paid
FROM public.invoice_payments WHERE invoice_id = v_invoice_id;
v_new_status := v_current_status;
IF v_current_status <> 'void' AND v_current_status <> 'draft' THEN
IF v_paid >= v_total AND v_total > 0 THEN
v_new_status := 'paid';
ELSIF v_due IS NOT NULL AND v_due < CURRENT_DATE AND v_paid < v_total THEN
v_new_status := 'overdue';
ELSIF v_current_status = 'paid' AND v_paid < v_total THEN
v_new_status := 'sent';
END IF;
END IF;
UPDATE public.invoices
SET amount_paid = v_paid,
paid_at = CASE WHEN v_paid >= v_total AND v_total > 0 THEN v_last_paid ELSE NULL END,
status = v_new_status,
updated_at = now()
WHERE id = v_invoice_id;
RETURN COALESCE(NEW, OLD);
END;
$$;
CREATE TRIGGER trg_invoice_payments_recompute
AFTER INSERT OR UPDATE OR DELETE ON public.invoice_payments
FOR EACH ROW EXECUTE FUNCTION public.tg_recompute_invoice_payments();