Skip to content

Confirmation Workflow SSOT

SSOT Key: confirmation-workflow Authority: This document defines the pending_review state machine used across both Stage 1 (statement import) and Stage 2 (reconciliation) of the review pipeline. Cross-references: reconciliation.md §7 (Stage 1 & Stage 2 state machines), schema.md, extraction.md


1. Source of Truth

Concern Location
Stage1Status enum apps/backend/src/models/statement.pyBankStatement.stage1_status (nullable; None at upload, set during review workflow)
Stage2Status on match apps/backend/src/models/reconciliation.pyReconciliationMatch.status
pending_review usage apps/backend/src/routers/statements.py, apps/backend/src/routers/reconciliation.py
Balance-chain validation apps/backend/src/services/statement_validation.py
Consistency checks apps/backend/src/services/consistency_checks.py

2. The pending_review Status

pending_review appears on two distinct model fields. They are NOT the same concept.

Field Model Meaning
BankStatement.stage1_status = PENDING_REVIEW Stage 1 Parsed statement awaiting user visual verification against the original PDF (nullableNone after upload; set to PENDING_REVIEW when review is triggered)
ReconciliationMatch.status = PENDING_REVIEW Stage 2 Reconciliation match scoring 60–84 pts, requiring human decision before journal entry creation

Both use the string value "pending_review" by convention, but the state machines they live in are independent.


3. Cross-Cutting State Machine

The following diagram shows how a bank statement travels from upload through to posted journal entries.

                 ┌─────────────────────────────────────────────────────┐
                 │                  STAGE 1 (Record-Level)              │
                 │                                                       │
  Upload         │  BankStatement.stage1_status                          │
  ──────►  parsed│                                                       │
                 │   pending_review ──► approved ──────────────────────►│──► Stage 2 queue
                 │         │                                             │
                 │         └──► rejected ──► re-parse (loop)            │
                 │              (edit → re-validate → approved)         │
                 └─────────────────────────────────────────────────────┘

                 ┌─────────────────────────────────────────────────────┐
                 │                  STAGE 2 (Run-Level)                 │
                 │                                                       │
  Stage 2 queue  │  ReconciliationMatch.status                          │
  ──────────────►│                                                       │
                 │  score ≥ 85    ──► auto_accepted ──► journal posted  │
                 │  60 ≤ score < 85 ──► pending_review ──► accepted/   │
                 │                                         rejected      │
                 │  score < 60    ──► unmatched                         │
                 │                                                       │
                 │  Consistency checks must ALL be resolved before      │
                 │  batch_approve is permitted.                          │
                 └─────────────────────────────────────────────────────┘

Stage 1 Transitions

From Event To Guard
pending_review approve_statement() approved Balance delta ≤ 0.001 USD
pending_review reject_statement() rejected
pending_review edit_and_approve() approved Balance delta ≤ 0.001 USD after edits
rejected re-parse triggered pending_review

Stage 2 Transitions

From Event To Guard
pending_review accept_match() accepted All consistency checks resolved
pending_review reject_match() rejected
auto_accepted system auto-accept accepted Score ≥ 85
accepted journal created (terminal) Accounting equation holds

4. Design Constraints

DO

  • ✅ Always pass user_id to service methods that mutate pending_review state (ownership check)
  • ✅ Validate balance chain (0.001 USD tolerance) before advancing Stage 1
  • ✅ Resolve all consistency checks before Stage 2 batch approval
  • ✅ Create journal entry only on accepted transition (never on pending_review)
  • ✅ Emit an audit log entry on every state transition

DO NOT

  • ❌ Combine Stage 1 status and Stage 2 status into a single field — they are independent
  • ❌ Create journal entries from pending_review matches
  • ❌ Auto-accept Stage 1 statements without balance chain validation
  • ❌ Allow pending_review → approved bypass when duplicate/transfer checks are unresolved
  • ❌ Hardcode tolerance as 0.10 in Stage 1 — Stage 1 requires 0.001 USD

5. Tolerance Reference

Context Tolerance Source
Stage 1 balance chain validation 0.001 USD EPIC-016 Q1 user decision
Stage 2 reconciliation match (amount score) 0.10 USD AGENTS.md, reconciliation.md
Reconciliation statistics comparison 1% AGENTS.md

6. API Contract

Stage 1 Endpoints (in routers/statements.py)

Endpoint Input Side Effect
POST /api/statements/{id}/review/approve statement_id, bearer token stage1_status → approved; balance chain validation enforced (≤ 0.001 USD); queues to Stage 2
POST /api/statements/{id}/review/reject statement_id, reason, bearer token stage1_status → rejected; triggers re-parse
POST /api/statements/{id}/review/edit statement_id, edits, bearer token Updates transactions, re-validates, approves if valid
GET /api/statements/pending-review bearer token Returns [BankStatement] where status=PARSED and confidence_score is 60–84 (does not filter on stage1_status)
### Stage 2 Endpoints (reconciliation + statements routers)
Endpoint Input Side Effect
POST /api/reconciliation/matches/{id}/accept match_id, bearer token status → accepted; creates journal entry
POST /api/reconciliation/matches/{id}/reject match_id, bearer token status → rejected
POST /api/reconciliation/batch-accept [match_id], bearer token Accepts all provided matches; blocked if any related consistency check is unresolved; creates journal entries
POST /api/statements/batch-approve-matches statement_id, [match_id], bearer token Stage 2 batch acceptance scoped to a statement; updates ReconciliationMatch.status to accepted
GET /api/reconciliation/pending bearer token Returns [ReconciliationMatch] with status=pending_review

7. Verification (The Proof)

Test File What It Verifies
test_validate_balance_chain_within_tolerance review/test_statement_validation.py 0.001 USD tolerance passes
test_validate_balance_chain_exceeds_tolerance review/test_statement_validation.py 0.0011 USD delta fails
test_approve_statement_invalid_balance_fails review/test_statement_validation.py Approve blocked if balance bad
test_batch_approve_requires_checks_resolved review/test_review_workflow.py Stage 2 batch blocked by open checks (⏳ Planned)
test_journal_entry_created_on_accept review/test_review_workflow.py Journal entry only on accepted transition (⏳ Planned)
test_stage1_approve_promotes_source_type extraction/test_source_type_promotion.py Stage 1 approve raises source_type to user_confirmed (⏳ Planned)

  • reconciliation.md §7 — Stage 1 and Stage 2 detailed state machines with DB column definitions
  • schema.mdBankStatement, ReconciliationMatch, ConsistencyCheck table definitions
  • extraction.md — How parsed statements enter pending_review (Stage 1 entry point)
  • source-type-priority.md — How source_type is promoted through the confirmation lifecycle