From 3374695d1b78ecd9134d49bc79ce1ccab71912ae Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:33:34 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 141 +++++++++++++++++ ...1_e1a87595-77f2-46d4-add0-3f20c9b52d1c.sql | 149 ++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 supabase/migrations/20260417023331_e1a87595-77f2-46d4-add0-3f20c9b52d1c.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 98fc144..c1c5e05 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1110,6 +1110,147 @@ export type Database = { }, ] } + invoice_line_items: { + Row: { + amount: number + case_id: string | null + created_at: string + description: string + expense_id: string | null + id: string + invoice_id: string + kind: string + quantity: number + rate: number + sort_order: number + time_entry_id: string | null + updated_at: string + user_id: string | null + work_date: string | null + } + Insert: { + amount?: number + case_id?: string | null + created_at?: string + description?: string + expense_id?: string | null + id?: string + invoice_id: string + kind?: string + quantity?: number + rate?: number + sort_order?: number + time_entry_id?: string | null + updated_at?: string + user_id?: string | null + work_date?: string | null + } + Update: { + amount?: number + case_id?: string | null + created_at?: string + description?: string + expense_id?: string | null + id?: string + invoice_id?: string + kind?: string + quantity?: number + rate?: number + sort_order?: number + time_entry_id?: string | null + updated_at?: string + user_id?: string | null + work_date?: string | null + } + Relationships: [ + { + foreignKeyName: "invoice_line_items_case_id_fkey" + columns: ["case_id"] + isOneToOne: false + referencedRelation: "cases" + referencedColumns: ["id"] + }, + { + foreignKeyName: "invoice_line_items_expense_id_fkey" + columns: ["expense_id"] + isOneToOne: false + referencedRelation: "expenses" + referencedColumns: ["id"] + }, + { + foreignKeyName: "invoice_line_items_invoice_id_fkey" + columns: ["invoice_id"] + isOneToOne: false + referencedRelation: "invoices" + referencedColumns: ["id"] + }, + { + foreignKeyName: "invoice_line_items_time_entry_id_fkey" + columns: ["time_entry_id"] + isOneToOne: false + referencedRelation: "time_entries" + referencedColumns: ["id"] + }, + { + foreignKeyName: "invoice_line_items_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["id"] + }, + ] + } + invoice_payments: { + Row: { + amount: number + created_at: string + created_by: string | null + id: string + invoice_id: string + method: string | null + notes: string | null + paid_on: string + reference: string | null + } + Insert: { + amount?: number + created_at?: string + created_by?: string | null + id?: string + invoice_id: string + method?: string | null + notes?: string | null + paid_on?: string + reference?: string | null + } + Update: { + amount?: number + created_at?: string + created_by?: string | null + id?: string + invoice_id?: string + method?: string | null + notes?: string | null + paid_on?: string + reference?: string | null + } + Relationships: [ + { + foreignKeyName: "invoice_payments_created_by_fkey" + columns: ["created_by"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["id"] + }, + { + foreignKeyName: "invoice_payments_invoice_id_fkey" + columns: ["invoice_id"] + isOneToOne: false + referencedRelation: "invoices" + referencedColumns: ["id"] + }, + ] + } invoices: { Row: { amount_paid: number diff --git a/supabase/migrations/20260417023331_e1a87595-77f2-46d4-add0-3f20c9b52d1c.sql b/supabase/migrations/20260417023331_e1a87595-77f2-46d4-add0-3f20c9b52d1c.sql new file mode 100644 index 0000000..260cb37 --- /dev/null +++ b/supabase/migrations/20260417023331_e1a87595-77f2-46d4-add0-3f20c9b52d1c.sql @@ -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(); \ No newline at end of file