Skip to main content

Two-person posted-entry reversal

A posted journal entry is never edited, deleted, or voided. For a supported event, GreatBook can stage a durable reversal proposal whose plan is derived from the exact journal and native business state. Staging writes no compensating journal and changes no native record.

The current web path

  1. A person opens an eligible posted event and supplies only a reversal date and written reason.
  2. The server resolves the exact journal UUID and derives its checksum, source family, native references, compensating lines, and plan fingerprint.
  3. POST /gl/journal-entries/{entry_id}/reversal-proposals stores the attributed proposal.
  4. The browser navigates to /books/ledger/reversals/{proposal_id}, where a second person reads that exact plan.
  5. Approval carries the reviewed plan fingerprint; the approver identity comes from the verified session, never the request body.
  6. The service re-derives the plan against current state, records the distinct approver, posts the compensating journal, and compensates the linked native records in one transaction.
This proposal-specific path replaced the older generic staged-workspace route. The generic workspace adapter cannot create the durable approved reversal_proposals row that the canonical writer now requires, so documenting it as the web commit path would lead a reviewer to a control that can only refuse or replay.

Two distinct people

The proposer may be a named human or an attributable assistant. The approver must be a named human and must not be the same person as the proposer after canonical identity normalization. The database also requires decided reversal proposals to carry a decider distinct from the proposer. An approval is not stored as a durable intermediate state. The approval and compensating post occur in the same transaction, so a failed post rolls the approval back with it.

What it refuses

The browser cannot supply journal lines, amount, family, checksum, organization, role, or approver. The service refuses unsupported or partial native state, an already reversed event, a date before the original entry, a closed reversal period, a stale plan fingerprint, a changed source plan, a same-person approval, or a concurrent live proposal. The losing request writes no GL-only mirror. Rejecting a proposal requires a written reason and writes nothing to the ledger. The proposer may withdraw their own staged proposal; another named person may reject it.

Production boundary

The backend and web path are deployed at the pinned release heads, but it is not usable in production today. The control requires two distinct eligible identities and production currently has only one bound identity, so no production reversal was attempted for this documentation cut.

Verified at source

  • backend/app/services/reversal_proposal_service.py owns proposal lifecycle, distinct-human approval, exact replay, staleness checks, and atomic posting.
  • backend/app/services/business_reversal_service.py owns supported-family compensation and native-state refusals.
  • langgraph_chat/devcenter/reversal_api.py stages and lists proposals; reversal_proposal_api.py reads, approves, and rejects one proposal.
  • src/components/ledger/propose-reversal-action.tsx stages from an exact entry and navigates without posting.
  • src/components/ledger/reversal-proposal-review.tsx and src/lib/reversal-proposals.ts bind the second person’s decision to the exact reviewed plan.