Tenancy Term Lifecycle
Tenancy Term Lifecycle
The tenancy term lifecycle system manages the full journey of a tenancy term — from initial setup through to move-in, active tenancy, and eventual end — using a server-enforced state machine.
A tenancy may have multiple sequential terms (e.g. an initial fixed term followed by renewals). Each term is linked to its parent tenancy via tenancyId.
Term Types
| Value | Description |
|---|---|
fixed | Fixed-term tenancy with a defined start and end date. An end date is required. |
periodic | Rolling tenancy with no fixed end date. |
hmo | House in Multiple Occupation. |
Lifecycle Statuses
Every tenancy term has a status field that progresses through the following values:
| Status | Description |
|---|---|
pending | Term created but not yet being actively processed |
in_progress | Default initial status — term is being set up |
ready_to_move_in | All checks complete; awaiting physical move-in |
on_hold | Temporarily paused; can resume to in_progress or ready_to_move_in |
moved_in | Tenant has physically moved in |
active | Term is live and rent is being collected |
periodic | Fixed term has expired and converted to a rolling periodic tenancy |
expired | Term end date passed without explicit action |
set_to_end | End of tenancy has been formally scheduled |
ending | Active notice period in progress |
ended | Term has formally concluded (terminal) |
fallen_through | Term did not proceed (terminal) |
State Machine
Transitions are enforced server-side. Attempting an invalid transition returns a BAD_REQUEST error with a message listing the allowed next states.
pending
└─► in_progress
└─► fallen_through ← terminal
in_progress
└─► ready_to_move_in
└─► on_hold
└─► fallen_through ← terminal
ready_to_move_in
└─► moved_in
└─► on_hold
└─► fallen_through ← terminal
on_hold
└─► in_progress
└─► ready_to_move_in
└─► fallen_through ← terminal
moved_in
└─► active
active
└─► periodic
└─► expired
└─► set_to_end
└─► ended
periodic
└─► set_to_end
└─► ended
expired
└─► ended
set_to_end
└─► ended
└─► ending
ending
└─► ended
ended ← terminal
fallen_through ← terminal
Terminal states (
ended,fallen_through) do not allow any further transitions. Calls toupdateStatusorupdateTermDetailsagainst a term in a terminal state will be rejected.
tRPC Procedures
All procedures are available under the tenancyTermLifecycle namespace and require an authenticated organisation context.
tenancyTermLifecycle.createTenancyTerm
Creates a new tenancy term in in_progress status, linked to an existing tenancy.
Input:
{
tenancyId: string; // Required — must exist in the same org
termType?: "fixed" | "periodic" | "hmo"; // Default: "fixed"
startDate: string; // ISO date string
endDate?: string; // Required for fixed-term types
monthlyRent: string;
holdingDepositAmountPence?: number; // Integer, in pence
securityDepositAmountPence?: number; // Integer, in pence
depositProtectionProvider?: string; // Max 200 chars
breakClause?: string; // Max 2000 chars
tenantName?: string;
tenantEmail?: string; // Must be a valid email
landlordName?: string;
landlordEmail?: string; // Must be a valid email
}
Notes:
- Fixed-term tenancies require an
endDate. Omitting it returns aBAD_REQUESTerror. - The property address is automatically resolved from the parent tenancy or property record if not already stored.
- The calling user is recorded as
createdByUserId.
tenancyTermLifecycle.getById
Fetches a single tenancy term by ID. The response includes a computed allowedTransitions array indicating which statuses the term can move to next — useful for conditionally rendering action buttons in the UI.
tenancyTermLifecycle.updateStatus
Transitions a term to a new lifecycle status. The transition is validated against the state machine before being applied.
Input:
{
termId: string;
newStatus: TermStatus;
reason?: string;
metadata?: Record<string, unknown>;
}
On success, a record is written to tenancy_term_status_transitions and an entry is added to audit_log.
tenancyTermLifecycle.confirmMoveIn
A convenience procedure that transitions the term from ready_to_move_in → moved_in → active in a single call. It also activates the parent tenancy record.
Input:
{
termId: string;
movedInAt?: string; // ISO timestamp; defaults to now
}
tenancyTermLifecycle.endTerm
Formally ends a term. Updates the term status to ended, records endedAt and endedReason, and propagates the change to the parent tenancy.
Input:
{
termId: string;
reason: string;
endedAt?: string; // ISO timestamp; defaults to now
}
tenancyTermLifecycle.listTransitions
Returns the full status change history for a term in reverse chronological order. Each record includes fromStatus, toStatus, changedByUserId, reason, metadata, and createdAt.
Input:
{
termId: string;
}
tenancyTermLifecycle.updateTermDetails
Updates financial and deposit fields on a term. Blocked for terminal states (ended, fallen_through).
Input:
{
termId: string;
holdingDepositAmountPence?: number;
securityDepositAmountPence?: number;
depositProtectionProvider?: string;
breakClause?: string;
monthlyRent?: string;
}
tenancyTermLifecycle.getStatusTransitions
Returns the full state machine definition — all valid statuses, allowed transitions per status, and available term types. Intended for use by the UI to dynamically determine which actions to display.
Response:
{
statuses: string[]; // All valid status values
transitions: Record<string, string[]>; // From-status → allowed to-statuses
termTypes: ["fixed", "periodic", "hmo"];
}
Audit Trail
Every status mutation (via updateStatus, confirmMoveIn, endTerm) writes to two places:
tenancy_term_status_transitions— structured, queryable history of every transition for a term, includingfromStatus,toStatus, user, reason, and optional metadata.audit_log— organisation-wide audit log vialogAudit, consistent with all other mutations in the system.
This dual-write ensures both per-term history (retrievable via listTransitions) and cross-entity audit visibility.
Deposit Fields
All monetary deposit values are stored as integers in pence to avoid floating-point precision issues.
| Field | Description |
|---|---|
holdingDepositAmountPence | Holding deposit taken to secure the property |
securityDepositAmountPence | Tenancy deposit (protected under a scheme) |
depositProtectionProvider | Scheme name, e.g. DPS, TDS, MyDeposits |
To display values in pounds, divide by 100: (amountPence / 100).toFixed(2).