Service Charge Demands
Service Charge Demands
The Demands module lets you generate, manage, and dispatch per-unit service charge demands directly from your approved budgets. Each demand captures a leaseholder's full annual liability, splits it into installments, and tracks payment status — all within a tamper-evident audit trail.
Prerequisites
Before generating demands you need:
- At least one block with configured units
- Units with apportionment basis points set (the percentages must be defined; the block total must be non-zero)
- A budget in approved or final status — draft budgets cannot be used
Generating Demands
- Navigate to Demands in the dashboard sidebar
- Click Generate Demands
- Select an approved or finalised budget from the dropdown
- Choose an installment schedule:
- Annual — one payment due at the start of the financial year
- Half-Yearly — two payments, 6 months apart
- Quarterly — four payments, 3 months apart
- Click Generate Demands
The system will:
- Create one demand record per unit in the block
- Calculate each unit's share as
apportionmentBasisPoints / totalBasisPoints × budgetTotal - Snapshot the leaseholder's name, email, and unit number at generation time
- Store an itemised breakdown (each budget line item × the unit's share) on the demand
- Create the installment records with due dates starting from the financial year start month
Note: If demands already exist for a budget you must delete them before regenerating. Only undispatched demands can be deleted.
Installment Due Dates
Due dates are calculated from the block's financial year start month (default: April).
| Schedule | Installment dates |
|---|---|
| Annual | Month 1 of FY |
| Half-Yearly | Month 1, Month 7 of FY |
| Quarterly | Month 1, Month 4, Month 7, Month 10 of FY |
Penny-exact rounding is applied: any remainder from integer division is added to the first installment.
Demand List View (/dashboard/demands)
The list page shows:
- Summary cards — total demands, total charged, amount collected, and dispatched count
- Search — by leaseholder name, unit number, block name, or financial year
- Filters — by block and by payment status (Unpaid / Partial / Paid / Overdue)
Each demand row shows the unit number, leaseholder name, block, financial year, total amount, installment schedule, payment status badge, and dispatch status.
Demand Detail View (/dashboard/demands/[demandId])
The detail page provides:
Summary Cards
- Total Demand — the full annual amount
- Paid — amount received to date
- Outstanding — remaining balance
- Schedule — chosen installment schedule and the unit's apportionment share percentage
Itemised Breakdown
A table listing each budget category (e.g. Insurance, Management Fee, Reserve Fund Contribution) with its description and the unit's share amount.
Supported categories include: Cleaning, Insurance, Management Fee, Repairs & Maintenance, Reserve Fund Contribution, Utilities, Lift Maintenance, Fire Safety, Grounds Maintenance, Professional Fees, Communal Electricity, Water Rates, Door Entry System, Pest Control, Health & Safety, Accountancy, Company Secretary, Bank Charges, Sundries, Other.
Payment Installments
A table showing each installment with:
- Installment number
- Due date
- Amount due
- Amount paid
- Status badge (
Upcoming/Due/Paid/Overdue)
Payment Status Badges
| Badge | Meaning |
|---|---|
| Unpaid | Nothing received yet |
| Partial | Some payment received, balance outstanding |
| Paid | Demand settled in full |
| Overdue | Past due date with balance remaining |
Dispatching Demands
Dispatching marks a demand as sent to the leaseholder and creates a timestamped communication audit record.
- From the detail page: click Dispatch Demand on an individual demand
- From the list page or via the API: use
demand.bulkDispatchwith a list of demand IDs
Once dispatched:
- The demand shows a Dispatched badge with the dispatch timestamp
- The dispatch action button is hidden — the record is immutable
- The demand cannot be deleted (audit trail immutability)
- A communication record and recipient record are created per the Communication Audit Trail spec
QR Code Payment Reference
Every demand has a payment reference derived from the first 8 characters of its ID (uppercased). This reference is displayed on the detail page and will be embedded as a QR code in generated PDF demand letters to simplify payment identification.
Regenerating Demands
If you need to regenerate demands for a budget (e.g. after adding a unit or correcting apportionment):
- Ensure no demands have been dispatched for that budget — dispatched demands cannot be deleted
- Use
demand.deleteByBudget(or the delete action when available in the UI) to remove the undispatched demands - Generate demands again with the updated settings
tRPC API Reference
demand.list
Paginated list of demands for the organisation.
Input
{
limit?: number; // default 50
cursor?: string; // for pagination
budgetId?: string;
blockId?: string;
unitId?: string;
paymentStatus?: "unpaid" | "partial" | "paid" | "overdue";
dispatched?: boolean;
}
Returns paginated result with demand rows including blockName.
demand.getById
Fetch a single demand with its installments and parsed itemised breakdown.
Input
{ id: string }
Returns demand record plus:
breakdownItems: { category: string; description: string; amountPence: number }[]installments: DemandInstallment[]
demand.generate (admin)
Generate demands for all units from an approved or finalised budget.
Input
{
budgetId: string;
installmentSchedule: "annual" | "half_yearly" | "quarterly"; // default "annual"
}
Returns { demandsCreated: number }
Errors
PRECONDITION_FAILED— budget is a draftPRECONDITION_FAILED— demands already exist for this budgetPRECONDITION_FAILED— no units found for the blockPRECONDITION_FAILED— apportionment not configured (total basis points = 0)
demand.deleteByBudget (admin)
Delete all undispatched demands for a given budget.
Input
{ budgetId: string }
Errors
PRECONDITION_FAILED— some demands for this budget have already been dispatched
demand.bulkDispatch (admin)
Mark one or more demands as dispatched and create communication audit records.
Input
{
budgetId: string;
demandIds: string[]; // subset of demands to dispatch
}
Returns { dispatched: number }
demand.pdfData
Return all data required to render a PDF demand letter.
Input
{ id: string }
Returns demand details plus block address, agent branding, and bank account details.
demand.budgetDemandSummary
Aggregate statistics for all demands generated from a specific budget.
Input
{ budgetId: string }
Returns { count, totalAmountPence, paidAmountPence, dispatchedCount }
Data Model
service_charge_demands
| Column | Type | Description |
|---|---|---|
id | text PK | Auto-generated |
org_id | text | RLS tenant isolation |
budget_id | text | Source budget |
block_id | text | Block the demand belongs to |
unit_id | text | Unit (leaseholder) the demand is for |
financial_year | integer | e.g. 2025 for FY 2025/26 |
leaseholder_name | text | Snapshot at generation time |
leaseholder_email | text | Snapshot at generation time |
unit_number | text | Snapshot at generation time |
apportionment_basis_points | integer | Unit's share in basis points |
total_amount_pence | integer | Full annual demand |
paid_amount_pence | integer | Amount received (default 0) |
demand_payment_status | enum | unpaid | partial | paid | overdue |
installment_schedule | enum | annual | half_yearly | quarterly |
dispatched | boolean | Whether sent to leaseholder |
dispatched_at | timestamp | When dispatched |
communication_id | text | Linked communication record |
breakdown | text | JSON: [{category, description, amountPence}] |
notes | text | Optional notes |
generated_by | text | User who generated the demand |
demand_installments
| Column | Type | Description |
|---|---|---|
id | text PK | Auto-generated |
org_id | text | RLS tenant isolation |
demand_id | text | Parent demand |
installment_number | integer | 1, 2, 3, or 4 |
amount_pence | integer | Amount due for this installment |
paid_amount_pence | integer | Amount paid (default 0) |
due_date | timestamp | When payment is due |
installment_status | enum | upcoming | due | paid | overdue |
paid_at | timestamp | When this installment was settled |
notes | text | Optional notes |
Both tables are subject to Row-Level Security with org_id tenant isolation.