Overview
CommunityPay reconciles money at two layers. Every night, the ReconciliationService compares the general ledger with the payment processor's records in six steps. Every month, a person reconciles the bank account against the statement or the live bank feed, as described in Bank Statement Import and Matching. This page describes the nightly processor reconciliation and the checks that run beside it.
The processor reconciliation is not a monthly close activity. It runs daily as a scheduled task (run_daily_reconciliation, 2:00 AM) over the previous 24 hours, so a discrepancy is detected within a day of its occurrence.
Six-Step Reconciliation Process
Step 1: Payment Reconciliation
For each association with a connected processor account, the service fetches the processor's payments for the window and matches them to database records by provider_transaction_id. Matching is by identity, never by date window. Each processor charge is looked up by its own id, wherever the database row's creation date falls.
For each matched payment, the service creates a ReconciliationItem record with one of three outcomes:
- Matched: Amount and status align within $0.01 tolerance
- Amount Mismatch: Database amount differs from the processor's amount
- Status Mismatch: Database status differs from the processor's status
Critical detection: A payment found at the processor but missing from the database raises a high-severity alert. A payment in the database but missing from the processor is also flagged. Both represent potential data integrity issues.
Step 2: Refund Reconciliation
Refund records (the Refund model) are matched to the processor's refund records. Amount discrepancies are flagged as reconciliation items requiring action.
Step 3: Transfer Reconciliation
Platform fees were retired in favor of subscription pricing, so this step records zero fees collected.
Step 4: Payout Reconciliation
For each connected association, the processor's payouts are read over a window wider than the run, so a payout created yesterday and settled today is refreshed rather than left pending. Each is recorded as a StripePayout record. A failed payout raises a critical-severity alert that includes the failure reason and the affected amount.
For a vendor payout, this step is also the backstop for a missed webhook. It stamps the payout id on the vendor payment record, and a failed vendor payout is applied as a return: its entry reverses, the bill reopens, the record is stamped failed with the bank's reason, and the approval is used up, so paying again takes a new one.
Step 5: Balance Verification
For each association, the service reads its own processor balance and makes three checks.
- A negative available balance is a critical alert. The account is overdrawn at the processor, and the next vendor payout will fail.
- An available balance below the association's payout buffer target is a high alert.
- The ledger's processor sub-account is compared with what the processor holds for the association: the available, pending, and reserved balances, plus payouts created but not yet paid. If the books claim more than the processor holds by more than $0.01, that is a critical alert, because a vendor payout could be drawn against money that is not there. A negative sub-account balance is a critical alert in its own right.
The comparison is one-directional on purpose. The processor holding more than the books show is usually a charge that has settled whose webhook has not yet landed, and the integrity scan's receipt checks own that case. The processor sub-account has no bank statement and sits outside bank reconciliation, so this nightly comparison is the only external check on that balance.
Step 6: Alert Generation
All discrepancy items are reviewed. A payment above $100 missing from the processor is escalated to critical severity. When any discrepancy is found, a summary email goes to the configured payment-alert recipients. It also reports five payment-amount invariants, each of which should read zero: part-paid rows, overpaid rows, receivable-partial obligations, live rows on relieved invoices, and unlinked chargeable rows.
One condition is one alert. If an unresolved alert with the same type, association, and title already exists, the new occurrence is logged against it rather than duplicated.
ReconciliationRun Record
Each execution creates a ReconciliationRun record tracking:
- Run type (daily, monthly, manual, or processor payout)
- Date range covered
- Total payments checked
- Total processor transactions processed
- Matched count and discrepancy count
- Total amount disbursed in payouts, and fees collected (zero, since platform fees were retired)
- Completion status and any error message
Per-association balances are kept in the daily balance snapshots below.
Daily Balance Tracking
The DailyReconciliation service maintains per-association daily balance snapshots via the DailyBalance model:
- Opening balance: Previous day's closing balance (chain continuity)
- Payments received: Sum of the day's paid payments
- Refunds issued: Sum of completed refunds
- Payouts sent: Sum of paid payouts
- Closing balance: Computed from opening + receipts - refunds - payouts
Each snapshot is checked against the processor's available and pending balance for the connected account. Differences exceeding $1.00 are recorded in the balance_discrepancy field. The snapshot is a rollup derived from payments, with a one-dollar tolerance. The exact comparison is the nightly sub-account check in Step 5.
The Payout Bank Check
Each association has one pinned payout bank, and a vendor's bank can never be it. An hourly check, and a check on every bank and payout event, compare the processor's default payout bank with the pin. A mismatch pauses the association's automatic payouts and raises a critical sweep_destination alert. Payouts stay paused until a person restores the default and releases them. The sweep entry in the ledger posts only for a payout to the pinned bank.
Changing the pinned bank takes two people, and the association's bank PIN and a fresh authenticator code from each. A member who holds the Association funds permission asks, and a different board member confirms. The first payout to the new bank is held until a member who holds that permission, other than the requester, releases it, and the hold raises a high alert so staff can see it.
Month-End Bank Reconciliation
A person reconciles each bank account against a statement file or the live bank feed, one statement period at a time. The machine matches a bank transaction to a posted entry only when the amount is exact to the cent and no other entry fits. A person decides everything else, completes the month, and the match never posts an entry. The mechanism is described in Bank Statement Import and Matching.
Alert Severity Levels
| Condition | Alert Type | Severity |
|---|---|---|
| Payment at the processor, not in the database | missing_payment |
High |
| Processor data could not be fetched for an association | missing_payment |
High |
| High-value payment (> $100) in the database, not at the processor | missing_payment |
Critical |
| Payout failed at the processor | failed_payout |
Critical |
| Processor balance negative | balance_discrepancy |
Critical |
| Ledger processor sub-account negative | balance_discrepancy |
Critical |
| Ledger claims more processor cash than the processor holds | balance_discrepancy |
Critical |
| Available balance below the payout buffer target | balance_discrepancy |
High |
| Processor's default payout bank is not the pinned bank | sweep_destination |
Critical |
| First payout to a new payout bank held for release | sweep_destination |
High |
An amount or status difference on a matched payment is recorded as a reconciliation item that requires action.
Scheduling
| Task | Schedule | Purpose |
|---|---|---|
run_daily_reconciliation |
Daily, 2:00 AM | Full six-step reconciliation for the previous 24 hours |
update_all_daily_balances |
Daily, 3:00 AM | Per-association balance snapshots checked against the processor |
verify_payout_banks |
Hourly | Compares each association's processor default bank with its pinned bank |
bank_feed_sync_all |
Daily, 6:40 AM Pacific | Reads each linked bank account's posted transactions and balance. Posts nothing to the ledger |
How CommunityPay Enforces This
- Six-step processor reconciliation runs daily via a scheduled task
- Every processor payment is matched to its database record by the processor's transaction id
- Amount differences over $0.01 are flagged; a payment at the processor missing from the database raises a high alert, and a payment over $100 missing at the processor raises a critical one
- Every night the ledger's processor sub-account is compared with what the processor holds; the books claiming more than the processor holds is a critical alert
- Hourly, and on every bank and payout event, the processor's default payout bank is compared with the association's pinned bank; a mismatch pauses automatic payouts
- Month-end bank reconciliation by statement file or the live bank feed, matched one to one by machine