fix(pending-ops): record posted ids and land failed_partial instead of clean rejected after partial commits (#842) (#1110)
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
27ef398623
commit
3e1ea29d02
@@ -0,0 +1,86 @@
|
||||
-- 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';
|
||||
Reference in New Issue
Block a user