Five-Level Entity Hierarchy
Five-Level Entity Hierarchy
The platform organises all data within a strict five-level hierarchy:
Managing Agent → Owner → Block → Unit/Leaseholder → Management Accounts
This structure governs data organisation, reporting scopes, and permission boundaries across the entire application. Every entity must exist within this hierarchy — no entity can be created outside it.
Hierarchy Levels
Level 1 — Managing Agent
The top-level organisational container. Represents the property management agency and maps 1:1 with the shell organisations table. There is exactly one managing agent per organisation.
Level 2 — Owner
An individual or company portfolio holder. An owner holds one or more blocks under the managing agent. At least one owner must exist before blocks can be assigned.
Level 3 — Block
A single residential building. A block belongs to exactly one owner and cannot be shared across owners. Each block contains units and has management accounts attached to it.
Level 4 — Unit / Leaseholder
An individual flat or apartment within a block. Each unit has one current leaseholder with their own financial ledger and document storage.
Level 5 — Management Accounts
Financial data sources for a block — bank feeds (TrueLayer), Calmony client money accounts, manual ledgers, or accounting connectors. A block requires at least one management account before financial reporting is enabled.
Hierarchy Rules
- No orphaned entities. Every entity must be positioned correctly within the hierarchy. Blocks without owners, or units referencing non-existent blocks, are flagged as hierarchy violations.
- Reporting flows upward. Financial and compliance data aggregates from Units → Block → Owner → Managing Agent.
- Permissions cascade downward. A user's access scope is defined at a level in the hierarchy, and they see all entities below that level (subject to their role).
- Multi-tenant isolation. All hierarchy queries are scoped by
orgId. No data crosses organisational boundaries.
Dashboard Widget
The Entity Hierarchy widget on the main dashboard provides a live overview of the hierarchy for your organisation:
- Count of entities at each level.
- Completion indicator (✓ or ⚠) per level showing whether that level meets its minimum requirement.
- Completeness score (0–100%) shown as a progress bar, calculated as 25 points per completed requirement.
- Navigation links to the management page for each level.
- Guidance panel listing outstanding setup steps when setup is incomplete.
The widget is displayed alongside the Recent Activity feed in a two-column layout on the dashboard.
Completeness Scoring
| Requirement | Points |
|---|---|
| Managing agent profile configured | 25 |
| At least one owner exists | 25 |
| All blocks assigned to an owner | 25 |
| At least one management account connected to a block | 25 |
A score of 100% means the hierarchy is fully configured and all financial reporting features are available.
Permission Cascade
The hierarchy defines how block-level access is scoped per user role:
| Platform Role | Accessible Blocks |
|---|---|
principal | All blocks in the organisation |
read_only | All blocks in the organisation |
property_manager | Explicitly assigned blocks, or all blocks if no restriction is set |
freeholder | All blocks belonging to their scoped owner |
leaseholder | Their single scoped block only |
This is enforced at the data layer via getAccessibleBlockIds, which is called before any block-scoped query.
tRPC API Reference
All hierarchy endpoints are under the hierarchy namespace and require the blocks.read RBAC permission.
hierarchy.summary
Returns a full overview of the hierarchy for the current organisation.
Response
{
orgId: string;
agent: { id: string; name: string | null; tradingName: string | null } | null;
counts: {
owners: number;
blocks: number;
unassignedBlocks: number;
units: number;
managementAccountSources: number;
clientMoneyAccounts: number;
};
completeness: {
hasAgentProfile: boolean;
hasOwners: boolean;
allBlocksAssigned: boolean;
hasManagementAccounts: boolean;
score: number; // 0–100
};
hierarchyLevels: Record<HierarchyLevel, HierarchyLevelInfo>;
}
hierarchy.children
Returns the children of a given entity at the next hierarchy level.
Input
{
level: HierarchyLevel; // "managing_agent" | "owner" | "block" | "unit" | "management_account"
entityId: string;
}
Response
{
parentLevel: HierarchyLevel;
parentId: string;
children: HierarchyNode[];
}
For management_account (the leaf level), children is always an empty array.
hierarchy.ancestors
Resolves the full ancestor chain for an entity, ordered from root (Managing Agent) down to the entity's immediate parent. Used for breadcrumb navigation.
Input
{ level: HierarchyLevel; entityId: string; }
Response
{
entityId: string;
entityLevel: HierarchyLevel;
ancestors: Array<{ level: HierarchyLevel; id: string; name: string }>;
}
Example — resolving ancestors for a unit returns:
[ Managing Agent ] → [ Owner: "Smith Properties" ] → [ Block: "Maple Court" ]
hierarchy.validate
Validates that an entity is correctly positioned in the hierarchy. Returns a result object rather than throwing, making it suitable for diagnostic and admin views.
Input
{ level: HierarchyLevel; entityId: string; }
Response
{
valid: boolean;
level: HierarchyLevel;
entityId: string;
parentId: string | null;
parentLevel: HierarchyLevel | null;
issues: string[]; // human-readable descriptions of any hierarchy violations
}
Validation failures are written to the audit log with action hierarchy.validation_failed.
hierarchy.levels
Returns static metadata about all five hierarchy levels. No input required.
Response
{
levels: Array<{
key: HierarchyLevel;
label: string;
description: string;
parentLevel: HierarchyLevel | null;
childLevel: HierarchyLevel | null;
position: number; // 1–5
}>;
totalLevels: number; // 5
structure: string; // "Managing Agent → Owner → Block → Unit/Leaseholder → Management Accounts"
}
hierarchy.tree
Returns a two-level nested snapshot of owners and their blocks. Optimised for navigation UIs and portfolio selectors.
Response
{
tree: Array<{
owner: HierarchyOwnerNode;
blocks: HierarchyBlockNode[];
}>;
}
Hierarchy Node Types
type HierarchyLevel =
| "managing_agent"
| "owner"
| "block"
| "unit"
| "management_account";
interface HierarchyOwnerNode {
level: "owner";
id: string;
orgId: string;
name: string;
ownerType: string;
email: string | null;
childCount: number; // number of blocks
}
interface HierarchyBlockNode {
level: "block";
id: string;
orgId: string;
ownerId: string | null;
name: string;
addressLine1: string | null;
city: string | null;
postcode: string | null;
totalUnits: number | null;
childCount: number; // number of units
hasManagementAccount: boolean;
}
interface HierarchyUnitNode {
level: "unit";
id: string;
orgId: string;
blockId: string;
unitNumber: string;
floor: string | null;
status: string | null;
leaseholderName: string | null;
leaseholderEmail: string | null;
}
interface HierarchyAccountNode {
level: "management_account";
id: string;
orgId: string;
blockId: string;
sourceType: string; // e.g. "truelayer", "calmony", "manual"
accountType?: string;
label: string;
isActive: boolean;
}