Overview
The Accounts Receivable system manages the complete resident billing lifecycle: assessment generation, invoice delivery, payment receipt, late fee enforcement, payment plan management, and aging analysis. Every financial operation in this lifecycle flows through the JournalEngine with full enforcement guard evaluation.
The ARService and ARAgingService together provide the operational and analytical layers of AR management.
Invoice Generation
Assessment Scheduling
The generate_assessments method creates invoices from assessment schedules for all applicable units. Key controls:
- Per-unit duplicate checking: Before creating an invoice, the service checks for existing invoices matching the same unit, schedule, and period. This prevents double-billing on re-runs and automatically handles new units added after initial generation.
- Voided invoice handling: Previously voided invoices are excluded from duplicate checking, allowing regeneration when needed.
- Schedule tracking: The schedule's
last_generated_dateandlast_generated_periodare updated for reporting. - Schedule changes are recorded: Every create and edit of an assessment schedule writes an audit entry — who, which fields, before and after — in the same transaction as the change, so a change with no record cannot commit. The board is told of any changed field.
- Billing every unit takes a fresh authenticator code: Generating invoices from a schedule asks for a second factor confirmed within the last ten minutes.
Journal Entry Creation
Every invoice creates a journal entry through JournalEngine:
DR: Accounts Receivable (assessment amount)
CR: Income Account (assessment amount)
This records revenue at invoice creation (accrual accounting). The entry includes subledger references linking to both the invoice and unit for drill-down capability.
Payment Processing
Payment Receipt
The receive_payment method records incoming payments with mandatory controls:
- Stripe fee linkage: When a payment originates from Stripe (
stripe_payment_intentis set), the receipt must link to the sourcePaymentobject. This is enforced at the method level — calling with a Stripe intent but no source payment raises aValueError.
Payment Application
The apply_payment method supports three allocation strategies:
| Strategy | Behavior |
|---|---|
| FIFO (default) | Apply to oldest outstanding invoices first |
| Specific | Apply to explicitly specified invoices with specified amounts |
| Proportional | Apply proportionally across all outstanding invoices |
Defense-in-Depth Fee Verification
Before any Stripe payment can be applied to invoices, two conditions are enforced:
- Fee reconciliation check: If
fee_reconciliation_failedisTrueon the source payment, application is permanently blocked until manual review. - Fee confirmation check: The source payment must have
fee_confirmed=True, meaning the actual Stripe fee has been retrieved from the Balance Transaction API.
These checks are unskippable — they evaluate the FK on the receipt, not an optional parameter. They apply to a payment that carries a processor transaction. A check or cash receipt has no processor fee to confirm.
Payment Journal Entry
DR: Cash account where the money landed (net amount received)
DR: Bank Charges (processor fee, if applicable)
CR: Assessments Receivable (amount applied to invoices)
CR: Prepaid Assessments (any amount beyond what the invoices needed)
The entry separates the fee from the payment, ensuring accurate expense tracking. A payment that settled through the processor debits the association's processor sub-account, because that is where the cash physically lands. A check, cash, or direct deposit debits operating cash. The bank account is debited later, by the entry that records the processor's payout. Cash beyond what the applied invoices needed is held as prepaid assessments, a current liability, and never as a negative receivable.
Matching to the Bank
Bank transactions, from the bank feed or an imported statement, are matched one to one to the ledger's cash entries by machine. The match never posts an entry. A person reconciles and completes the month. See Bank Reconciliation Controls.
Late Fee Enforcement
Late fees are off until the board turns them on. The association's settings hold the master switch, the fee amount, the day dues become late, and the first dues period fees apply to, so enabling late fees is never retroactive. Two further settings roll the late day past weekends and past US federal holidays for associations whose governing documents say so. Each is its own switch.
A daily job, check_late_fees, runs just after midnight and assesses the fee. Its rules:
- Dues are late the day after the late day passes, rolled forward past weekends and holidays if the board chose. An invoice generated late in its month gets the same runway as every other.
- Other charges, such as special assessments and manual invoices, are late the day after the due date the board set. A charge with no due date on record is never assessed. A charge accrues a late fee only if it was marked eligible when it was created: dues are eligible by default, and special assessments and manual invoices opt in.
- A payment in flight accrues nothing. An ACH initiated before the late day is a good-faith payment. If it later fails, the next run assesses the fee.
- A canceled, refunded, or relieved charge accrues nothing, and an archived association is frozen.
- A deadline that fell while the association was archived earns the standard runway from the restore.
- One fee per charge, ever. A unique database constraint refuses a second fee for the same charge, so a re-run cannot double-assess.
Each fee is its own invoice with its own journal entry, created in one step:
DR: Accounts Receivable
CR: Late Fee Income
Accounts resolve by role, not by number. If any piece fails, nothing is billed, the failure is reported, and the next run retries. When the unit's owner has an email on file, the fee is sent as a new invoice.
Payment Plans
For delinquent accounts, the service supports structured payment arrangements:
- Plan creation: Links multiple outstanding invoices into a single payment plan with configurable monthly amounts, number of payments, start date, payment day, optional down payment, and interest rate.
- Default detection: The
check_payment_plan_defaultsmethod monitors active plans for missed scheduled payments and transitions plans toDEFAULTEDstatus when tolerance is exceeded.
Aging Analysis
The ARAgingService provides institutional-standard aging analysis:
Standard Aging Buckets
| Bucket | Days Past Due |
|---|---|
| Current | Not yet due (due_date >= as_of_date) |
| 1-30 Days | 1-30 days past due |
| 31-60 Days | 31-60 days past due |
| 61-90 Days | 61-90 days past due |
| Over 90 Days | 91+ days past due |
Summary Report
The get_ar_aging_summary method produces:
- Bucket totals and percentages
- Per-unit aging detail with owner names
- Top 10 delinquent units ranked by total amount
- Severely delinquent list (units with balances in 90+ bucket)
Collection Effectiveness Metrics
The get_collection_effectiveness method calculates for any fiscal year:
- Collection rate: Total payments / total charges
- Days Sales Outstanding (DSO): Ending AR balance / average daily charges
- Net change: Ending balance vs. beginning balance
Aging Trend Analysis
The get_ar_aging_trend method provides month-over-month aging snapshots for up to 12 months, using stored ARSnapshot records when available or reconstructing from invoice data for historical periods.
AR Snapshots
The generate_ar_snapshot method creates point-in-time AR records including:
- Bucket totals and percentages
- Delinquent unit counts and delinquency rate
- Average days delinquent (weighted by amount)
- Per-unit detail with owner information
Unit Ledger
Payments applied through the receivables service, refunds, and relief adjustments create a UnitLedger entry with a running balance. A late fee, which the daily command posts directly, creates none. The get_statement_data method rebuilds statements from invoice and payment records, so a statement is accurate even where a ledger entry is missing, and a late fee still appears on it.
CARI Integration
Delinquency is read from one place. The aging service computes the past-due share of the open receivable, and the L5 rule Maximum Delinquency is written on the aging service's own population and buckets, so the rule and the delinquency report cannot disagree. The rule is defined and is not in force in production today. The CARI market snapshot records the number of delinquent units from the same service and a delinquency rate that is the share of units with a balance, a different measure from the past-due share of the receivable that the rule reads, and a trailing twelve-month collection rate, collected over billed.
For published methodology and component weights, see CARI Methodology and Scoring Framework.
How CommunityPay Enforces This
- All invoice and payment journal entries flow through JournalEngine with guard evaluation
- Per-unit duplicate checking prevents double-billing for the same assessment period
- A processor payment posts only after its actual fee is confirmed from the processor's records
- One late fee per charge, enforced by a unique database constraint
- Every create and edit of an assessment schedule leaves an audit record in the same transaction, and billing every unit takes a fresh authenticator code
- Institutional-standard aging buckets: Current, 1-30, 31-60, 61-90, 90+ days