{
 "$schema": "https://rapideyeinspections.com/blog/hotel-operations-data-model/schema.json",
 "name": "Hotel Operations Data Model",
 "version": "0.1",
 "published": "2026-08-01",
 "publisher": "RapidEye Research",
 "canonical": "https://rapideyeinspections.com/blog/hotel-operations-data-model/",
 "companion_model": "https://rapideyeinspections.com/blog/str-operations-data-model/",
 "formats": {
  "sql": "https://rapideyeinspections.com/blog/hotel-operations-data-model/schema.sql",
  "markdown": "https://rapideyeinspections.com/blog/hotel-operations-data-model/schema.md"
 },
 "license": "Free to use, adapt, and build on, with or without attribution.",
 "design_principles": [
  "Occupancy and cleanliness are two separate axes owned by two separate departments: front office owns occupied/vacant, housekeeping owns clean/dirty/inspected. Composite codes (VC, VD, VI, OC, OD) are derived by a generated column, never typed by hand. Discrepancy reports exist because the axes can disagree.",
  "Out of Order is not Out of Service: per Oracle OPERA's documented model, OOO rooms are removed from inventory availability while OOS rooms remain sellable. They are distinct availability states with their own episode records.",
  "Room status is an event log, not a column: the room row carries current state for board queries, room_status_log is the append-only history.",
  "Credits are the workload currency: housekeeping boards balance credits, not door counts. Credit values live on room_type and are copied onto each task at creation so history survives re-weighting.",
  "The inspection is a first-class entity separate from the clean: a clean makes a room Clean, only a supervisor inspection makes it Inspected. Findings escalate to work orders.",
  "The PMS stays the system of record for reservations: guest and reservation are a one-way synced mirror keyed by (source, external_id) for idempotent ingestion."
 ],
 "room_status_state_machine": {
  "codes": {
   "VC": "Vacant Clean: vacant + clean, awaiting inspection",
   "VD": "Vacant Dirty: vacant + dirty; every checkout lands here",
   "VI": "Vacant Inspected: the only state front desk should assign from where the inspection step runs",
   "OC": "Occupied Clean: occupied + serviced today",
   "OD": "Occupied Dirty: occupied + service due",
   "OOO": "Out of Order: removed from inventory availability; not sellable",
   "OOS": "Out of Service: flagged (minor defect) but remains in sellable inventory"
  },
  "notes": [
   "There is no OI (occupied inspected) state; inspection applies to vacant rooms.",
   "OPERA's optional Pickup status (light-touch refresh) is treated as dirty on the cleanliness axis; add a fourth axis value if your operation runs it.",
   "status_code on room is generated from occupancy_status x cleanliness_status x availability_status; OOO and OOS mask the other axes because availability outranks both."
  ],
  "transitions": [
   {
    "from": "OC",
    "to": "OD",
    "trigger": "night audit rollover: an occupied room owes service each day"
   },
   {
    "from": "OD",
    "to": "OC",
    "trigger": "stayover service completed"
   },
   {
    "from": "OC",
    "to": "VD",
    "trigger": "guest checkout"
   },
   {
    "from": "OD",
    "to": "VD",
    "trigger": "guest checkout"
   },
   {
    "from": "VD",
    "to": "VC",
    "trigger": "departure clean completed by room attendant"
   },
   {
    "from": "VC",
    "to": "VI",
    "trigger": "supervisor inspection passed"
   },
   {
    "from": "VC",
    "to": "VD",
    "trigger": "supervisor inspection failed, room returned to the board"
   },
   {
    "from": "VI",
    "to": "OC",
    "trigger": "guest check-in"
   },
   {
    "from": "VC",
    "to": "OC",
    "trigger": "guest check-in at properties not running the inspection step"
   },
   {
    "from": "VI",
    "to": "VD",
    "trigger": "inspected room sat vacant too long and reverts to dirty"
   },
   {
    "from": "VD",
    "to": "OOO",
    "trigger": "room removed from inventory (maintenance, renovation)"
   },
   {
    "from": "VC",
    "to": "OOO",
    "trigger": "room removed from inventory (maintenance, renovation)"
   },
   {
    "from": "VI",
    "to": "OOO",
    "trigger": "room removed from inventory (maintenance, renovation)"
   },
   {
    "from": "OOO",
    "to": "VD",
    "trigger": "room returned to inventory; it always re-enters dirty"
   },
   {
    "from": "VD",
    "to": "OOS",
    "trigger": "room flagged out of service but left sellable"
   },
   {
    "from": "VC",
    "to": "OOS",
    "trigger": "room flagged out of service but left sellable"
   },
   {
    "from": "VI",
    "to": "OOS",
    "trigger": "room flagged out of service but left sellable"
   },
   {
    "from": "OOS",
    "to": "VD",
    "trigger": "out-of-service flag cleared; room re-enters dirty"
   },
   {
    "from": "OOS",
    "to": "OC",
    "trigger": "room sold while out of service: OOS rooms remain sellable"
   }
  ]
 },
 "layers": {
  "property": "what you operate: property, floor_zone, room_type, room, rate_plan",
  "stay": "synced mirror, the PMS owns it: guest, reservation, stay",
  "room_status": "the state machine: room_status_log, room_status_transition, ooo_oos_event, discrepancy_report",
  "housekeeping": "the board, the credits, the inspection: staff, hk_assignment, hk_task, inspection, inspection_finding, linen_par_level",
  "maintenance": "assets, work orders, preventive cycles: asset, work_order, pm_schedule",
  "incidents": "what happens around the rooms: incident, lost_found_item"
 },
 "entities": {
  "property": {
   "layer": "property",
   "description": "The hotel. Carries its IANA timezone (all timestamps are stored UTC) and the PMS it syncs from.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "brand",
     "type": "text",
     "note": "flag / brand, if any"
    },
    {
     "name": "address_line1",
     "type": "text"
    },
    {
     "name": "address_line2",
     "type": "text"
    },
    {
     "name": "city",
     "type": "text"
    },
    {
     "name": "region",
     "type": "text"
    },
    {
     "name": "postal_code",
     "type": "text"
    },
    {
     "name": "country_code",
     "type": "text",
     "note": "ISO 3166-1 alpha-2"
    },
    {
     "name": "timezone",
     "type": "text",
     "required": true,
     "note": "IANA, e.g. America/Denver"
    },
    {
     "name": "source",
     "type": "text",
     "unique_with": "external_id",
     "note": "PMS this property syncs from, e.g. opera"
    },
    {
     "name": "external_id",
     "type": "text",
     "note": "id in that PMS"
    },
    {
     "name": "status",
     "type": "text",
     "enum": [
      "active",
      "pre_opening",
      "closed"
     ],
     "default": "active"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    },
    {
     "name": "updated_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "floor_zone": {
   "layer": "property",
   "description": "Floors, wings, towers, and housekeeping zones. Boards are built per zone; assets can live in a zone without living in a room.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "name",
     "type": "text",
     "required": true,
     "note": "'Floor 4', 'North Wing', 'Tower B 12-18'"
    },
    {
     "name": "kind",
     "type": "text",
     "enum": [
      "floor",
      "wing",
      "tower",
      "building",
      "zone"
     ],
     "default": "floor"
    },
    {
     "name": "sort_order",
     "type": "int"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "room_type": {
   "layer": "property",
   "description": "Carries the housekeeping credit baseline: workload is a property of the room type, and a checkout clean is worth roughly twice a stayover service.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "code",
     "type": "text",
     "required": true,
     "unique_with": "property_id",
     "note": "'KNG', 'DBL', 'STE'; unique per property"
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "description",
     "type": "text"
    },
    {
     "name": "max_occupancy",
     "type": "int"
    },
    {
     "name": "bed_config",
     "type": "text",
     "note": "'1 King', '2 Queens'"
    },
    {
     "name": "departure_credits",
     "type": "numeric(4,1)",
     "required": true,
     "default": "1.0",
     "note": "credit value of a checkout clean"
    },
    {
     "name": "stayover_credits",
     "type": "numeric(4,1)",
     "required": true,
     "default": "0.5",
     "note": "credit value of a stayover service"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    },
    {
     "name": "updated_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "room": {
   "layer": "property",
   "description": "Current state is denormalized here for fast board queries; authoritative history lives in room_status_log. status_code is a stored generated column, never written by hand.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "floor_zone_id",
     "type": "uuid",
     "references": "floor_zone"
    },
    {
     "name": "room_type_id",
     "type": "uuid",
     "required": true,
     "references": "room_type"
    },
    {
     "name": "room_number",
     "type": "text",
     "required": true,
     "unique_with": "property_id",
     "note": "text, not int: '204A', 'PH1'; unique per property"
    },
    {
     "name": "connects_to",
     "type": "text",
     "note": "connecting room number, if any"
    },
    {
     "name": "occupancy_status",
     "type": "text",
     "required": true,
     "enum": [
      "vacant",
      "occupied"
     ],
     "default": "vacant",
     "note": "front office's axis"
    },
    {
     "name": "cleanliness_status",
     "type": "text",
     "required": true,
     "enum": [
      "clean",
      "dirty",
      "inspected"
     ],
     "default": "dirty",
     "note": "housekeeping's axis"
    },
    {
     "name": "availability_status",
     "type": "text",
     "required": true,
     "enum": [
      "in_service",
      "out_of_service",
      "out_of_order"
     ],
     "default": "in_service"
    },
    {
     "name": "status_code",
     "type": "text",
     "enum": [
      "VC",
      "VD",
      "VI",
      "OC",
      "OD",
      "OOO",
      "OOS"
     ],
     "generated": true,
     "note": "projection of the three axes; OOO/OOS mask the others because availability outranks both"
    },
    {
     "name": "status_changed_at",
     "type": "timestamptz",
     "required": true,
     "default": "now()"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    },
    {
     "name": "updated_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "rate_plan": {
   "layer": "property",
   "description": "Deliberately light: exists so a reservation can say what it was sold on. Pricing, restrictions, and yielding stay in the PMS/RMS.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "code",
     "type": "text",
     "required": true,
     "unique_with": "property_id",
     "note": "'BAR', 'CORP', 'AAA'; unique per property"
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "description",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "guest": {
   "layer": "stay",
   "description": "Minimal PII on purpose: an operations tool needs to attribute a stay, not run a CRM.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "full_name",
     "type": "text"
    },
    {
     "name": "email",
     "type": "text"
    },
    {
     "name": "phone",
     "type": "text"
    },
    {
     "name": "source",
     "type": "text",
     "unique_with": "external_id"
    },
    {
     "name": "external_id",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "reservation": {
   "layer": "stay",
   "description": "One-way mirror of the PMS reservation, keyed by (source, external_id) so re-syncing is idempotent. Never write bookings here first. Reservations sell a room type, not a room.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "guest_id",
     "type": "uuid",
     "references": "guest"
    },
    {
     "name": "room_type_id",
     "type": "uuid",
     "references": "room_type"
    },
    {
     "name": "rate_plan_id",
     "type": "uuid",
     "references": "rate_plan"
    },
    {
     "name": "confirmation_number",
     "type": "text"
    },
    {
     "name": "source",
     "type": "text",
     "required": true,
     "unique_with": "external_id",
     "note": "'opera', 'mews', 'cloudbeds', 'ota'"
    },
    {
     "name": "external_id",
     "type": "text",
     "required": true
    },
    {
     "name": "arrival_date",
     "type": "date",
     "required": true
    },
    {
     "name": "departure_date",
     "type": "date",
     "required": true
    },
    {
     "name": "adults",
     "type": "int"
    },
    {
     "name": "children",
     "type": "int"
    },
    {
     "name": "status",
     "type": "text",
     "required": true,
     "enum": [
      "booked",
      "in_house",
      "checked_out",
      "cancelled",
      "no_show"
     ],
     "default": "booked"
    },
    {
     "name": "synced_at",
     "type": "timestamptz",
     "required": true,
     "default": "now()"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "stay": {
   "layer": "stay",
   "description": "The physical occupancy of one room. A reservation with a room move has two stay rows; occupancy questions join through stay, never through reservation.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "reservation_id",
     "type": "uuid",
     "required": true,
     "references": "reservation"
    },
    {
     "name": "room_id",
     "type": "uuid",
     "required": true,
     "references": "room"
    },
    {
     "name": "checked_in_at",
     "type": "timestamptz",
     "required": true
    },
    {
     "name": "checked_out_at",
     "type": "timestamptz",
     "note": "null while in-house"
    },
    {
     "name": "move_reason",
     "type": "text",
     "note": "set on the new row after a room move"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "room_status_log": {
   "layer": "room_status",
   "description": "Append-only history of every status change: when did 412 become guest-ready, who made it so, and through which states did it pass.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "room_id",
     "type": "uuid",
     "required": true,
     "references": "room"
    },
    {
     "name": "from_code",
     "type": "text",
     "enum": [
      "VC",
      "VD",
      "VI",
      "OC",
      "OD",
      "OOO",
      "OOS"
     ]
    },
    {
     "name": "to_code",
     "type": "text",
     "required": true,
     "enum": [
      "VC",
      "VD",
      "VI",
      "OC",
      "OD",
      "OOO",
      "OOS"
     ]
    },
    {
     "name": "changed_at",
     "type": "timestamptz",
     "required": true,
     "default": "now()"
    },
    {
     "name": "changed_by",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "source",
     "type": "text",
     "required": true,
     "enum": [
      "app",
      "pms_sync",
      "night_audit",
      "housekeeping",
      "front_office",
      "maintenance"
     ],
     "default": "app"
    },
    {
     "name": "note",
     "type": "text"
    }
   ]
  },
  "room_status_transition": {
   "layer": "room_status",
   "description": "The legal transitions of the state machine, as data, so an application or AI agent can validate a proposed change with one lookup. Seeded by schema.sql (see room_status_state_machine below).",
   "columns": [
    {
     "name": "from_code",
     "type": "text",
     "required": true,
     "enum": [
      "VC",
      "VD",
      "VI",
      "OC",
      "OD",
      "OOO",
      "OOS"
     ],
     "note": "primary key with to_code"
    },
    {
     "name": "to_code",
     "type": "text",
     "required": true,
     "enum": [
      "VC",
      "VD",
      "VI",
      "OC",
      "OD",
      "OOO",
      "OOS"
     ]
    },
    {
     "name": "trigger_event",
     "type": "text",
     "required": true,
     "note": "what legitimately causes this edge"
    }
   ]
  },
  "ooo_oos_event": {
   "layer": "room_status",
   "description": "One row per OOO or OOS episode. kind encodes the distinction that matters to revenue: out_of_order rooms are deducted from inventory availability; out_of_service rooms remain sellable while flagged.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "room_id",
     "type": "uuid",
     "required": true,
     "references": "room"
    },
    {
     "name": "kind",
     "type": "text",
     "required": true,
     "enum": [
      "out_of_order",
      "out_of_service"
     ]
    },
    {
     "name": "reason_code",
     "type": "text",
     "required": true,
     "enum": [
      "maintenance",
      "renovation",
      "deep_clean",
      "damage",
      "pest_control",
      "other"
     ],
     "default": "maintenance"
    },
    {
     "name": "description",
     "type": "text"
    },
    {
     "name": "starts_at",
     "type": "timestamptz",
     "required": true,
     "default": "now()"
    },
    {
     "name": "expected_return_at",
     "type": "timestamptz",
     "note": "what revenue management plans around"
    },
    {
     "name": "ended_at",
     "type": "timestamptz",
     "note": "null while the episode is open"
    },
    {
     "name": "work_order_id",
     "type": "uuid",
     "references": "work_order"
    },
    {
     "name": "created_by",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "discrepancy_report": {
   "layer": "room_status",
   "description": "Front office and housekeeping each own one axis of room status, so the two can disagree. skip: FO occupied, HK finds vacant. sleep: FO vacant, HK finds occupied.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "room_id",
     "type": "uuid",
     "required": true,
     "references": "room"
    },
    {
     "name": "business_date",
     "type": "date",
     "required": true
    },
    {
     "name": "fo_status",
     "type": "text",
     "required": true,
     "enum": [
      "vacant",
      "occupied"
     ],
     "note": "what the front office system shows"
    },
    {
     "name": "hk_status",
     "type": "text",
     "required": true,
     "enum": [
      "vacant",
      "occupied"
     ],
     "note": "what the attendant physically found"
    },
    {
     "name": "discrepancy_type",
     "type": "text",
     "enum": [
      "skip",
      "sleep",
      "none"
     ],
     "generated": true
    },
    {
     "name": "detected_by",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "detected_at",
     "type": "timestamptz",
     "required": true,
     "default": "now()"
    },
    {
     "name": "resolved_at",
     "type": "timestamptz"
    },
    {
     "name": "resolution_note",
     "type": "text"
    }
   ]
  },
  "staff": {
   "layer": "housekeeping",
   "description": "Everyone who touches a room or a work order.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "full_name",
     "type": "text",
     "required": true
    },
    {
     "name": "role",
     "type": "text",
     "required": true,
     "enum": [
      "room_attendant",
      "hk_supervisor",
      "exec_housekeeper",
      "front_desk",
      "maintenance",
      "engineer",
      "manager",
      "other"
     ]
    },
    {
     "name": "active",
     "type": "boolean",
     "required": true,
     "default": "true"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "hk_assignment": {
   "layer": "housekeeping",
   "description": "One attendant's section for one business date: the row a printed board line becomes. Sum of task credits against credit_target makes over-assignment visible.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "business_date",
     "type": "date",
     "required": true,
     "unique_with": "staff_id, shift"
    },
    {
     "name": "staff_id",
     "type": "uuid",
     "required": true,
     "references": "staff"
    },
    {
     "name": "floor_zone_id",
     "type": "uuid",
     "references": "floor_zone"
    },
    {
     "name": "shift",
     "type": "text",
     "required": true,
     "enum": [
      "am",
      "pm",
      "turndown",
      "overnight"
     ],
     "default": "am"
    },
    {
     "name": "credit_target",
     "type": "numeric(5,1)",
     "note": "what a full shift is at this property, e.g. 15.0"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "hk_task": {
   "layer": "housekeeping",
   "description": "One unit of housekeeping work against one room. credit_value is copied from room_type at creation so re-weighting credits later does not rewrite history.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "room_id",
     "type": "uuid",
     "required": true,
     "references": "room"
    },
    {
     "name": "assignment_id",
     "type": "uuid",
     "references": "hk_assignment"
    },
    {
     "name": "stay_id",
     "type": "uuid",
     "references": "stay",
     "note": "the stay being serviced, if any"
    },
    {
     "name": "business_date",
     "type": "date",
     "required": true
    },
    {
     "name": "task_type",
     "type": "text",
     "required": true,
     "enum": [
      "departure_clean",
      "stayover_service",
      "turndown",
      "deep_clean",
      "touch_up",
      "correction"
     ]
    },
    {
     "name": "credit_value",
     "type": "numeric(4,1)",
     "required": true,
     "default": "1.0",
     "note": "copied at creation"
    },
    {
     "name": "status",
     "type": "text",
     "required": true,
     "enum": [
      "pending",
      "in_progress",
      "done",
      "verified",
      "dnd",
      "refused",
      "skipped"
     ],
     "default": "pending"
    },
    {
     "name": "started_at",
     "type": "timestamptz"
    },
    {
     "name": "completed_at",
     "type": "timestamptz"
    },
    {
     "name": "note",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "inspection": {
   "layer": "housekeeping",
   "description": "The supervisor pass. A clean makes a room Clean; only this makes it Inspected. result drives the state machine: pass is VC to VI, fail is VC back to VD.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "room_id",
     "type": "uuid",
     "required": true,
     "references": "room"
    },
    {
     "name": "hk_task_id",
     "type": "uuid",
     "references": "hk_task",
     "note": "the clean being verified"
    },
    {
     "name": "inspector_id",
     "type": "uuid",
     "required": true,
     "references": "staff"
    },
    {
     "name": "inspected_at",
     "type": "timestamptz",
     "required": true,
     "default": "now()"
    },
    {
     "name": "result",
     "type": "text",
     "required": true,
     "enum": [
      "pass",
      "fail"
     ]
    },
    {
     "name": "score",
     "type": "numeric(5,2)",
     "note": "optional 0-100 against the property standard"
    },
    {
     "name": "note",
     "type": "text"
    }
   ]
  },
  "inspection_finding": {
   "layer": "housekeeping",
   "description": "What the inspection found, item-level. Findings survive the inspection that raised them: a chipped headboard found today is the work order verified next week.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "inspection_id",
     "type": "uuid",
     "required": true,
     "references": "inspection"
    },
    {
     "name": "category",
     "type": "text",
     "required": true,
     "enum": [
      "cleanliness",
      "damage",
      "maintenance",
      "missing_item",
      "safety",
      "other"
     ]
    },
    {
     "name": "severity",
     "type": "text",
     "required": true,
     "enum": [
      "minor",
      "major",
      "critical"
     ],
     "default": "minor"
    },
    {
     "name": "location",
     "type": "text",
     "note": "'bathroom', 'entry', 'balcony'"
    },
    {
     "name": "description",
     "type": "text",
     "required": true
    },
    {
     "name": "photo_uri",
     "type": "text",
     "note": "evidence beats adjectives"
    },
    {
     "name": "work_order_id",
     "type": "uuid",
     "references": "work_order",
     "note": "escalation"
    },
    {
     "name": "resolved_at",
     "type": "timestamptz"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "linen_par_level": {
   "layer": "housekeeping",
   "description": "Par levels per room type: what a made-up room carries, times the full sets in circulation (in the room, in the laundry, on the shelf).",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "room_type_id",
     "type": "uuid",
     "required": true,
     "references": "room_type",
     "unique_with": "item"
    },
    {
     "name": "item",
     "type": "text",
     "required": true,
     "note": "'bath_towel', 'king_sheet_set', 'bath_mat'"
    },
    {
     "name": "per_room_qty",
     "type": "int",
     "required": true,
     "note": "how many one made-up room carries"
    },
    {
     "name": "par_sets",
     "type": "numeric(3,1)",
     "required": true,
     "default": "3.0",
     "note": "full sets in circulation"
    },
    {
     "name": "note",
     "type": "text"
    }
   ]
  },
  "asset": {
   "layer": "maintenance",
   "description": "Things that break. An asset can live in a room (PTAC unit in 412), in a zone (Floor 4 ice machine), or in neither (the boiler).",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "room_id",
     "type": "uuid",
     "references": "room"
    },
    {
     "name": "floor_zone_id",
     "type": "uuid",
     "references": "floor_zone"
    },
    {
     "name": "name",
     "type": "text",
     "required": true
    },
    {
     "name": "category",
     "type": "text",
     "required": true,
     "enum": [
      "hvac",
      "plumbing",
      "electrical",
      "appliance",
      "furniture",
      "fixture",
      "elevator",
      "life_safety",
      "laundry",
      "kitchen",
      "other"
     ],
     "default": "other"
    },
    {
     "name": "manufacturer",
     "type": "text"
    },
    {
     "name": "model",
     "type": "text"
    },
    {
     "name": "serial_number",
     "type": "text"
    },
    {
     "name": "installed_on",
     "type": "date"
    },
    {
     "name": "warranty_expires_on",
     "type": "date"
    },
    {
     "name": "status",
     "type": "text",
     "required": true,
     "enum": [
      "in_service",
      "out_of_service",
      "retired"
     ],
     "default": "in_service"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "work_order": {
   "layer": "maintenance",
   "description": "The maintenance unit of work. source records where it came from, because 'how much of engineering's day is reactive vs preventive' is a question worth answering.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "room_id",
     "type": "uuid",
     "references": "room"
    },
    {
     "name": "asset_id",
     "type": "uuid",
     "references": "asset"
    },
    {
     "name": "reported_by",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "assigned_to",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "source",
     "type": "text",
     "required": true,
     "enum": [
      "inspection_finding",
      "hk_task",
      "guest_report",
      "pm_schedule",
      "discrepancy",
      "ad_hoc"
     ],
     "default": "ad_hoc"
    },
    {
     "name": "priority",
     "type": "text",
     "required": true,
     "enum": [
      "urgent",
      "high",
      "routine",
      "low"
     ],
     "default": "routine"
    },
    {
     "name": "status",
     "type": "text",
     "required": true,
     "enum": [
      "open",
      "assigned",
      "in_progress",
      "on_hold",
      "completed",
      "verified",
      "cancelled"
     ],
     "default": "open"
    },
    {
     "name": "title",
     "type": "text",
     "required": true
    },
    {
     "name": "description",
     "type": "text"
    },
    {
     "name": "due_at",
     "type": "timestamptz"
    },
    {
     "name": "completed_at",
     "type": "timestamptz"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    },
    {
     "name": "updated_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "pm_schedule": {
   "layer": "maintenance",
   "description": "Preventive maintenance cycles. A scheduler walks this table and creates work_order rows with source = 'pm_schedule'. Room PM cycles are the most common planned reason a room goes OOO.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "asset_id",
     "type": "uuid",
     "references": "asset"
    },
    {
     "name": "room_type_id",
     "type": "uuid",
     "references": "room_type",
     "note": "for per-room cycles, e.g. quarterly room PM"
    },
    {
     "name": "name",
     "type": "text",
     "required": true,
     "note": "'Quarterly PTAC filter change'"
    },
    {
     "name": "description",
     "type": "text"
    },
    {
     "name": "frequency_days",
     "type": "int",
     "required": true,
     "note": "90 for quarterly, 365 for annual"
    },
    {
     "name": "takes_room_ooo",
     "type": "boolean",
     "required": true,
     "default": "false",
     "note": "whether this cycle pulls the room from inventory"
    },
    {
     "name": "last_completed_on",
     "type": "date"
    },
    {
     "name": "next_due_on",
     "type": "date"
    },
    {
     "name": "active",
     "type": "boolean",
     "required": true,
     "default": "true"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "incident": {
   "layer": "incidents",
   "description": "Anything that needs a record. Linking to stay (not just room) is what lets a damage incident carry the 'who was in the room' answer with it.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "room_id",
     "type": "uuid",
     "references": "room"
    },
    {
     "name": "stay_id",
     "type": "uuid",
     "references": "stay"
    },
    {
     "name": "occurred_at",
     "type": "timestamptz",
     "required": true
    },
    {
     "name": "category",
     "type": "text",
     "required": true,
     "enum": [
      "guest_injury",
      "staff_injury",
      "property_damage",
      "security",
      "theft",
      "smoking",
      "biohazard",
      "noise",
      "other"
     ]
    },
    {
     "name": "severity",
     "type": "text",
     "required": true,
     "enum": [
      "minor",
      "major",
      "critical"
     ],
     "default": "minor"
    },
    {
     "name": "description",
     "type": "text",
     "required": true
    },
    {
     "name": "reported_by",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "status",
     "type": "text",
     "required": true,
     "enum": [
      "open",
      "under_review",
      "resolved",
      "closed"
     ],
     "default": "open"
    },
    {
     "name": "resolution_note",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  },
  "lost_found_item": {
   "layer": "incidents",
   "description": "Chain of custody for guest property. Status is a lifecycle, not a flag.",
   "columns": [
    {
     "name": "id",
     "type": "uuid",
     "primary_key": true
    },
    {
     "name": "property_id",
     "type": "uuid",
     "required": true,
     "references": "property"
    },
    {
     "name": "room_id",
     "type": "uuid",
     "references": "room"
    },
    {
     "name": "stay_id",
     "type": "uuid",
     "references": "stay",
     "note": "whose stay it likely belongs to"
    },
    {
     "name": "found_at",
     "type": "timestamptz",
     "required": true,
     "default": "now()"
    },
    {
     "name": "found_by",
     "type": "uuid",
     "references": "staff"
    },
    {
     "name": "description",
     "type": "text",
     "required": true
    },
    {
     "name": "storage_location",
     "type": "text",
     "note": "'HK office shelf B'"
    },
    {
     "name": "status",
     "type": "text",
     "required": true,
     "enum": [
      "logged",
      "claimed",
      "returned",
      "shipped",
      "donated",
      "disposed"
     ],
     "default": "logged"
    },
    {
     "name": "released_at",
     "type": "timestamptz"
    },
    {
     "name": "released_note",
     "type": "text"
    },
    {
     "name": "created_at",
     "type": "timestamptz",
     "default": "now()"
    }
   ]
  }
 }
}
