B-12 · Reviewer exception queue
SOP:
SOP_AI_Bookkeeping_Automation.md§5.3.4 + §7 (Exception handling)Actors: Senior Accountant (SENIOR_ACCOUNTANTrole recommended;BOOKKEEPERcan view but not override). Pre-state: At least oneUnreconciledItemwithstatus = CLIENT_RESPONDEDandclientResponse = NO_SUPPORTINGwhose amount is at or above the client'smaterialityThresholdSgd. Post-state: Item resolved with a reviewer-assigned classification (default: write-off / suspense). Item flips toRESOLVED.
ZENITH seeded one item that lands directly here: the SGD 500 ATM withdrawal (above the SGD 50 materiality threshold) with the client note "Counter-branch cash withdrawal — no invoice trail."
0. Prerequisites
- At least one item meeting the criteria above.
- Logged in as
SENIOR_ACCOUNTANT/PLATFORM_ADMIN.
1. Steps
1.1 Open the queue
Web: /dashboard/bookkeeping/exceptions.
ZENITH should show two rows by default (PENDING + CLIENT_RESPONDED states):
- IRAS GST payment (PENDING, SGD 25.50 — under threshold but surfaced because the paired journal entry is FLAGGED).
- ATM withdrawal (CLIENT_RESPONDED, SGD 500.00 — above the SGD 50 materiality threshold → reviewer judgement required).
API:
GET /ops/bookkeeping/exceptions?clientId=<zenithId>&minAmount=50Filters:
clientId(required)minAmount— defaults to client'smaterialityThresholdSgd. Override to inspect items below threshold (e.g. for a sweep).status—PENDING | CLIENT_RESPONDED | RESOLVED.
1.2 Open an exception
Click the ATM withdrawal row.
The detail surface shows:
- Bank txn details (date, narration, amount, balance).
- The client note (rendered as a quoted block): "Counter-branch cash withdrawal — no invoice trail."
- Suggested classifications based on similar past items + a free-form Classify and resolve form:
- Debit account dropdown (filtered to client's CoA)
- Credit account dropdown (defaults to the bank account)
- Tax treatment dropdown (defaults to
EXEMPTfor no-supporting cases) - Override reason text area
1.3 Resolve
Pick a classification (e.g. Drawings or Misc Expense) and click Resolve.
POST /ops/bookkeeping/exceptions/<unreconciledItemId>/acknowledge
{
"decision": "RESOLVE",
"debitAccount": "9999", // suspense account
"creditAccount": "1000",
"taxTreatment": "EXEMPT",
"overrideReason": "Owner draw, confirmed via email 2026-04-23. No supporting document expected."
}Server:
- Validates the reviewer has
Action.OVERRIDE_JOURNAL_FLAG(SENIOR_ACCOUNTANT/PLATFORM_ADMIN). - Creates a new
JournalEntryin the current month's batch withstatus = APPROVED,flags = ['NO_SUPPORTING_CLIENT_DECLARED', 'REVIEWER_RESOLVED'],reviewedBy = <staff>,reviewNotes = overrideReason. - Links the entry back to the unreconciled item (
unreconciled_items.journal_entry_id). - Transitions
UnreconciledItem.status = RESOLVED. StampsresolvedBy,resolvedAt. - Writes
bookkeeping.exception.resolvedaudit event with the override reason.
The row disappears from the default queue filter (it's now in status = RESOLVED).
1.4 Reject and bounce back
For an item that should not be resolved without more client input:
POST /ops/bookkeeping/exceptions/<unreconciledItemId>/acknowledge
{
"decision": "BOUNCE_BACK",
"note": "Need a copy of the cash advance memo before we can post."
}Server flips the item back to status = PENDING, optionally re-dispatching the magic link if you tick Re-notify client. Audit event bookkeeping.exception.bounced_back.
2. Verification
Database
SELECT status, resolved_by, resolved_at, journal_entry_id
FROM unreconciled_items WHERE id = '<itemId>';
SELECT status, debit_account, credit_account, flags, review_notes
FROM journal_entries WHERE id = '<unreconciledItem.journalEntryId>';Web UI
- Default filter (
status NOT IN ('RESOLVED')) no longer shows the resolved row. - The associated
JournalBatchincrements its approved-entry count. - The reconciliation run dashboard shows the unreconciled count drop.
Audit log
bookkeeping.exception.resolved itemId=<id> entryId=<id> overrideReason=<...>
bookkeeping.journal_entry.drafted sourceChannel=EXCEPTION_QUEUE3. Negative & edge cases
BOOKKEEPER(without override scope) → 403"OVERRIDE_JOURNAL_FLAG requires SENIOR_ACCOUNTANT or PLATFORM_ADMIN."- Resolve without
debitAccount/creditAccount→ 400 with the missing fields. - Item already
RESOLVED→ 409"Exception already resolved." - Resolve below materiality threshold — allowed (the queue is filterable, not gated). Reviewer can opt to clear small items in bulk via a multi-select UI.
- Bounce back without
note→ 400"Note is required when bouncing back to client." - Materiality threshold change after the fact — past resolutions stay; future items use the new threshold. The exception queue surfaces a one-time banner the first time the queue runs after the threshold changed.
Next
Once every item is RESOLVED, proceed to B-13 · Complete reconciliation & generate report.