Skip to content

Workflow State Machines ​

Canonical workflow reference for Pay MVP import, transaction, allocation, posting, and organization states.

This page is the source of truth for backend guards, API conflict responses, and frontend status mapping. Existing enum names and string constants are public contract values; do not rename or renumber them without updating backend, API docs, frontend contracts, and tests in the same change.

Common Rules ​

  • Guarded writes use IWorkflowStateGuardService.
  • Invalid status transitions return 409 with workflow.invalid_transition.
  • Lifecycle locks return 409 with workflow.locked.
  • Stale optimistic concurrency tokens return 409 with concurrency.conflict.
  • rowVersion is app-managed on BankStatementImport, BankTransaction, PaymentAllocation, and SyrvePaymentPosting.
  • UI should map exact status strings and keep an unknown-status fallback that disables destructive or write actions.
  • After any 409, UI should refresh the entity/list before offering retry.
  • DeletionLifecycleState is orthogonal to business status. SoftDeleted, PendingHardDelete, and HardDeleted lock active workflow operations unless an explicit restore/delete lifecycle operation is being performed.

Import Workflow ​

Entity: BankStatementImport

StateMeaning
PendingImport record is declared but not yet processed. Currently mostly available for future async processing.
DetectingBank/format detection is running.
ParsingAdapter parsing is running.
ParsedTransactions and quality report were persisted. Current successful import terminal state in the implemented upload path.
NeedsReviewParsed successfully but balance validation failed (balance_mismatch). Matching/allocation allowed; posting blocked until override.
MatchingAvailable/planned matching stage marker.
ReadyForAllocationAvailable/planned allocation-ready marker.
CompletedAvailable/planned end-to-end completion marker.
FailedDetection, parsing, or persistence failed.

Allowed transitions:

OperationFromTo
processImportPendingDetecting
processImportDetectingParsing
processImportParsingParsed
processImportParsingNeedsReview when balanceMatches === false
processImportParsedMatching, ReadyForAllocation, Completed when those stages are implemented
processImportDetecting, Parsing, Matching, ReadyForAllocationFailed

Terminal states: implemented today Parsed, NeedsReview, Failed; planned/available enum state Completed.

Balance review gate (MHPAY-63):

  • quality report warning code: balance_mismatch;
  • while BalanceMatches == false and BalanceReviewOverridden == false, queue posting is blocked with bank_import_balance_review_required;
  • override endpoint: POST /api/bank-statements/imports/{id}/quality-review/override with mandatory reason;
  • after override, posting may proceed if allocation/posting lifecycle guards and other safety gates pass.

Locked states: any import with DeletionLifecycleState != Active.

Guard coverage: current import upload owns its internal sequence. There is no public import status mutation API, so MHPAY-93 does not add a new external transition endpoint. Allocation/posting paths already check import lifecycle locks before using its transactions.

Transaction Workflow ​

Entity: BankTransaction

BankTransaction intentionally has no persisted BankTransactionStatus. Effective workflow state is derived from existing fields:

Effective stateDerived from
Imported active transactionDeletionLifecycleState = Active and persisted from a parsed import
Duplicate candidateBankTransactionDuplicateCandidate.Status = Candidate or transaction duplicate marker indicates candidate state
Duplicate confirmedduplicate candidate/status marked confirmed
Duplicate dismissedduplicate candidate/status marked dismissed/none
UnmatchedCounterpartyMatch.Status = Unmatched or no match row
Auto matchedCounterpartyMatch.Status = AutoMatched
Manual matchedCounterpartyMatch.Status = ManualMatched
Match excludedCounterpartyMatch.Status = Excluded (not a supplier payment; allocation blocked)
Allocation-readyactive transaction, not lifecycle-locked, not Excluded, and not blocked by existing approved/queued allocation workflow
LockedDeletionLifecycleState != Active
OperationEffective transition or check
manualCounterpartyMatchUnmatched / low-confidence auto match -> ManualMatched or creates reusable rule.
buildPaymentCandidatesReads matched active transactions and eligible document snapshots.
confirmAllocationRequires active transaction/import lifecycle and no existing approved/queued allocation lock for the transaction.
queuePostingRequires active transaction/import lifecycle through the allocation path.

Terminal states: no dedicated terminal transaction state exists today. Duplicate-confirmed and lifecycle-locked records should be treated as unavailable for normal allocation/posting UI actions.

Guard coverage: MHPAY-93 does not introduce BankTransactionStatus. Lifecycle locks are enforced in allocation confirm and posting queue paths through the centralized guard.

Allocation Workflow ​

Entity: PaymentAllocation

StateMeaning
DraftDeclared enum state; not commonly emitted by current confirm path.
PreviewPreview/review state.
SuggestedAuto-suggested allocation.
NeedsReviewOperator review needed, usually warning-bearing allocation.
ConfirmedOperator confirmed allocation lines.
ApprovalRequiredMandatory initial state for every new posting; explicit manual approval is required.
ApprovedAllocation approved; edit/cancel lock applies.
PostingQueuedPosting was queued; edit/cancel lock applies.
PostedExternal write evidence exists; reconciliation is still pending.
ReconciledPayment posting reconciled.
FailedAllocation/posting path failed and may need review if a service supports it.
CancelledCancelled allocation; terminal for active allocation workflow.
OperationFromTo
confirmAllocationtransaction allocation-readyConfirmed or NeedsReview
approveAllocationConfirmed, NeedsReview, ApprovalRequired, Suggested, PreviewApproved
cancelAllocationDraft, Preview, Suggested, NeedsReview, Confirmed, ApprovalRequiredCancelled
queuePostingApproved, Confirmed, NeedsReview when no posting existsPostingQueued
worker/readbackPostingQueuedPosted
worker/reconcilePostedReconciled

Terminal states: Reconciled, Cancelled. Posted is not end-to-end completion.

Locked states:

  • Approved and PostingQueued block allocation edit/cancel/confirm conflicts with allocation-specific codes.
  • Posted, Reconciled, and Cancelled block active allocation mutations with workflow.invalid_transition.
  • Any allocation with DeletionLifecycleState != Active returns workflow.locked.

Guard coverage:

  • EnsureAllocationCanApprove
  • EnsureAllocationCanCancel
  • EnsureAllocationCanQueuePosting
  • EnsureAllocationEditable
  • EnsureExpectedRowVersion

Posting Workflow ​

Entity: SyrvePaymentPosting

StateMeaning
DraftPosting shell before approval/queue.
ApprovalRequiredOperator approval required before queue.
ApprovedApproved and eligible for worker pickup.
QueuedQueued for worker execution.
PostingWorker is attempting write.
PostedWrite completed, readback/reconciliation still pending.
ReadbackPendingAwaiting retry/readback reconciliation.
ReconciledPosted payment reconciled against Syrve/iiko readback.
FailedWrite or reconciliation failed.
RetryScheduledRetry scheduled after failure.
WriteOutcomeUnknownTransport may have written before timeout/failure; readback is mandatory and blind retry is forbidden.
CancelledCancelled before posting.
PendingLegacy/available pending marker.
SkippedAvailable skip marker.
CancelledByHardDeleteLifecycle hard-delete cancellation marker.
OperationFromTo
queuePostingallocation queue pathApprovalRequired or Queued depending write approval options
approvePostingApprovalRequiredQueued
configureWriteTargetDraft, ApprovalRequiredsame state, write target fields updated
workerApproved, Queued, RetryScheduledPosting
worker successPostingReadbackPending
worker failurePostingFailed or RetryScheduled
worker unknown result / expired leasePostingWriteOutcomeUnknown
retryPostingFailed, RetryScheduledRetryScheduled
reconcilePostingPosted, ReadbackPending, WriteOutcomeUnknown, FailedReconciled, Failed, or stays ReadbackPending
lifecycle hard deleteunfinished active statesCancelledByHardDelete

Terminal states: Reconciled, Cancelled, CancelledByHardDelete, Skipped.

ReconciledAtUtc is set only on a successful transition to Reconciled. A readback mismatch creates/links an anomaly, transitions the posting and allocation to Failed, and leaves ReconciledAtUtc null. Thus successful transport evidence (Posted) and even a completed reconciliation attempt are never treated as successful reconciliation without matching per-document debt readback. The accountant workbench maps Posted to Processing; only Reconciled maps to Completed or AlreadyCompleted.

Locked states:

  • Posting status outside the allowed operation source states returns workflow.invalid_transition.
  • Any posting with DeletionLifecycleState != Active returns workflow.locked.

Guard coverage:

  • EnsurePostingCanApprove
  • EnsurePostingCanConfigureWriteTarget
  • EnsurePostingCanRetry
  • EnsurePostingCanReconcile
  • EnsureExpectedRowVersion

Additional posting safety gates:

  • open Critical PaymentAnomaly for the same external document blocks queue, approve, retry, and worker execute with payment_anomaly_open;
  • BankTransaction.Amount must equal document-settlement lines plus confirmed supplier-advance operations; otherwise confirm, approve, queue, dry-run, execute, and retry fail closed with payment_not_fully_distributed;
  • if a corrupt or legacy queued row violates that invariant, the worker does not call transport, moves the posting to Failed, moves the allocation to NeedsReview, and persists the same diagnostic code;
  • method-specific preflight blocks write when persisted PostingType does not match the allocation document type write method with posting_type_document_type_mismatch;
  • dry-run remains read-only and may return payment_anomaly_open warning without executing a write.

Organization Workflow ​

Entity: Organization

StateMeaning
ActiveNormal scoped operations are allowed subject to permissions, supported quotas, workflow guards and safety controls.
SuspendedOperational writes should be blocked by domain-specific gates where implemented; admin reads/recovery remain possible.
ArchivedNear-terminal administrative state. Operational workflows should not target archived organizations unless an explicit restore is performed.
OperationFromTo
updateOrganizationActiveSuspended
updateOrganizationSuspendedActive
archiveOrganizationActive, SuspendedArchived
restoreOrganizationArchivedActive

Blocked transitions:

  • Unknown status strings are rejected with workflow.invalid_transition.
  • Archived -> Suspended is rejected; restore must return to Active first.

Location/Outlet activity: Location.IsActive is a separate outlet activity flag. It is not renamed or merged into organization status in this workflow.

Guard coverage: existing organization update paths call EnsureOrganizationCanUpdate. MHPAY-93 intentionally does not add a broad organization lifecycle refactor.

API And UI Contract Notes ​

  • API returns exact enum/string status values.
  • Frontend unions may list known statuses, but runtime rendering must handle unknown values safely.
  • Disabled UI actions should mirror backend guard source states, but backend remains authoritative.
  • Guarded mutation requests should include the latest rowVersion when available:
    • allocation approve/cancel;
    • posting queue via allocationRowVersion;
    • posting approve/retry/reconcile/configure write target.
  • Missing rowVersion remains accepted for compatibility, but UI should pass it for stale-action detection.
  • On workflow.invalid_transition, workflow.locked, or concurrency.conflict, refresh the affected entity/list and show a state-specific conflict message.