Fix/multiple domain issue (#401)
* feat: enhance invoice management and immutability checks - Update InvoiceDetailPage to prevent deletion of drafts with assigned invoice numbers, providing user feedback. - Modify the invoice conversion API to ensure invoice number allocation occurs only after successful item insertion and proforma cancellation. - Implement structured error responses for invoice deletion, ensuring only drafts without assigned numbers can be deleted. - Add comprehensive tests for invoice deletion and conversion scenarios, including edge cases for draft invoices. - Introduce immutability checks in the document management system to prevent unauthorized changes to linked documents. - Create SQL migration to enforce document metadata immutability, ensuring compliance with accounting regulations. * fix(invoice): prevent invoice number consumption on PDF render failure * feat: add document journal entry immutability enforcement for delete_last_voucher RPC * fix(invoice): implement rollback for orphan invoices on proforma cancel failure * fix(document): extend immutability trigger to protect journal entry links
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
-- enforce_document_journal_entry_immutability (added in 20260506130000)
|
||||
-- blocks any change to a non-null journal_entry_id once set. That is
|
||||
-- correct for end-user paths but breaks delete_last_voucher: the RPC
|
||||
-- legitimately needs to clear journal_entry_id before deleting a posted
|
||||
-- voucher in a series (BFNAR 2013:2 permits removing the last voucher).
|
||||
--
|
||||
-- Bring this trigger in line with enforce_journal_entry_immutability,
|
||||
-- which already honors a transaction-local gnubok.allow_delete=true bypass.
|
||||
-- Only delete_last_voucher sets that flag, and only after enforcing all
|
||||
-- legal constraints (last-in-series, no references, period not locked,
|
||||
-- owner/admin). Outside the RPC the flag is unset, so end-user UPDATE
|
||||
-- paths remain blocked.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.enforce_document_journal_entry_immutability()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF current_setting('gnubok.allow_delete', true) = 'true' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF NEW.journal_entry_id IS NOT DISTINCT FROM OLD.journal_entry_id THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF OLD.journal_entry_id IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF NEW.journal_entry_id IS NULL OR NEW.journal_entry_id <> OLD.journal_entry_id THEN
|
||||
RAISE EXCEPTION
|
||||
'BFL_DOCUMENT_IMMUTABILITY: cannot clear or change journal_entry_id on document % once set (BFL 5 kap 6 §). Reverse the journal entry first.',
|
||||
OLD.id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,64 @@
|
||||
-- Closes BFL 7 kap 2§ retention bypass on document_attachments.
|
||||
--
|
||||
-- Problem: enforce_document_metadata_immutability protected file identity
|
||||
-- columns but not journal_entry_id / journal_entry_line_id. block_document_deletion
|
||||
-- only fires when OLD.journal_entry_id IS NOT NULL. An attacker could therefore
|
||||
-- run UPDATE document_attachments SET journal_entry_id = NULL WHERE id = X,
|
||||
-- followed by DELETE FROM document_attachments WHERE id = X, severing
|
||||
-- räkenskapsinformation from the audit trail in violation of BFL 7 kap 2§.
|
||||
--
|
||||
-- Fix: extend the existing immutability trigger to also reject changes to
|
||||
-- journal_entry_id and journal_entry_line_id when the document is currently
|
||||
-- linked to a posted or reversed journal entry. Honour the existing
|
||||
-- gnubok.allow_delete transaction-local bypass that delete_last_voucher uses
|
||||
-- when intentionally tearing down a voucher.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.enforce_document_metadata_immutability()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO 'public'
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_entry_status text;
|
||||
BEGIN
|
||||
IF current_setting('gnubok.allow_delete', true) = 'true' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF OLD.journal_entry_id IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
SELECT status INTO v_entry_status
|
||||
FROM public.journal_entries
|
||||
WHERE id = OLD.journal_entry_id;
|
||||
|
||||
IF v_entry_status IS NULL OR v_entry_status NOT IN ('posted', 'reversed') THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF NEW.file_name IS DISTINCT FROM OLD.file_name
|
||||
OR NEW.storage_path IS DISTINCT FROM OLD.storage_path
|
||||
OR NEW.file_size_bytes IS DISTINCT FROM OLD.file_size_bytes
|
||||
OR NEW.mime_type IS DISTINCT FROM OLD.mime_type
|
||||
OR NEW.sha256_hash IS DISTINCT FROM OLD.sha256_hash
|
||||
OR NEW.upload_source IS DISTINCT FROM OLD.upload_source
|
||||
OR NEW.digitization_date IS DISTINCT FROM OLD.digitization_date
|
||||
OR NEW.uploaded_by IS DISTINCT FROM OLD.uploaded_by
|
||||
OR NEW.version IS DISTINCT FROM OLD.version
|
||||
OR NEW.original_id IS DISTINCT FROM OLD.original_id
|
||||
OR NEW.is_current_version IS DISTINCT FROM OLD.is_current_version
|
||||
OR NEW.journal_entry_id IS DISTINCT FROM OLD.journal_entry_id
|
||||
OR NEW.journal_entry_line_id IS DISTINCT FROM OLD.journal_entry_line_id
|
||||
THEN
|
||||
INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description)
|
||||
VALUES (OLD.user_id, OLD.company_id, 'SECURITY_EVENT', 'document_attachments', OLD.id,
|
||||
'Blocked metadata or link modification of document linked to ' || v_entry_status || ' entry ' || OLD.journal_entry_id);
|
||||
|
||||
RAISE EXCEPTION 'Cannot modify metadata or journal entry link of document linked to a % journal entry (BFL 7 kap)', v_entry_status;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$;
|
||||
Reference in New Issue
Block a user