Fund accounting is the practice of segregating an organization's money into distinct pools — each with its own constraints, its own balance, and its own reporting. For HOAs, the most important boundary is between the operating fund and the reserve fund.
CommunityPay enforces fund segregation at the ledger level. This page describes how that enforcement works, what it prevents, and why "tagging" transactions with fund labels is not the same as enforcing fund boundaries.
The Problem with Tagging
A common way to track funds is to tag transactions with a fund label. The transaction is posted to the general ledger, and the fund label is metadata attached after the fact.
This approach has a structural weakness: the tag is advisory. Nothing prevents a user from posting a transaction that moves cash between funds without changing the tag. Nothing prevents a report from aggregating tagged and untagged transactions. Nothing prevents a migration or bulk import from omitting tags entirely.
The result is that fund balances in tag-based systems are only as reliable as the discipline of the people entering data. For an HOA with a volunteer board and a rotating property manager, that discipline is inconsistent.
CommunityPay takes a different approach. Fund assignment is not a tag — it is a constraint enforced by the same guard system that enforces double-entry balance, closed period restrictions, and subledger reconciliation.
FundSegregationGuard
The FundSegregationGuard is one of 18 guards the enforcement dispatcher runs. It runs on every journal entry that is not an explicit fund transfer.
What It Checks
For each journal entry, the guard examines which bank-reconciled cash accounts are touched by the entry's lines. It then determines which funds those accounts belong to.
The guard flags a violation when:
- The entry touches cash accounts from two or more different funds in a single posting
- The entry touches cash from one fund plus untagged accounts that could represent a different fund
What It Excludes
The guard recognizes three explicit transfer transaction types:
| Transaction Type | Direction |
|---|---|
| transfer_to_reserve | Operating to Reserve |
| transfer_from_reserve | Reserve to Operating |
| fund_equity_transfer | Any inter-fund equity movement |
When the transaction type is one of these, the guard passes without evaluation. These are the only code paths that are permitted to cross fund boundaries.
The guard also excludes accounts that are not bank-reconciled cash — clearing accounts, undeposited funds, suspense accounts, and accounts receivable holding accounts. These accounts exist to facilitate transaction processing and are not subject to fund segregation rules.
What Happens on Failure
If the guard detects cross-fund cash movement on a non-transfer transaction, it returns a failure with reason code cross_fund_cash_movement. The enforcement dispatcher records the failure in an EnforcementDecision, and the journal entry is blocked.
The failure message identifies the violation and suggests two remediation paths:
- Reclassify the transaction as a transfer type
- Create an AuditOverride with scope FUND_SEGREGATION (which requires documented justification and is itself an immutable audit record)
Override Path
The guard supports overrides through the AuditOverride system. An authorized user can create a time-limited override with scope FUND_SEGREGATION. The override must include:
- Justification, of at least twenty characters, for why the override is necessary (writing an override takes the Change rules ability and a fresh authenticator code)
- The one fund it applies to
- An expiry, which for this scope may be at most fourteen days out
The override is itself an immutable record. Every journal entry that executes under an override links to the override in its EnforcementDecision. This means the audit trail shows not just that the entry was allowed, but why it was allowed and who authorized the exception.
FundConsistencyGuard
A second guard, fund_consistency, applies to every flow and cannot be overridden. Where the segregation guard fails an entry that touches cash in two funds, the consistency guard asks whether the one cash account an entry touches belongs to the fund the entry is posted in. Every fund-boundary cash account on an entry must belong to the entry's own fund. A payment out of operating cash tagged as a reserve entry fails it, because the reserve's statements would show reserve cash leaving while the operating account is the one that moved.
The guard also closes the transfer path. A posting of transfer_to_reserve or transfer_from_reserve must carry out one approved fund transfer of the association — the same amount and the same two cash accounts, approved by someone the rules authorize and posted by someone who may pay — or it does not post. fund_equity_transfer reclassifies equity between funds and moves no cash, so it has no disbursement for a board to authorize.
Each fund balances on its own: the ledger adds the interfund lines that keep every fund in balance, a reserve bill is recorded to reserve payables, and reserve interest credits the reserve's interest account. A reserve bill is paid online only after the board-approved transfer that moves its money to operating has posted.
Fund Model
Each HOA has one or more funds, each with a distinct type:
| Fund Type | Purpose |
|---|---|
| OPERATING | Day-to-day operational expenses |
| RESERVE | Long-term capital replacement |
| SPECIAL | Special assessment collections |
| CAPITAL | Capital improvement projects |
Balance Computation
Fund balances are computed from the general ledger, not maintained as a separate field. The get_balance() method sums the balances of all accounts assigned to the fund, applying the accounting equation:
Fund Balance = Total Assets - Total Liabilities
A separate get_equity_balance() method sums only the equity accounts assigned to the fund. In a properly balanced fund, these two values are equal. The get_balance_summary() method returns both values and flags whether they agree — a built-in reconciliation check.
Funding Adequacy
For reserve funds, the model tracks:
| Field | Source |
|---|---|
| Funding target | Set from the most recent professional reserve study |
| Percent funded | Current balance divided by funding target |
| Funding status | fully_funded (>=100%), adequate (70-99%), underfunded (30-69%), critically_underfunded (<30%) |
These values are computed on demand from the ledger, not cached. The percent funded that appears in a report is the percent funded at the moment the report is generated.
Fund Policies
Fund policies are declarative constraints that govern how fund money can be spent, transferred, or received. Each policy has a type, a set of rules, and an enforcement level. Changing a fund policy asks for a fresh authenticator code.
Policy Types
| Type | What It Controls | At payment |
|---|---|---|
| EXPENSE_CATEGORY | Which expense accounts can be charged to this fund | Enforced |
| APPROVAL_THRESHOLD | Requires board vote or a specific approver seat at or above a dollar amount | Enforced |
| PROJECT_RESTRICTION | Limits spending to a specific project, with optional vendor and budget restrictions | Recorded |
| TRANSFER_RESTRICTION | Limits which funds can receive transfers, with maximum amounts and approval requirements | Recorded |
| SOURCE_RESTRICTION | Controls which revenue sources can contribute to this fund | Recorded |
The fund_policy guard enforces the first two on bill payments. It reads the expense accounts the bill charges, and the bill's live approval for the board vote or approver seat. The other three types are recorded and are not enforced by the guard.
Enforcement Levels
| Level | Behavior |
|---|---|
| BLOCK | The payment is refused if it violates the policy |
| WARN | The payment posts, and the finding is recorded in the guard's evidence on the enforcement decision |
| AUDIT | The payment posts, and the finding is recorded in the guard's evidence on the enforcement decision |
The guard cannot be overridden. The board's levers are the policy itself: its enforcement level, its end date, and its active flag. A policy that exists but cannot be evaluated — the bill is missing, its expense accounts cannot be resolved, a required role has no record — is a finding against that policy, judged at the policy's own level. Policies are selected as of the payment's effective date, so a policy dated after the payment does not govern it.
Policy State on Every Decision
The fund policies in force, the association's duty settings, and the active eligibility rules and exclusion triggers are hashed into the policy_snapshot_hash recorded on every enforcement decision, and the canonical description behind each hash is stored and never modified. The policy that governed a payment on January 15 can therefore be reproduced exactly, even if the policy was edited or ended afterward.
Fund Transfer Workflow
Moving money between funds is a controlled operation with a mandatory approval workflow.
Transfer Lifecycle
DRAFT → PENDING_APPROVAL → APPROVED → COMPLETED
↘ REJECTED
↘ CANCELLED
- DRAFT: Created but not submitted. Can be edited.
- PENDING_APPROVAL: Submitted for board review. Cannot be edited.
- APPROVED: Board has approved. Ready for execution.
- COMPLETED: Transfer executed. Journal entry created via JournalEngine.
- REJECTED: Board rejected. Cannot be resubmitted (new transfer required).
- CANCELLED: Withdrawn before completion.
No backward transitions. A rejected transfer cannot be re-opened. A completed transfer cannot be reversed through the transfer workflow — it requires a separate reversal entry through the enforcement dispatcher, and a reversed transfer entry leaves its transfer marked REVERSED.
Journal Entry Creation
When a transfer is completed, it creates a journal entry through the JournalEngine — the same choke point used by all financial transactions. The entry:
- Debits the destination fund's cash account
- Credits the source fund's cash account
- Passes through the enforcement dispatcher (which skips the FundSegregationGuard for transfer types, and whose FundConsistencyGuard requires the approved transfer)
- Creates an immutable EnforcementDecision
The transfer record links to the resulting journal entry, creating a traceable chain from the board approval to the ledger posting.
Validation
Fund transfers enforce several constraints:
- Amount must be greater than zero
- Source and destination funds must be different
- Source and destination accounts must belong to their respective funds
- Justification is required before submission for approval
These constraints are enforced at the model level, not just in the UI. A transfer that violates them cannot be saved regardless of how the save is attempted.
CARI Integration
Fund accounting health signals, including the reserve ratio and the operating ratio, feed the CARI Financial Health sub-score, which carries a 30% weight, the heaviest single component in the composite score.
For published methodology and component weights, see CARI Methodology and Scoring Framework.
This de
How CommunityPay Enforces This