Skip to content

B-12 · Reviewer exception queue

SOP: SOP_AI_Bookkeeping_Automation.md §5.3.4 + §7 (Exception handling)Actors: Senior Accountant (SENIOR_ACCOUNTANT role recommended; BOOKKEEPER can view but not override). Pre-state: At least one UnreconciledItem with status = CLIENT_RESPONDED and clientResponse = NO_SUPPORTING whose amount is at or above the client's materialityThresholdSgd. Post-state: Item resolved with a reviewer-assigned classification (default: write-off / suspense). Item flips to RESOLVED.

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:

http
GET /ops/bookkeeping/exceptions?clientId=<zenithId>&minAmount=50

Filters:

  • clientId (required)
  • minAmount — defaults to client's materialityThresholdSgd. Override to inspect items below threshold (e.g. for a sweep).
  • statusPENDING | 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 EXEMPT for no-supporting cases)
    • Override reason text area

1.3 Resolve

Pick a classification (e.g. Drawings or Misc Expense) and click Resolve.

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

  1. Validates the reviewer has Action.OVERRIDE_JOURNAL_FLAG (SENIOR_ACCOUNTANT / PLATFORM_ADMIN).
  2. Creates a new JournalEntry in the current month's batch with status = APPROVED, flags = ['NO_SUPPORTING_CLIENT_DECLARED', 'REVIEWER_RESOLVED'], reviewedBy = <staff>, reviewNotes = overrideReason.
  3. Links the entry back to the unreconciled item (unreconciled_items.journal_entry_id).
  4. Transitions UnreconciledItem.status = RESOLVED. Stamps resolvedBy, resolvedAt.
  5. Writes bookkeeping.exception.resolved audit 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:

http
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

sql
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 JournalBatch increments 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_QUEUE

3. 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.

Internal use only — BreezyCorp