B-09 · Confirm proposed matches
SOP:
SOP_AI_Bookkeeping_Automation.md§5.2 — proposed matches require reviewer confirmationActors: Bookkeeper. Pre-state:ReconciliationRuninAWAITING_REVIEWwith one or morePROPOSEDmatches. Post-state: Each PROPOSED match is either confirmed (becomesMANUAL/ staysPROPOSEDwithconfirmedAt) or rejected (deleted, source bank txn flips toUnreconciledItem).
0. Prerequisites
- Run created via B-08 — or use the seeded ZENITH run.
1. Steps
1.1 Open the run
Web: /dashboard/bookkeeping/reconciliations/<runId>. The run detail shows two side-by-side panes — bank transactions (left) and journal entries (right) — with a Match status column. PROPOSED rows have a Confirm button.
1.2 Confirm a match
Click Confirm on the Uber PROPOSED match.
POST /ops/bookkeeping/reconciliations/<runId>/matches/confirm
{
"matchId": "<matchId>"
}Server:
- Validates the match belongs to this run.
- Stamps
confirmedAt = now(),matchedBy = <staff-jwt-email>. - Writes
bookkeeping.reconciliation.match_confirmed.
The match count in the header increments. The PROPOSED chip flips to CONFIRMED.
1.3 Reject a match (becomes unreconciled)
For a wrong PROPOSED, click Reject:
POST /ops/bookkeeping/reconciliations/<runId>/matches/<matchId>/reject
{ "reason": "Different vendor — same amount coincidence." }Server:
- Deletes the
ReconciliationMatchrow. - Creates an
UnreconciledItemwithstatus = PENDINGfor the bank txn. - Writes
bookkeeping.reconciliation.match_rejected.
1.4 Manual match (pair manually)
If the engine missed an obvious match (e.g. the journal entry was added after the matcher ran), use:
POST /ops/bookkeeping/reconciliations/<runId>/matches
{
"bankTxnId": "<txnId>",
"journalEntryId": "<entryId>",
"reason": "Manual pair — vendor name mismatch in bank narration."
}Creates a MANUAL match. The associated UnreconciledItem (if any) is removed. Audit event bookkeeping.reconciliation.manual_match_created.
1.5 Re-run the matcher (optional)
After fixing journal entries (e.g. flipping IRAS from FLAGGED to APPROVED):
POST /ops/bookkeeping/reconciliations/<runId>/run-matcherRe-evaluates only the unreconciled set. Preserves existing confirmed matches.
2. Verification
Database
SELECT match_type, confirmed_at IS NOT NULL AS confirmed, matched_by
FROM reconciliation_matches WHERE run_id = '<runId>';
SELECT bank_txn_id, status FROM unreconciled_items WHERE run_id = '<runId>';Web UI
- Header counts update live (
Confirmed: 3 / Proposed: 0 / Unreconciled: 3). - Each row shows the reviewer email and timestamp.
Audit log
bookkeeping.reconciliation.match_confirmed matchId=<id> matchedBy=<email>
bookkeeping.reconciliation.match_rejected matchId=<id> reason=<...>
bookkeeping.reconciliation.manual_match_created matchId=<id> entryId=<id>3. Negative & edge cases
- Confirm an EXACT match → 400
"Exact matches are auto-confirmed."(Idempotent — returns the existing state.) - Confirm a non-PROPOSED match → 400 with current
matchType. - Reject the only match for a bank txn → unreconciled item appears → expected. The downstream client portal flow (B-11) picks it up.
- Manual match where amounts don't match → 400
"Manual match requires amount equality (within 0.01 tolerance)."Use override (Action.OVERRIDE_JOURNAL_FLAG) to bypass — onlySENIOR_ACCOUNTANT+ can. - Manual match where journal entry is
DRAFT/FLAGGED→ 400"Cannot match against a non-APPROVED journal entry."Approve the entry first. - Run already
COMPLETE→ 400"Run is closed. Open a new run for further matches."
Next
The remaining bank lines are unreconciled. Proceed to B-10 · Dispatch unreconciled items to client.