Reconciliation Engine Domain Model SSOT¶
SSOT Key:
reconciliationCore Definition: Bank reconciliation matching algorithm, confidence scoring, and state machine.
1. Source of Truth¶
| Dimension | Physical Location (SSOT) | Description |
|---|---|---|
| Matching Algorithm | apps/backend/src/services/reconciliation.py |
Core logic |
| Scoring Config | apps/backend/config/reconciliation.yaml |
Weight parameters |
| Model Definition | apps/backend/src/models/reconciliation.py |
ORM |
2. Architecture Model¶
Bank transaction entity: BankStatementTransaction (from statement extraction) is the reconciliation input.
Reconciliation Thresholds¶
| Score Range | Action | Status Transition |
|---|---|---|
| ≥ 85 | Auto-Accept | pending → auto_accepted |
| 60–84 | Review Queue | pending → pending_review |
| < 60 | Unmatched | stays pending |
SSOT: This table is the single authoritative definition of reconciliation score thresholds. All other files that mention these thresholds should reference this section:
See: docs/ssot/reconciliation.md#thresholds
Reconciliation Flow¶
flowchart TB
A[Import Statement] --> B[Parse Transactions]
B --> C[Generate Candidate Entries]
C --> D[Multi-Dimensional Scoring]
D --> E{Score Threshold}
E -->|≥85| F[Auto-Accept]
E -->|60-84| G[Review Queue]
E -->|<60| H[Unmatched]
F --> I[Status: reconciled]
G --> J[Manual Review]
J -->|Approve| I
J -->|Reject| H
State Machine¶
stateDiagram-v2
[*] --> pending: Create match
pending --> auto_accepted: Score ≥ 85
pending --> pending_review: Score 60-84
pending_review --> accepted: Manual confirm
pending_review --> rejected: Manual reject
pending_review --> superseded: Replace match
auto_accepted --> [*]
accepted --> [*]
rejected --> [*]
3. Multi-Dimensional Match Scoring¶
Scoring Weight Configuration¶
# apps/backend/config/reconciliation.yaml
scoring:
weights:
amount: 0.40 # Amount matching
date: 0.25 # Date proximity
description: 0.20 # Description similarity
business: 0.10 # Business logic
history: 0.05 # Historical pattern
thresholds:
auto_accept: 85 # Auto-accept
pending_review: 60 # Enter review queue
tolerances:
amount_percent: 0.005 # Amount tolerance 0.5%
amount_absolute: 0.10 # Amount absolute tolerance $0.10
date_days: 7 # Date tolerance days
Environment overrides:
RECONCILIATION_AUTO_ACCEPT_THRESHOLDRECONCILIATION_REVIEW_THRESHOLD
Scoring Algorithm¶
def calculate_match_score(
transaction: BankStatementTransaction,
entries: list[JournalEntry]
) -> MatchScore:
scores = {}
# 1. Amount matching (40%)
amount_diff = abs(transaction.amount - entry_total)
if amount_diff <= Decimal("0.01"):
scores["amount"] = 100
elif amount_diff / transaction.amount < Decimal("0.005"):
scores["amount"] = 90
elif amount_diff <= Decimal("5.00"):
scores["amount"] = 70 # Fee split heuristic
else:
scores["amount"] = max(0, 100 - float(amount_diff) * 10)
# 2. Date proximity (25%)
date_diff = min(abs((transaction.date - e.entry_date).days) for e in entries)
if date_diff == 0:
scores["date"] = 100
elif date_diff <= 3:
scores["date"] = 90
elif date_diff <= 7:
scores["date"] = 70
else:
scores["date"] = max(0, 100 - date_diff * 10)
# 3. Description similarity (20%)
scores["description"] = calculate_text_similarity(
transaction.description,
" / ".join(e.memo or "" for e in entries)
)
# 4. Business logic (10%)
scores["business"] = min(validate_business_logic(transaction, e) for e in entries)
# 5. Historical pattern (5%)
scores["history"] = check_historical_pattern(transaction)
# Weighted calculation
total = sum(
scores[k] * WEIGHTS[k]
for k in scores
)
return MatchScore(
total=total,
breakdown=scores
)
Versioning & Audit Trail¶
ReconciliationMatchrecords are immutable; corrections create a new version.- Use
versionandsuperseded_by_idto link replacements. - Active matches satisfy:
status != supersededandsuperseded_by_id IS NULL.
4. EPIC-011 Migration (Dual Read & Cutover)¶
During the migration to 4-Layer Architecture, the reconciliation engine supports two modes:
Phase 3: Dual Read Validation¶
Consistency checks run alongside normal operations: 1. Dual Read: Fetches both Layer 0 (Legacy) and Layer 2 (New) transactions. 2. Validation: Compares count and total amount. 3. Logging: Discrepancies logged as warnings.
Phase 4: Cutover (Layer 2 Read)¶
Activated via ENABLE_4_LAYER_READ=true:
1. Source Switch: Reads pending transactions from AtomicTransaction (Layer 2) instead of BankStatementTransaction.
2. Matching: Creates ReconciliationMatch records linked via atomic_txn_id.
3. Status: Uses existence of match record to determine status (since Atomic records are immutable).
4. Legacy Bypass: Does not update BankStatementTransaction status.
5. Design Constraints (Dos & Don'ts)¶
✅ Recommended Patterns¶
- Pattern A: Auto-matches must record
score_breakdownfor audit - Pattern B: One-to-many matches must verify amount totals
- Pattern C: Cross-period matches extend date tolerance to ±7 days
- Pattern D: Review queue updates use row-level locking and increment
versionto prevent concurrent overwrites - Pattern E (Performance): Matching engine must pre-fetch candidates for the entire statement period and cache historical pattern scores to avoid N+1 database queries.
⛔ Prohibited Patterns¶
- Anti-pattern A: NEVER mark as matched without scoring
- Anti-pattern B: NEVER delete rejected match records (preserve audit trail)
5. Standard Operating Procedures (Playbooks)¶
SOP-001: Handle Unmatched Transactions¶
- Check if there's a delayed corresponding record
- Try expanding date range to re-match
- Manually create entry and link
SOP-002: Batch Review¶
- Filter pending review records with same counterparty, similar amounts
- Sample verify 10% of matches for correctness
- Batch accept or reject
SOP-003: Handle Fee Discrepancies¶
- Identify difference < tolerance threshold
- Auto-suggest creating fee entry
- Link as combined match
6. Verification & Testing (The Proof)¶
| Behavior | Verification Method | Status |
|---|---|---|
| Exact match score > 85 | test_execute_matching_auto_accepts_exact_match |
✅ Done |
| Tolerance match score 60-84 | test_execute_matching_pending_review_and_unmatched |
✅ Done |
| One-to-many match | test_one_to_many |
⏳ Pending |
| Cross-period match | test_cross_period |
⏳ Pending |
Used by¶
7. EPIC-016 Two-Stage Review (New)¶
EPIC-016 introduces a two-stage review workflow before reconciliation:
Stage 1: Record-Level Review¶
Location: /statements/{id}/review
Purpose: Validate extracted transaction data against original document.
Flow:
1. Parse statement → status=PARSED
2. User reviews transactions with PDF preview
3. Balance chain validation (tolerance: 0.001 USD)
4. Approve → stage1_status=APPROVED, status=APPROVED
5. Reject → stage1_status=REJECTED, status=REJECTED
Balance Validation Logic:
opening_delta = abs(stated_opening - derived_opening)
closing_delta = abs(stated_closing - calculated_closing)
valid = (opening_delta <= 0.001) AND (closing_delta <= 0.001)
New Fields (BankStatement):
- stage1_status: PENDING_REVIEW | APPROVED | REJECTED | EDITED
- balance_validation_result: JSONB with validation details (opening/closing deltas)
- stage1_reviewed_at: Timestamp
- manual_opening_balance: Manual override for first statement
Stage 2: Consistency Checks¶
Location: /reconciliation/review-queue
Purpose: Run deduplication, transfer detection, and anomaly checks before batch approval.
Check Types:
| Type | Description | Severity |
|------|-------------|----------|
| duplicate | Same amount/date/description within 1 day (global check) | high |
| transfer_pair | Matching OUT/IN across accounts (global check) | medium |
| anomaly | Large amount, frequency spike, new merchant | varies |
Constraint: Batch approve blocked if unresolved checks exist.
State Machine:
[*] --> pending: Check detected
pending --> approved: User acknowledges (idempotent)
pending --> rejected: User flags for fix
pending --> flagged: Needs manual review
API Endpoints¶
| Method | Path | Description |
|---|---|---|
| GET | /statements/{id}/review |
Stage 1 review data with PDF URL |
| POST | /statements/{id}/review/approve |
Approve with balance validation |
| POST | /statements/{id}/review/reject |
Reject and trigger re-parse |
| POST | /statements/{id}/review/edit |
Edit transactions and approve |
| POST | /statements/{id}/review/opening-balance |
Set manual opening balance |
| GET | /statements/stage2/queue |
Stage 2 review queue (global) |
| POST | /statements/{id}/stage2/run-checks |
Run consistency checks for statement |
| POST | /statements/consistency-checks/{id}/resolve |
Resolve a check |
| GET | /statements/consistency-checks/list |
List/filter consistency checks |
| POST | /statements/batch-approve-matches |
Batch approve matches |
| POST | /statements/batch-reject-matches |
Batch reject matches |
Files¶
| Dimension | Location |
|---|---|
| Model | apps/backend/src/models/statement.py (Stage1Status) |
| Model | apps/backend/src/models/consistency_check.py |
| Service | apps/backend/src/services/statement_validation.py |
| Service | apps/backend/src/services/consistency_checks.py |
| Router | apps/backend/src/routers/statements.py |
| Frontend | apps/frontend/src/app/(main)/statements/[id]/review/page.tsx |
| Frontend | apps/frontend/src/app/(main)/reconciliation/review-queue/page.tsx |