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

Offer State Machine & Pipeline

Offer State Machine & Pipeline

As of v0.1.27, offers move through a formal state machine with enforced transitions. This replaces the previous freeform status field and ensures every status change is validated, timestamped, and recorded in an immutable audit trail.


Pipeline Statuses

StatusLabelDescription
invitedInvitedOffer created; applicant invited to proceed.
in_progressIn ProgressApplicant is actively filling in the offer.
with_agentWith AgentAgent is reviewing the offer.
awaiting_amendmentsAwaiting AmendmentsAgent has requested changes from the applicant.
sent_to_landlordSent to LandlordOffer forwarded to landlord for consideration.
landlord_reviewedLandlord ReviewedLandlord has reviewed the offer.
acceptedAccepted✅ Terminal — offer accepted.
rejectedRejected❌ Terminal — offer rejected.
cancelledCancelled❌ Terminal — offer cancelled.

Terminal statuses (accepted, rejected, cancelled) cannot transition to any other status.


Valid Transitions

invited            → in_progress, cancelled
in_progress        → with_agent, cancelled
with_agent         → awaiting_amendments, sent_to_landlord, cancelled
awaiting_amendments→ in_progress, cancelled
sent_to_landlord   → landlord_reviewed, cancelled
landlord_reviewed  → accepted, rejected, cancelled
accepted           → (terminal)
rejected           → (terminal)
cancelled          → (terminal)

Database Schema

offers Table

Key columns added in v0.1.27:

ColumnTypeDescription
propertyIdUUID FKLinks offer to a property.
leadApplicantIdUUID FKLinks offer to the lead applicant.
createdByUserIdUUID FKUser who created the offer.
statusenumCurrent pipeline status (default: invited).
invitedAttimestampSet when status enters invited.
inProgressAttimestampSet when status enters in_progress.
withAgentAttimestampSet when status enters with_agent.
awaitingAmendmentsAttimestampSet when status enters awaiting_amendments.
sentToLandlordAttimestampSet when status enters sent_to_landlord.
landlordReviewedAttimestampSet when status enters landlord_reviewed.
acceptedAttimestampSet when status enters accepted.
rejectedAttimestampSet when status enters rejected.
cancelledAttimestampSet when status enters cancelled.

offerStatusTransitions Table (Audit Trail)

Immutable — rows are never updated or deleted.

ColumnTypeDescription
idUUIDPrimary key.
offerIdUUID FKThe offer being transitioned.
fromStatusenumPrevious status (null for initial creation).
toStatusenumNew status.
changedByUserIdUUID FKUser who triggered the transition.
reasontextOptional reason / note.
createdAttimestampWhen the transition occurred.

tRPC API Reference

offer.create

Creates a new offer at invited status.

Input:

{
  propertyId: string;       // UUID of the property
  leadApplicantId: string;  // UUID of the applicant
  // ...other offer fields
}

Behaviour: Sets status = "invited", records invitedAt, inserts an initial row in offerStatusTransitions, and writes an audit log entry.


offer.transitionStatus

Moves an offer to a new status. The transition is validated against the state machine before any write occurs.

Input:

{
  offerId: string;    // UUID of the offer
  toStatus: string;   // Target pipeline status
  reason?: string;    // Optional reason for the change
}

Response:

{
  offer: Offer;               // Updated offer record
  validNextStatuses: string[]; // Available transitions from the new status
}

Errors: Throws if the transition is invalid (e.g. attempting to move from accepted to any status).


offer.getValidTransitions

Returns the list of statuses an offer can move to from its current state. Use this to drive UI action buttons.

Input:

{ offerId: string }

Response:

{ validNextStatuses: string[] }

offer.getTransitionHistory

Returns the full audit trail of status changes for an offer.

Input:

{ offerId: string }

Response: Array of offerStatusTransitions rows ordered by createdAt.


offer.listByProperty

Lists all offers associated with a specific property, with pagination.

Input:

{
  propertyId: string;
  limit?: number;
  offset?: number;
}

offer.getById

Fetches a single offer. Now includes:

  • transitionHistory — ordered list of all status changes.
  • validNextStatuses — statuses the offer can move to.
  • isTerminal — boolean flag indicating no further transitions are possible.

offer.pipelineSummary

Returns a count of offers grouped by status, with human-readable labels for each group.


State Machine Helpers (src/lib/offer-state-machine.ts)

// Check whether a transition is permitted
isValidTransition(from: OfferPipelineStatus, to: OfferPipelineStatus): boolean

// Assert a transition is permitted — throws if not
assertValidTransition(from: OfferPipelineStatus, to: OfferPipelineStatus): void

// Get all statuses reachable from the current one
getValidNextStatuses(from: OfferPipelineStatus): OfferPipelineStatus[]

// Check whether a status is terminal
isTerminalStatus(status: OfferPipelineStatus): boolean

Exported constants:

  • OFFER_PIPELINE_STATUSES — ordered array of all 9 statuses.
  • ACTIVE_STATUSES — statuses considered active for query filtering (excludes terminal statuses).
  • OfferPipelineStatus — TypeScript type union.

Upgrade Notes

  • The updateStatus procedure has been replaced by transitionStatus. Update any client code that called offer.updateStatus.
  • The default offer status is now "invited" (previously "draft"). Any code that created offers with status: "draft" should be updated.
  • Management metrics now use ACTIVE_STATUSES from the state machine module — the stale statuses "draft", "submitted", "under_review", and "amended" are no longer referenced.