Skip to main content
All Docs
FeaturesPurple PepperUpdated April 7, 2026

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

ValueDescription
fixedFixed-term tenancy with a defined start and end date. An end date is required.
periodicRolling tenancy with no fixed end date.
hmoHouse in Multiple Occupation.

Lifecycle Statuses

Every tenancy term has a status field that progresses through the following values:

StatusDescription
pendingTerm created but not yet being actively processed
in_progressDefault initial status — term is being set up
ready_to_move_inAll checks complete; awaiting physical move-in
on_holdTemporarily paused; can resume to in_progress or ready_to_move_in
moved_inTenant has physically moved in
activeTerm is live and rent is being collected
periodicFixed term has expired and converted to a rolling periodic tenancy
expiredTerm end date passed without explicit action
set_to_endEnd of tenancy has been formally scheduled
endingActive notice period in progress
endedTerm has formally concluded (terminal)
fallen_throughTerm 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 to updateStatus or updateTermDetails against 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 a BAD_REQUEST error.
  • 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:

  1. tenancy_term_status_transitions — structured, queryable history of every transition for a term, including fromStatus, toStatus, user, reason, and optional metadata.
  2. audit_log — organisation-wide audit log via logAudit, 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.

FieldDescription
holdingDepositAmountPenceHolding deposit taken to secure the property
securityDepositAmountPenceTenancy deposit (protected under a scheme)
depositProtectionProviderScheme name, e.g. DPS, TDS, MyDeposits

To display values in pounds, divide by 100: (amountPence / 100).toFixed(2).