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:
- Scheduled: a daily task at 2:00 AM scans every association in the integrity scope, which is live books and not demo associations.
- 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.
- 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