Skip to content

B-09 · Confirm proposed matches

SOP: SOP_AI_Bookkeeping_Automation.md §5.2 — proposed matches require reviewer confirmationActors: Bookkeeper. Pre-state: ReconciliationRun in AWAITING_REVIEW with one or more PROPOSED matches. Post-state: Each PROPOSED match is either confirmed (becomes MANUAL / stays PROPOSED with confirmedAt) or rejected (deleted, source bank txn flips to UnreconciledItem).

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.

http
POST /ops/bookkeeping/reconciliations/<runId>/matches/confirm
{
  "matchId": "<matchId>"
}

Server:

  1. Validates the match belongs to this run.
  2. Stamps confirmedAt = now(), matchedBy = <staff-jwt-email>.
  3. 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:

http
POST /ops/bookkeeping/reconciliations/<runId>/matches/<matchId>/reject
{ "reason": "Different vendor — same amount coincidence." }

Server:

  1. Deletes the ReconciliationMatch row.
  2. Creates an UnreconciledItem with status = PENDING for the bank txn.
  3. 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:

http
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):

http
POST /ops/bookkeeping/reconciliations/<runId>/run-matcher

Re-evaluates only the unreconciled set. Preserves existing confirmed matches.

2. Verification

Database

sql
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 — only SENIOR_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.

Internal use only — BreezyCorp