Ledger Integrity Verification

Automated general ledger health verification with 12 integrity checks, immutable scan snapshots, an anchored decision chain, and severity-based finding management.

11 min read Accounting Controls Published Oct 22, 2024

CommunityPay runs automated integrity checks against the general ledger to verify that the accounting data is internally consistent. This page describes what those checks examine, what they produce, and how findings connect to payment controls.

Purpose

A general ledger can develop inconsistencies through several mechanisms: application bugs, interrupted transactions, manual corrections applied incorrectly, migration errors, or race conditions in concurrent posting. Most of these are rare. Some are inevitable over a long enough timeline.

The question is not whether inconsistencies will occur, but whether they will be detected when they do. CommunityPay's integrity scan converts "we believe the ledger is correct" into "we can demonstrate the ledger is correct as of this scan."

The Checks

Each scan runs 12 checks.

1. Balance Verification

What it checks: Every journal entry in the ledger has total debits equal to total credits.

This is the most fundamental accounting invariant. A journal entry where debits do not equal credits indicates a broken posting — either a line was added or removed after the entry was created, or the posting logic produced an unbalanced entry.

On failure: Each unbalanced entry is recorded as a finding with the debit total, credit total, and difference amount.

2. Orphan Line Detection

What it checks: Every journal entry line has a valid parent journal entry.

An orphaned line is a line item that exists in the database but is not associated with a journal entry. This can occur if a journal entry is deleted (which should not happen — entries are immutable) or if a line is created through a code path that bypasses the JournalEngine.

On failure: Each orphaned line is recorded as a finding with the line ID and account reference.

3. Enforcement Coverage

What it checks: Every journal entry created after the enforcement cutover date (January 24, 2025) has an associated EnforcementDecision.

The enforcement dispatcher is the mandatory choke point for all financial decisions. If a journal entry exists without an EnforcementDecision, it means the entry was created through a code path that bypassed the dispatcher. This is either a bug or a policy violation.

A database constraint trigger now refuses any new entry that carries no decision. This check detects what the trigger cannot see: rows that predate it.

Cutover scoping: Journal entries created before January 24, 2025 are excluded from this check. The enforcement dispatcher was activated on that date. Entries before that date were created under the prior system and are not expected to have enforcement decisions.

On failure: Each entry without an enforcement decision is recorded as a finding with the entry ID, posting date, and amount.

4. Fund Assignment

What it checks: Journal entry lines that touch fund-tracked accounts have a fund assignment.

Fund accounting requires that every transaction affecting a fund's accounts is assigned to the correct fund. A line without a fund assignment means the transaction's impact on fund balances is ambiguous.

On failure: Each unassigned line is recorded as a finding with the line ID, account, and amount.

5. Closed Period Compliance

What it checks: No journal entries have posting dates that fall within a closed accounting period.

When an accounting period is closed, no new entries should be posted to that period. An entry in a closed period indicates either a bug in the period-closing logic or a manual intervention that bypassed the ClosedPeriodGuard.

On failure: Each entry in a closed period is recorded as a finding with the entry ID, posting date, and the closed period reference.

6. Control Account Reconciliation

What it checks: The general ledger balance of each control account — accounts receivable, accounts payable, and loan control — equals the total of its subledger.

A variance means subledger activity is not reflected in the GL, or the GL carries an entry the subledger does not know about. Either direction breaks the reconciliation an auditor performs first.

On failure: Each control account whose GL balance diverges from its subledger total is recorded as a finding with both figures and the variance.

7. Payment Receipt

What it checks: Every succeeded payment has a receipt, every completed receipt has a posted journal entry, and every reversed receipt has its reversal.

The check follows each payment forward, from payment to receipt to journal entry. It does not scan the ledger back to receipts. A completed receipt whose journal entry never posted means money arrived and the books do not show it.

On failure: Each unmatched receipt is recorded as a CRITICAL finding.

8. Subscription Invoice

What it checks: Every platform subscription charge has its expense journal entry posted.

The subscription webhook posts the HOA-side expense entry. If the charge left the association's bank and the entry never posted, the GL understates expense. The finding auto-resolves once the entry posts, whether by webhook retry or by hand.

On failure: Each charge without a posted expense entry is recorded as a finding.

9. Report Recompute

What it checks: The stored account balance equals a full recompute from posted journal entry lines, and the financial reports reproduce when recomputed. Each night the recompute covers the trial balance, the balance sheet, the accrual income statement, and the general-ledger endings; the cash-basis, cash flow, and fund-comparative recomputes run in the report verification command, not in the nightly scan.

This is the check that catches drift between the incrementally maintained current_balance field and the ledger it is supposed to summarize.

On failure: Each divergence is recorded as an ACCOUNT_BALANCE_DRIFT finding at CRITICAL severity, with the instruction to investigate the posting path rather than correct the stored number.

10. Decision Chain

What it checks: The enforcement decision log is an unbroken hash chain.

Immutability stops a decision being edited or deleted through the ORM. It says nothing about direct database access. Each decision hashes its predecessor, so a row removed or altered outside the ORM breaks the link. Rows after the gap still verify — the blast radius is one link, and the gap itself is the signal.

The chain is also anchored. Each integrity snapshot records an anchor into the chain inside its own hash, and every scan re-verifies up to the last 1,000 historical anchors. A decision deleted and a chain recomputed after it no longer resolves against the anchors already recorded.

On failure: The broken link is recorded as a finding identifying the decision where the chain does not reconcile.

11. Interfund

What it checks: The events that create a balance owed between funds, whether or not the ledger recorded it.

Three events are found: reserve cash paying an operating expense (and the reverse, reported separately), a budgeted reserve contribution whose cash never moved, and a reserve-to-operating transfer still unrestored past its one-year deadline, with the deadline computed.

On failure: This check reports and does not block. Every finding is a WARNING, so the snapshot reads YELLOW and no finding can stop a payment.

12. Vendor Payment Record

What it checks: The vendor payment record agrees with the ledger.

The record is written inside the posting and stamped inside the reversal, so any disagreement is a bypass or a bug. Every posted vendor payment entry has exactly one record. Every processing, paid, failed, or voided record has an entry. A record whose entry has a posted reversal is failed or voided, with the reversal linked. A paid record on the processor rail carries its payout id. A record still processing after 24 hours is reported as a warning: a held payout someone has to reconcile against the processor.

On failure: Each disagreement is recorded as a finding; a stale processing record is a WARNING.

IntegritySnapshot

Every scan produces an IntegritySnapshot — an immutable record of the scan results.

What It Contains

Field Description
Snapshot ID Unique identifier (UUID) for external reference
HOA Which HOA was scanned
Status Overall result: GREEN, YELLOW, or RED
Check results Pass/warn/fail status for each check
Finding counts CRITICAL, WARNING, and INFO findings
Ledger metrics Total journal entries and enforcement decisions at scan time
Content hash SHA-256 hash of the snapshot content
Scanned at Timestamp of the scan
Scanned by User who triggered the scan (null for automated scans)
Duration Execution time in milliseconds

Status Computation

The overall status is computed deterministically:

Condition Status
Any check returns FAIL RED
Any check returns WARN (and none FAIL) YELLOW
All checks return PASS GREEN

There is no weighting, no scoring, and no threshold configuration. A single failed check turns the entire scan RED. This is intentional — an accounting system with known integrity failures should not report a "mostly healthy" status.

Immutability

IntegritySnapshot inherits from both ImmutableModelMixin and ContentHashMixin. Once created, the snapshot cannot be modified or deleted. The content hash is computed before the first save and is included in the immutable field set.

This means a scan result from six months ago can be verified: recompute the content hash from the snapshot's fields and compare with the stored hash. If they match, the scan result has not been tampered with.

IntegrityFinding

Individual issues discovered during a scan are recorded as IntegrityFinding records.

Severity Levels

Severity Meaning Example
CRITICAL Ledger integrity is compromised; immediate action required Unbalanced journal entry, entry in closed period
WARNING Potential issue that should be investigated Missing fund assignment on a non-exempt account
INFO Informational observation, no action required Legacy entry without enforcement decision (pre-cutover)

Finding Lifecycle

Findings have a status: OPEN, ACKNOWLEDGED, or RESOLVED. New findings are created as OPEN. Staff can acknowledge a finding (indicating awareness without resolution) or resolve it (indicating the underlying issue has been corrected).

On subsequent scans, the service checks whether previously recorded findings still exist:

  • If a finding's underlying condition persists, the finding is updated with the latest scan reference
  • If the condition has been corrected, the finding is marked RESOLVED
  • New conditions produce new findings

Finding Cap

Each check caps the findings it records per scan, at a limit between 20 and 100 that depends on the check. If a check discovers more issues than its cap, only the first are recorded as individual findings. The check result still reflects the total count.

This cap exists to prevent a single systemic issue (e.g., a migration that broke fund assignments on thousands of entries) from overwhelming the findings table. The check result captures the full scope; the findings table captures enough detail for investigation.

What Happens to a CRITICAL Finding

A CRITICAL finding is reported. It appears on the scan history and detail pages, in the CPA audit portal, and in the close evidence pack. It stays OPEN until the condition is corrected; the next scan marks it RESOLVED once the condition is gone.

The scan does not stop a payment by itself. What refuses a bad posting is upstream of the scan: the enforcement guards evaluate every entry before it posts, and the database refuses an entry with no decision, an unbalanced posted entry, and a change to a posted line. The scan is the second look, at the whole ledger, that catches what those missed or what predates them.

Scheduling

A scan runs in three ways:

  1. Scheduled: a daily task at 2:00 AM scans every association in the integrity scope, which is live books and not demo associations.
  2. At period close: closing a period runs a scan, so the close evidence pack carries results as of the close, not a stale daily scan.
  3. Manual: anyone with permission to run integrity scans can start one from the association's audit scans page.

All paths produce identical IntegritySnapshot records. The scan_type records which path produced it, and scanned_by records the user for a manual scan and is null for an automated one.

What This Proves

A series of IntegritySnapshots over time proves:

  • The ledger was verified on each scan date
  • The specific checks that were performed and their results
  • Whether findings were detected, acknowledged, and resolved
  • That each scan result is tamper-evident (content hash)

An auditor reviewing an HOA's books can request the IntegritySnapshot history and verify that the ledger has been continuously monitored, that issues were detected and resolved, and that the scan results themselves have not been modified after the fact.

This is the difference between "we run checks" and "we can prove we run checks, and we can prove the results haven't been altered."


This describes pr

How CommunityPay Enforces This
  • 12 automated integrity checks: balance, orphan lines, enforcement coverage, fund assignment, closed period, control account reconciliation, payment receipt, subscription invoice, report recompute, decision chain, interfund, vendor payment record
  • IntegritySnapshot is immutable (ImmutableModelMixin) with SHA-256 content hash via ContentHashMixin
  • Enforcement cutover date (January 24, 2025) scopes enforcement coverage checks to avoid false positives on legacy entries
  • Findings are capped per check, between 20 and 100, to prevent database overload while the check result keeps the full count
  • CRITICAL findings stay open until the condition is corrected; the scan reports, and the enforcement guards and database rules refuse a bad posting before it lands
  • Overall status computed deterministically: any FAIL = RED, any WARN = YELLOW, all PASS = GREEN
Login