3e1ea29d02
Multi-step executors (match_transaction_invoice, credit_invoice) post an irreversible voucher or persist a credit note and then run later fallible steps. A failure there previously marked the whole op status=rejected, hiding the posted entity and its id from operators. - new migration 20260722134114: add failed_partial to the pending_operations status CHECK and treat it as terminal in both immutability triggers (immutable, undeletable, never re-claimable) - PartialCommitError + ExecutorResult.partialPostedIds carry the posted ids; the dispatcher writes status=failed_partial with result_data.posted_ids and returns code=partial_commit - instrument only the two named executors; hoist the read-only settlement-account resolution above the storno in the match executor - consumer sweep: status union + query schema widened, failed_partial folds into the Avvisade tab with a badge and posted-ids detail line, bulk/reject routes and MCP tools message it explicitly, worklist and expiry sweep intentionally untouched (not pending work) - tests: pg-real coverage for the new terminal semantics, dispatcher unit tests for both partial paths plus byte-for-byte regression guards Fixes #842 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.9 KiB
PL/PgSQL
87 lines
3.9 KiB
PL/PgSQL
-- Migration: add 'failed_partial' terminal status to pending_operations.
|
|
--
|
|
-- Issue #842 (follow-up deferred from PR #841): several multi-step executors
|
|
-- post an irreversible voucher and then perform a later fallible step (e.g.
|
|
-- match_transaction_invoice posts a storno before building the payment JE;
|
|
-- credit_invoice persists the credit note before posting its JE). When the
|
|
-- later step failed, the dispatcher marked the WHOLE op 'rejected', which
|
|
-- misrepresents reality: an immutable voucher/credit note already exists.
|
|
--
|
|
-- 'failed_partial' is the honest terminal state for that case: the operation
|
|
-- did NOT complete, but side-effects were posted and their ids are recorded
|
|
-- in result_data.posted_ids so an operator can find the orphaned voucher.
|
|
--
|
|
-- Semantics:
|
|
-- - terminal: rows are immutable and undeletable once in this state, same
|
|
-- as 'committed'/'rejected' (BFL 7 kap.: the posted underlag and the
|
|
-- record of what happened must be unalterable)
|
|
-- - NOT re-committable: the CAS claim only picks up status = 'pending',
|
|
-- which already excludes it; the immutability trigger below additionally
|
|
-- blocks any status rewrite
|
|
-- - NOT pending work: worklist/pending counts filter on 'pending' only
|
|
|
|
-- =============================================================================
|
|
-- 1. pending_operations_status_check: add 'failed_partial'
|
|
-- =============================================================================
|
|
-- Same drop + re-add pattern as 20260504100000 used when adding 'committing'.
|
|
ALTER TABLE public.pending_operations
|
|
DROP CONSTRAINT IF EXISTS pending_operations_status_check;
|
|
|
|
ALTER TABLE public.pending_operations
|
|
ADD CONSTRAINT pending_operations_status_check
|
|
CHECK (status IN ('pending', 'committing', 'committed', 'rejected', 'failed_partial'));
|
|
|
|
-- =============================================================================
|
|
-- 2. terminal-state immutability: treat 'failed_partial' as terminal
|
|
-- =============================================================================
|
|
-- Replaces the functions from 20260504100000 (never edit that migration) so
|
|
-- the UPDATE/DELETE blockers cover the new terminal state too.
|
|
|
|
CREATE OR REPLACE FUNCTION public.enforce_pending_operations_immutability()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF OLD.status IN ('committed', 'rejected', 'failed_partial') THEN
|
|
RAISE EXCEPTION
|
|
'pending_operations row % is in terminal state % and cannot be modified (BFL 7 kap.)',
|
|
OLD.id, OLD.status
|
|
USING ERRCODE = 'check_violation';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS pending_operations_immutability ON public.pending_operations;
|
|
CREATE TRIGGER pending_operations_immutability
|
|
BEFORE UPDATE ON public.pending_operations
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.enforce_pending_operations_immutability();
|
|
|
|
CREATE OR REPLACE FUNCTION public.enforce_pending_operations_no_delete()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF OLD.status IN ('committed', 'rejected', 'failed_partial') THEN
|
|
RAISE EXCEPTION
|
|
'pending_operations row % is in terminal state % and cannot be deleted (BFL 7 kap.)',
|
|
OLD.id, OLD.status
|
|
USING ERRCODE = 'check_violation';
|
|
END IF;
|
|
RETURN OLD;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS pending_operations_no_delete ON public.pending_operations;
|
|
CREATE TRIGGER pending_operations_no_delete
|
|
BEFORE DELETE ON public.pending_operations
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.enforce_pending_operations_no_delete();
|
|
|
|
-- Note: pending_ops_auto_commit_status (20260430120000) constrained
|
|
-- auto_committed_at to status = 'committed', but it was dropped together with
|
|
-- the auto-commit feature in 20260505190027, so there is nothing to expand
|
|
-- for the new status.
|
|
|
|
-- =============================================================================
|
|
-- 3. PostgREST schema reload
|
|
-- =============================================================================
|
|
NOTIFY pgrst, 'reload schema';
|