Skip to main content
All Docs
FeaturesagentOS Block ManagerUpdated April 13, 2026

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:

  1. At least one block with configured units
  2. Units with apportionment basis points set (the percentages must be defined; the block total must be non-zero)
  3. A budget in approved or final status — draft budgets cannot be used

Generating Demands

  1. Navigate to Demands in the dashboard sidebar
  2. Click Generate Demands
  3. Select an approved or finalised budget from the dropdown
  4. 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
  5. 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).

ScheduleInstallment dates
AnnualMonth 1 of FY
Half-YearlyMonth 1, Month 7 of FY
QuarterlyMonth 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

BadgeMeaning
UnpaidNothing received yet
PartialSome payment received, balance outstanding
PaidDemand settled in full
OverduePast 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.bulkDispatch with 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):

  1. Ensure no demands have been dispatched for that budget — dispatched demands cannot be deleted
  2. Use demand.deleteByBudget (or the delete action when available in the UI) to remove the undispatched demands
  3. 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 draft
  • PRECONDITION_FAILED — demands already exist for this budget
  • PRECONDITION_FAILED — no units found for the block
  • PRECONDITION_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

ColumnTypeDescription
idtext PKAuto-generated
org_idtextRLS tenant isolation
budget_idtextSource budget
block_idtextBlock the demand belongs to
unit_idtextUnit (leaseholder) the demand is for
financial_yearintegere.g. 2025 for FY 2025/26
leaseholder_nametextSnapshot at generation time
leaseholder_emailtextSnapshot at generation time
unit_numbertextSnapshot at generation time
apportionment_basis_pointsintegerUnit's share in basis points
total_amount_penceintegerFull annual demand
paid_amount_penceintegerAmount received (default 0)
demand_payment_statusenumunpaid | partial | paid | overdue
installment_scheduleenumannual | half_yearly | quarterly
dispatchedbooleanWhether sent to leaseholder
dispatched_attimestampWhen dispatched
communication_idtextLinked communication record
breakdowntextJSON: [{category, description, amountPence}]
notestextOptional notes
generated_bytextUser who generated the demand

demand_installments

ColumnTypeDescription
idtext PKAuto-generated
org_idtextRLS tenant isolation
demand_idtextParent demand
installment_numberinteger1, 2, 3, or 4
amount_penceintegerAmount due for this installment
paid_amount_penceintegerAmount paid (default 0)
due_datetimestampWhen payment is due
installment_statusenumupcoming | due | paid | overdue
paid_attimestampWhen this installment was settled
notestextOptional notes

Both tables are subject to Row-Level Security with org_id tenant isolation.