Five-Level Entity Hierarchy
Five-Level Entity Hierarchy
The platform organises all data within a strict five-level entity hierarchy. This structure is the foundational architectural constraint — every entity must exist within it, and no entity can be created outside it.
Level 1 — Managing Agent
Level 2 — Owner
Level 3 — Block
Level 4 — Unit / Leaseholder
Level 5 — Management Accounts
Two core data-flow rules apply across the entire platform:
- Reporting flows upward — child counts and financial data aggregate to parent levels
- Permissions cascade downward — access granted at a higher level applies to all children
Hierarchy Levels
Level 1 — Managing Agent
The top-level container. Represents the managing agent organisation (one per org/tenant). All other entities sit beneath the agent.
Level 2 — Owner
An individual or company that holds one or more blocks in their portfolio. An owner must exist before a block can be assigned to them.
Level 3 — Block
A residential block. Each block belongs to exactly one owner. A block must have a valid owner assigned before child entities (units, management accounts) can be created beneath it.
Level 4 — Unit / Leaseholder
An individual unit within a block. A unit must belong to a valid block.
Level 5 — Management Accounts
Financial account sources associated with a block (e.g. management account sources, client money accounts). Require the parent block to have a valid owner.
Hierarchy Completeness Score
The platform calculates a completeness percentage (0–100%) for the hierarchy, weighted equally across four checks:
| Check | Weight |
|---|---|
| Managing agent profile is configured | 25% |
| At least one owner exists | 25% |
| All blocks have an owner assigned | 25% |
| All blocks have at least one management account source | 25% |
A score of 100% means the hierarchy is fully established. Scores below 100% indicate missing configuration, shown as warnings on the dashboard.
When no blocks exist yet, the block and management account checks are considered passing — incomplete hierarchy warnings apply only once data starts being added.
Dashboard Widgets
Two widgets on the main dashboard surface hierarchy status at a glance.
Entity Hierarchy (HierarchySummary)
Displays all five hierarchy levels in sequence with per-level status:
| Indicator | Meaning |
|---|---|
| ✅ Green check | Level is fully configured |
| ⚠️ Amber warning | Level has data but with gaps (e.g. unassigned blocks) |
| ○ Empty circle | Level has no data yet |
Includes a colour-coded completeness progress bar:
- Green — 100% complete
- Amber — 50–99% complete
- Red — below 50% complete
Portfolio Structure (HierarchyTree)
A collapsible tree showing the live portfolio structure:
● Managing Agent
├── Owner A (3 blocks)
│ ├── Block 1 🚪 12 units
│ ├── Block 2 🚪 8 units
│ └── Block 3 🚪 24 units
└── Owner B (1 block)
└── Block 4 🚪 6 units
Blocks without an assigned owner appear under an "Unassigned Blocks" group, highlighted in amber.
Validation Rules
All entity-creation flows enforce hierarchy integrity through a set of validation utilities. The key business rules are:
- An owner must exist before a block can be assigned to it.
- A block must have a valid owner before units or management accounts can be created beneath it. Attempting to create a child entity on an ownerless block returns a
PRECONDITION_FAILEDerror:"This block does not have a valid owner assigned. Per the entity hierarchy, a block must belong to an owner before child entities (units, management accounts) can be created."
- Every entity is scoped to its org — cross-org access is prevented at every validation step.
API Reference
All hierarchy endpoints are available under the hierarchy tRPC router. They require the blocks.read permission and are scoped to the authenticated organisation.
hierarchy.summary
Returns aggregated entity counts and the hierarchy completeness score.
Returns:
{
agentConfigured: boolean;
ownerCount: number;
blockCount: number;
blocksWithoutOwner: number;
unitCount: number;
unitsWithoutBlock: number;
managementAccountSourceCount: number;
blocksWithManagementAccount: number;
blocksWithoutManagementAccount: number;
hierarchyComplete: boolean;
completenessPercentage: number; // 0 | 25 | 50 | 75 | 100
}
hierarchy.tree
Returns the full portfolio tree for visualization.
Returns:
{
agent: {
orgId: string;
configured: boolean;
};
owners: Array<{
id: string;
name: string;
level: "owner";
childCount: number; // number of blocks
children: Array<{
id: string;
name: string;
level: "block";
childCount: number; // number of units
}>;
}>;
}
Blocks without an owner are grouped under a synthetic node with id: "__unassigned__".
hierarchy.path
Resolves the full hierarchy path for a block or unit.
Input:
{
entityType: "block" | "unit";
entityId: string;
}
Returns:
{
agent: { orgId: string; agentId: string | null } | null;
owner: { id: string; name: string } | null;
block: { id: string; name: string } | null;
unit: { id: string; unitNumber: string } | null;
}
Useful for breadcrumb navigation and contextual display.
hierarchy.validate
Checks whether an entity is valid and correctly positioned in the hierarchy.
Input:
{
entityType: "agent" | "owner" | "block" | "unit";
entityId?: string; // required for owner, block, unit
}
Returns:
{
valid: boolean;
level: "agent" | "owner" | "block" | "unit";
issues: string[]; // human-readable descriptions of any hierarchy problems
}
Example issues returned:
"Managing agent profile has not been configured for this organisation.""Block does not have a valid owner assigned. Per the entity hierarchy, blocks must belong to an owner.""Unit's parent block does not have a valid owner assigned. The hierarchy chain is broken at the block → owner level."
Using Validation in Custom Routers
When building new entity-creation endpoints, import the validation utilities from @/lib/hierarchy to enforce hierarchy constraints before inserting records:
import {
requireBlockHasOwner,
validateOwnerInHierarchy,
} from "@/lib/hierarchy";
// Gate: block must have an owner before a unit can be created
await requireBlockHasOwner(input.blockId, ctx.orgId);
// Throws PRECONDITION_FAILED if not met
// Soft check: verify owner exists
const { valid, owner } = await validateOwnerInHierarchy(input.ownerId, ctx.orgId);
if (!valid) throw new TRPCError({ code: "NOT_FOUND", message: "Owner not found." });