Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma strängvärde byggs med konkatenering i stället för att matcha mönstret.
118 lines
4.5 KiB
PL/PgSQL
118 lines
4.5 KiB
PL/PgSQL
-- Offert -> kundorder (issue #2224): the sale behind a quote must never
|
|
-- exist twice. The services check for a live converted invoice and a live
|
|
-- kundorder before converting, but those reads are not serialized: two
|
|
-- conversions of an already-accepted quote (order + order, or order +
|
|
-- invoice) both pass their pre-checks, and the quote's compare-and-set
|
|
-- (accepted -> accepted) matches for both. Only the database can close
|
|
-- that window.
|
|
--
|
|
-- 1. One live kundorder per source document (partial unique index). A
|
|
-- cancelled order frees the source again, matching the service rule.
|
|
-- 2. A kundorder whose source is a quote cannot become live (insert, or
|
|
-- reopen from cancelled) while a live invoice was converted from that
|
|
-- quote. The trigger locks the quote row first (FOR UPDATE), so a
|
|
-- concurrent converted-invoice insert queues behind it and sees the
|
|
-- committed order.
|
|
-- 3. The mirror: an invoice with converted_from_id = a quote cannot become
|
|
-- live while a live kundorder points at that quote. Same lock, same
|
|
-- serialization, raised with the registry code the services map to 409
|
|
-- INVOICE_QUOTE_ALREADY_ORDERED.
|
|
--
|
|
-- Both triggers exit before the lock on every update that does not make a
|
|
-- row live (the common status changes), and never touch proformas: the
|
|
-- proforma path cancels its source, which its compare-and-set already
|
|
-- serializes.
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_sales_orders_one_live_per_source
|
|
ON public.sales_orders (source_invoice_id)
|
|
WHERE source_invoice_id IS NOT NULL AND status <> 'cancelled';
|
|
|
|
CREATE OR REPLACE FUNCTION public.sales_orders_source_guard()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SET search_path = ''
|
|
AS $function$
|
|
DECLARE
|
|
v_source_type text;
|
|
BEGIN
|
|
IF NEW.source_invoice_id IS NULL OR NEW.status = 'cancelled' THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
-- Only a row that BECOMES live is checked: an insert, a reopen from
|
|
-- cancelled, or a re-pointed source.
|
|
IF TG_OP = 'UPDATE'
|
|
AND OLD.status <> 'cancelled'
|
|
AND OLD.source_invoice_id IS NOT DISTINCT FROM NEW.source_invoice_id THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
SELECT i.document_type INTO v_source_type
|
|
FROM public.invoices i
|
|
WHERE i.id = NEW.source_invoice_id
|
|
FOR UPDATE;
|
|
|
|
IF v_source_type = 'quote' AND EXISTS (
|
|
SELECT 1 FROM public.invoices c
|
|
WHERE c.converted_from_id = NEW.source_invoice_id
|
|
AND c.status <> 'cancelled'
|
|
) THEN
|
|
RAISE EXCEPTION 'INVOICE_QUOTE_ALREADY_INVOICED: quote % has a live converted invoice', NEW.source_invoice_id
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$function$;
|
|
|
|
DROP TRIGGER IF EXISTS trg_sales_orders_source_guard ON public.sales_orders;
|
|
CREATE TRIGGER trg_sales_orders_source_guard
|
|
BEFORE INSERT OR UPDATE OF status, source_invoice_id ON public.sales_orders
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.sales_orders_source_guard();
|
|
|
|
CREATE OR REPLACE FUNCTION public.invoices_converted_source_guard()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SET search_path = ''
|
|
AS $function$
|
|
DECLARE
|
|
v_source_type text;
|
|
BEGIN
|
|
IF NEW.converted_from_id IS NULL OR NEW.status = 'cancelled' THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
IF TG_OP = 'UPDATE'
|
|
AND OLD.status <> 'cancelled'
|
|
AND OLD.converted_from_id IS NOT DISTINCT FROM NEW.converted_from_id THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
SELECT i.document_type INTO v_source_type
|
|
FROM public.invoices i
|
|
WHERE i.id = NEW.converted_from_id
|
|
FOR UPDATE;
|
|
|
|
IF v_source_type = 'quote' AND EXISTS (
|
|
SELECT 1 FROM public.sales_orders o
|
|
WHERE o.source_invoice_id = NEW.converted_from_id
|
|
AND o.status <> 'cancelled'
|
|
) THEN
|
|
RAISE EXCEPTION 'INVOICE_QUOTE_ALREADY_ORDERED: quote % has a live kundorder', NEW.converted_from_id
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$function$;
|
|
|
|
DROP TRIGGER IF EXISTS trg_invoices_converted_source_guard ON public.invoices;
|
|
CREATE TRIGGER trg_invoices_converted_source_guard
|
|
BEFORE INSERT OR UPDATE OF status, converted_from_id ON public.invoices
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.invoices_converted_source_guard();
|
|
|
|
COMMENT ON FUNCTION public.sales_orders_source_guard() IS
|
|
'A kundorder created from a quote cannot become live while a live invoice was converted from that quote. Locks the quote row so concurrent conversions serialize. Raises INVOICE_QUOTE_ALREADY_INVOICED.';
|
|
COMMENT ON FUNCTION public.invoices_converted_source_guard() IS
|
|
'An invoice converted from a quote cannot become live while a live kundorder was created from that quote. Locks the quote row so concurrent conversions serialize. Raises INVOICE_QUOTE_ALREADY_ORDERED.';
|
|
|
|
NOTIFY pgrst, 'reload schema';
|