Skip to content

Ledger audit trail

Backend domain: app/graphql/ledger_audit/ Migration: add_ledger_audit_logs RBAC Path: AUDIT_LOG

Overview

Every LedgerEntry POST and REVERSE now writes a row into ledger_audit_logs. Rows are hash-chained: each row's row_hash is sha256(prev_hash || canonical_json(payload) || at || actor_id). A broken link or altered payload makes ledgerAuditChainVerify return isValid: false.

payload is a JSON object snapshot of what changed (entry number, date, memo, source type/id, and the line breakdown for new entries).

Setup prerequisites

None — the audit is emitted automatically by LedgerService. Migrations must be applied per tenant.

GraphQL surface

Queries

ledgerEntryHistory(entryId: UUID!): [LedgerAuditLog!]!
ledgerAuditChainVerify(
  startSequence: Int = 1,
  endSequence: Int = null
): LedgerAuditChainVerification!

LedgerAuditLog fields: id, sequenceNumber, entryId, lineId, operation (POSTED | REVERSED | EDITED_MEMO | CLEARED | UNCLEARED), actorId, at, payload (JSON scalar), prevHash, rowHash.

LedgerAuditChainVerification: isValid, startSequence, endSequence.

Mutations

None. The audit is write-only via the ledger service.

Behaviour to handle in the UI

  • On any journal-entry detail page, add a History tab that calls ledgerEntryHistory(entryId) and renders a timeline. Each row shows operation, actor (resolve via users query if you need full name), timestamp, and a pretty-printed payload.
  • payload is a JSON scalar — render it with a collapsible JSON viewer rather than trying to map every field.
  • Add an Audit integrity badge in the global header for admin/accountant roles. Poll ledgerAuditChainVerify once per session (or on demand from a Settings → Audit page). Show green when isValid: true, red badge + actionable banner when false.

Suggested UX flow

  1. Entry history drawer — opens from any ledger entry showing the chain of POSTED → REVERSED events.
  2. Audit dashboard (admin only) — verify chain integrity, show last verified timestamp, expose a "Verify now" button that re-runs ledgerAuditChainVerify. If invalid, surface the sequence range to investigate.

Permissions

Both queries require Path.AUDIT_LOG. Limit menu visibility to ACCOUNTANT and ADMINISTRATOR roles.

Operation coverage

Operation Emitter
POSTED LedgerService.create_entry (every JE creation)
REVERSED LedgerService.reverse_entry
CLEARED BankReconciliationService.mark_cleared_ledger_lines
UNCLEARED BankReconciliationService.unmark_cleared_ledger_lines
EDITED_MEMO reserved — no current emitter; will fire from a future memo-edit mutation

Open follow-ups (not yet implemented)

  • No CSV export of the audit log yet; if a regulator asks, the data is already in ledger_audit_logs — easy add.