Domain Invariants & Audit Trail
v0.1.0The 8 non-negotiable General Ledger invariants, domain exception catalog, and append-only forensic audit logging.
Domain Invariants & Forensic Audit Trail
The General Ledger is a statutory system of record. Every financial action must adhere to strict mathematical rules, and every change must be permanently verifiable.
The 8 Non-Negotiable Invariants (“The Law”)
flowchart TD
LAW["The 8 Non-Negotiable Ledger Invariants"]
L1["1. Zero-Sum Balance Parity<br/>Σ Debits = Σ Credits ($0 drift)"]
L2["2. Ledger Immutability<br/>Row-level edits forbidden; Storno reversals only"]
L3["3. Leaf-Only Postings<br/>Aggregator parents reject direct journal lines"]
L4["4. Fiscal Period Hard Locks<br/>Closed periods reject all mutation attempts"]
L5["5. Maker-Checker Segregation<br/>Voucher creator cannot approve own entry"]
L6["6. Base Rate Immutability<br/>Base currency FX rate permanently locked to 1.0"]
L7["7. Functional GL Denomination<br/>Postings persist in functional minor units"]
L8["8. ACL Domain Isolation<br/>Operational domains never touch accounting tables"]
LAW --> L1
LAW --> L2
LAW --> L3
LAW --> L4
LAW --> L5
LAW --> L6
LAW --> L7
LAW --> L8
Exception Catalog & Diagnostic Reference
When a domain invariant is violated, the engine halts with a specific, strongly-typed domain exception:
| Exception Class | Trigger Condition | Corrective Resolution |
|---|---|---|
UnbalancedJournalEntryException | Total debit minor units do not equal total credit minor units. | Adjust line items so that total debits equal total credits to the exact minor unit ($0 imbalance). |
ImmutableJournalEntryException | Direct SQL update() or delete() attempted on a POSTED or REVERSED voucher. | Never edit posted vouchers. Issue a Storno reversal via ium()->accounting()->journalEntries()->reverse(...). |
NonPostingAccountException | Attempted to assign a non-leaf aggregator account (is_leaf = false) to a journal line. | Target a specific child leaf account (is_leaf = true). |
FiscalPeriodClosedException | Voucher submission targeting a locked (CLOSING_REVIEW) or sealed (CLOSED) period. | Use an open period, or perform an administrative review unlock if authorized. |
MakerCheckerViolationException | The user who created the draft attempts to approve or post it. | Ensure the approver ID differs from the creator ID ($approverId !== $creatorId). |
BaseCurrencyRateImmutableException | Attempted to set an exchange rate on the organization’s base currency. | The base currency is the denominator (rate = 1.000000). Quote rates on non-base currencies only. |
Append-Only Forensic Audit Trail
All lifecycle transitions, approvals, reversals, and configuration mutations automatically write an entry to ium_accounting_audit_logs.
What Is Recorded?
- Action Type:
DRAFT_CREATED,SUBMITTED,APPROVED,POSTED,REVERSED,PERIOD_CLOSED,FX_RATE_UPDATED. - Actor Metadata: User ID, IP address, user agent.
- Payload Snapshot: Complete serialized JSON representation of the entity state before and after the action.
- Reason String: Mandatory justification for reversals, overrides, or period state changes.
Querying the Audit Trail
// Retrieve audit trail for a specific voucher
$auditLogs = ium()->accounting()->audit()->forEntry($voucherId);
foreach ($auditLogs as $log) {
echo "Action: {$log->action}\n";
echo "By User: {$log->actor_id} at {$log->created_at}\n";
echo "Reason: {$log->reason}\n";
dump($log->snapshot);
}