Skip to content

Reconciliation Engine Domain Model SSOT

SSOT Key: reconciliation Core 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 pendingauto_accepted
60–84 Review Queue pendingpending_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_THRESHOLD
  • RECONCILIATION_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

  • ReconciliationMatch records are immutable; corrections create a new version.
  • Use version and superseded_by_id to link replacements.
  • Active matches satisfy: status != superseded and superseded_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)

  • Pattern A: Auto-matches must record score_breakdown for 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 version to 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

  1. Check if there's a delayed corresponding record
  2. Try expanding date range to re-match
  3. Manually create entry and link

SOP-002: Batch Review

  1. Filter pending review records with same counterparty, similar amounts
  2. Sample verify 10% of matches for correctness
  3. Batch accept or reject

SOP-003: Handle Fee Discrepancies

  1. Identify difference < tolerance threshold
  2. Auto-suggest creating fee entry
  3. 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