{
 "$schema": "https://rapideyeinspections.com/blog/rental-property-data-model/schema.json",
 "name": "Long-Term Rental Data Model",
 "version": "0.1",
 "published": "2026-08-01",
 "publisher": "RapidEye Research",
 "canonical": "https://rapideyeinspections.com/blog/rental-property-data-model/",
 "companion_models": {
  "hotel": "https://rapideyeinspections.com/blog/hotel-operations-data-model/",
  "str": "https://rapideyeinspections.com/blog/str-operations-data-model/"
 },
 "formats": {
  "sql": "https://rapideyeinspections.com/blog/rental-property-data-model/schema.sql",
  "markdown": "https://rapideyeinspections.com/blog/rental-property-data-model/schema.md"
 },
 "license": "Free to use, adapt, and build on, with or without attribution.",
 "engine": "PostgreSQL 14+ (executed and idempotency-tested before publication)",
 "design_principles": [
  "The deposit clock is law, and it varies by state: return deadlines and their statutes live in the seeded state_deposit_rule table as data, not as constants in application code. California gives 21 days (Civil Code 1950.5), Texas 30 (Property Code 92.103), New York 14 (General Obligations Law 7-108).",
  "A deduction is only as strong as its documentation: deposit_deduction.inspection_item_id is NOT NULL, so an undocumented deduction cannot be inserted. Unpaid rent settles through the rent ledger (entry_type deposit_applied), never as a condition deduction.",
  "Move-in and move-out are the same checklist run twice: inspection items are keyed (room_label, item) so the move-out row joins to its move-in twin, and the move-out inspection carries baseline_inspection_id. Damage is a delta between two documented states.",
  "Turn status is a projection of milestone dates, never a typed field: unit_turn.status is a generated column, and the legal lifecycle edges are seeded in unit_turn_transition for one-lookup validation.",
  "The rent ledger is append-only: corrections are adjustment rows, never updates; the balance of a lease is SUM(amount).",
  "Evidence beats adjectives: an inspection item rated damaged or missing with zero photos is rejected by CHECK."
 ],
 "turn_state_machine": {
  "states": {
   "notice_given": "Notice received; tenant still in possession. Every turn starts here.",
   "move_out_inspection": "Tenant vacated; the move-out inspection runs against the move-in baseline.",
   "make_ready": "Findings split into deposit deductions and the make-ready scope; work in progress.",
   "market_ready": "Punch list complete, final walk passed; unit is showable and leasable.",
   "leased": "New lease signed and unit handed over; the turn closes."
  },
  "derivation": "unit_turn.status is GENERATED from milestone columns: new_lease_id set means leased; else market_ready_on set means market_ready; else make_ready_started_on set means make_ready; else moved_out_on set means move_out_inspection; else notice_given.",
  "transitions": [
   {
    "from": "notice_given",
    "to": "move_out_inspection",
    "trigger": "tenant vacated and returned keys; the move-out inspection runs against the move-in baseline"
   },
   {
    "from": "move_out_inspection",
    "to": "make_ready",
    "trigger": "move-out inspection completed; findings split into deposit deductions and the make-ready scope"
   },
   {
    "from": "make_ready",
    "to": "market_ready",
    "trigger": "make-ready tasks complete and the final walk passed"
   },
   {
    "from": "market_ready",
    "to": "make_ready",
    "trigger": "a showing or pre-move-in walk surfaced new work; the unit goes back into make-ready"
   },
   {
    "from": "market_ready",
    "to": "leased",
    "trigger": "new lease signed and the unit handed over; the turn closes"
   }
  ]
 },
 "seeded_reference_data": {
  "state_deposit_rule": {
   "note": "Eight states seeded, each verified against the named statute on 2026-08-01. return_deadline_days is the itemized-statement/refund deadline; claim_notice_deadline_days is for two-clock states like Florida.",
   "rows": [
    {
     "state_code": "CA",
     "return_deadline_days": 21,
     "claim_notice_deadline_days": null,
     "deposit_cap": "One month's rent; small landlords meeting the statute's criteria may collect up to two months.",
     "statute": "Cal. Civ. Code 1950.5"
    },
    {
     "state_code": "TX",
     "return_deadline_days": 30,
     "claim_notice_deadline_days": null,
     "deposit_cap": "No statutory cap.",
     "statute": "Tex. Prop. Code 92.103"
    },
    {
     "state_code": "NY",
     "return_deadline_days": 14,
     "claim_notice_deadline_days": null,
     "deposit_cap": "One month's rent.",
     "statute": "N.Y. Gen. Oblig. Law 7-108"
    },
    {
     "state_code": "FL",
     "return_deadline_days": 15,
     "claim_notice_deadline_days": 30,
     "deposit_cap": "No statutory cap.",
     "statute": "Fla. Stat. 83.49(3)"
    },
    {
     "state_code": "WA",
     "return_deadline_days": 30,
     "claim_notice_deadline_days": null,
     "deposit_cap": "No general statutory cap.",
     "statute": "RCW 59.18.280"
    },
    {
     "state_code": "AZ",
     "return_deadline_days": 14,
     "claim_notice_deadline_days": null,
     "deposit_cap": "One and one-half months' rent.",
     "statute": "A.R.S. 33-1321"
    },
    {
     "state_code": "MA",
     "return_deadline_days": 30,
     "claim_notice_deadline_days": null,
     "deposit_cap": "First month's rent.",
     "statute": "M.G.L. c. 186, s. 15B"
    },
    {
     "state_code": "CO",
     "return_deadline_days": 30,
     "claim_notice_deadline_days": null,
     "deposit_cap": "No general statutory cap.",
     "statute": "C.R.S. 38-12-103"
    }
   ]
  }
 },
 "layers": [
  {
   "layer": "Portfolio",
   "description": "What you manage, and for whom. The unit is the atom everything operational references; a single-family home is a property with exactly one unit.",
   "tables": [
    {
     "name": "property_owner",
     "description": "The owner of record. Third-party managers answer to one: statements, disbursements, and turn costs roll up here.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "name",
       "type": "text",
       "required": true
      },
      {
       "name": "owner_type",
       "type": "text",
       "required": true,
       "default": "'individual'::text"
      },
      {
       "name": "email",
       "type": "text"
      },
      {
       "name": "phone",
       "type": "text"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((owner_type = ANY (ARRAY['individual'::text, 'llc'::text, 'trust'::text, 'corporation'::text, 'partnership'::text, 'other'::text])))"
     ]
    },
    {
     "name": "property",
     "description": "A building or home. state_code is the join key into state_deposit_rule, where the deposit-return clock for every lease at this property lives.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "property_owner_id",
       "type": "uuid",
       "references": "property_owner.id"
      },
      {
       "name": "name",
       "type": "text",
       "required": true
      },
      {
       "name": "address_line1",
       "type": "text",
       "required": true
      },
      {
       "name": "address_line2",
       "type": "text"
      },
      {
       "name": "city",
       "type": "text"
      },
      {
       "name": "state_code",
       "type": "text",
       "required": true
      },
      {
       "name": "postal_code",
       "type": "text"
      },
      {
       "name": "property_type",
       "type": "text",
       "required": true,
       "default": "'single_family'::text"
      },
      {
       "name": "year_built",
       "type": "integer"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      },
      {
       "name": "updated_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((property_type = ANY (ARRAY['single_family'::text, 'condo'::text, 'townhome'::text, 'duplex_to_fourplex'::text, 'small_multifamily'::text, 'large_multifamily'::text, 'mixed_use'::text])))",
      "CHECK ((char_length(state_code) = 2))"
     ]
    },
    {
     "name": "unit",
     "description": "The rentable unit, the atom of the model. Leases, turns, inspections, and maintenance all key off it.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "property_id",
       "type": "uuid",
       "required": true,
       "references": "property.id"
      },
      {
       "name": "unit_number",
       "type": "text",
       "required": true,
       "default": "'MAIN'::text"
      },
      {
       "name": "bedrooms",
       "type": "numeric(3,1)"
      },
      {
       "name": "bathrooms",
       "type": "numeric(3,1)"
      },
      {
       "name": "square_feet",
       "type": "integer"
      },
      {
       "name": "market_rent",
       "type": "numeric(10,2)"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      },
      {
       "name": "updated_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "unique_constraints": [
      "UNIQUE (property_id, unit_number)"
     ]
    },
    {
     "name": "appliance",
     "description": "Appliances get identity because they get inspected, break, and carry warranties. Inspection items and work orders can name the exact appliance.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "unit_id",
       "type": "uuid",
       "required": true,
       "references": "unit.id"
      },
      {
       "name": "category",
       "type": "text",
       "required": true
      },
      {
       "name": "brand",
       "type": "text"
      },
      {
       "name": "model",
       "type": "text"
      },
      {
       "name": "serial_number",
       "type": "text"
      },
      {
       "name": "installed_on",
       "type": "date"
      },
      {
       "name": "warranty_expires_on",
       "type": "date"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((category = ANY (ARRAY['refrigerator'::text, 'range_oven'::text, 'dishwasher'::text, 'microwave'::text, 'washer'::text, 'dryer'::text, 'water_heater'::text, 'hvac'::text, 'garbage_disposal'::text, 'other'::text])))"
     ]
    }
   ]
  },
  {
   "layer": "People",
   "description": "Who works the portfolio and who wants in. Tenants carry minimal PII on purpose; applications are their own lifecycle with the decision and reason preserved.",
   "tables": [
    {
     "name": "staff",
     "description": "The people who run the portfolio: managers, leasing agents, techs, inspectors.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "full_name",
       "type": "text",
       "required": true
      },
      {
       "name": "role",
       "type": "text",
       "required": true
      },
      {
       "name": "email",
       "type": "text"
      },
      {
       "name": "active",
       "type": "boolean",
       "required": true,
       "default": "true"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((role = ANY (ARRAY['property_manager'::text, 'leasing_agent'::text, 'maintenance_tech'::text, 'inspector'::text, 'accountant'::text, 'other'::text])))"
     ]
    },
    {
     "name": "vendor",
     "description": "Outside trades. Tracks certificate-of-insurance expiry and whether a W-9 is on file.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "name",
       "type": "text",
       "required": true
      },
      {
       "name": "trade",
       "type": "text",
       "required": true,
       "default": "'general'::text"
      },
      {
       "name": "email",
       "type": "text"
      },
      {
       "name": "phone",
       "type": "text"
      },
      {
       "name": "insurance_expires_on",
       "type": "date"
      },
      {
       "name": "w9_on_file",
       "type": "boolean",
       "required": true,
       "default": "false"
      },
      {
       "name": "active",
       "type": "boolean",
       "required": true,
       "default": "true"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((trade = ANY (ARRAY['cleaning'::text, 'painting'::text, 'flooring'::text, 'plumbing'::text, 'electrical'::text, 'hvac'::text, 'appliance'::text, 'landscaping'::text, 'locksmith'::text, 'pest_control'::text, 'roofing'::text, 'general'::text, 'other'::text])))"
     ]
    },
    {
     "name": "tenant",
     "description": "A person who rents. Minimal PII on purpose; screening data stays in the screening vendor’s system.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "full_name",
       "type": "text",
       "required": true
      },
      {
       "name": "email",
       "type": "text"
      },
      {
       "name": "phone",
       "type": "text"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ]
    },
    {
     "name": "rental_application",
     "description": "The application lifecycle, separate from tenant. Keeps the decision, the timing, and the denial reason; lease_id set on conversion.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "unit_id",
       "type": "uuid",
       "required": true,
       "references": "unit.id"
      },
      {
       "name": "applicant_name",
       "type": "text",
       "required": true
      },
      {
       "name": "email",
       "type": "text"
      },
      {
       "name": "phone",
       "type": "text"
      },
      {
       "name": "desired_move_in",
       "type": "date"
      },
      {
       "name": "monthly_income",
       "type": "numeric(10,2)"
      },
      {
       "name": "status",
       "type": "text",
       "required": true,
       "default": "'received'::text"
      },
      {
       "name": "screening_report_at",
       "type": "timestamptz"
      },
      {
       "name": "income_verified",
       "type": "boolean",
       "required": true,
       "default": "false"
      },
      {
       "name": "denial_reason",
       "type": "text"
      },
      {
       "name": "decided_at",
       "type": "timestamptz"
      },
      {
       "name": "lease_id",
       "type": "uuid",
       "references": "lease.id"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((status = ANY (ARRAY['received'::text, 'screening'::text, 'approved'::text, 'denied'::text, 'withdrawn'::text, 'converted'::text])))"
     ]
    }
   ]
  },
  {
   "layer": "Leases and money",
   "description": "The tenancy and its append-only ledger. Renewals are new lease rows chained by previous_lease_id; ledger corrections are adjustment rows, never updates; notices are dated legal facts.",
   "tables": [
    {
     "name": "lease",
     "description": "The tenancy. A renewal is a new row pointing at its predecessor via previous_lease_id, never an edit.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "unit_id",
       "type": "uuid",
       "required": true,
       "references": "unit.id"
      },
      {
       "name": "previous_lease_id",
       "type": "uuid",
       "references": "lease.id"
      },
      {
       "name": "lease_type",
       "type": "text",
       "required": true,
       "default": "'fixed_term'::text"
      },
      {
       "name": "start_date",
       "type": "date",
       "required": true
      },
      {
       "name": "end_date",
       "type": "date"
      },
      {
       "name": "rent_amount",
       "type": "numeric(10,2)",
       "required": true
      },
      {
       "name": "rent_due_day",
       "type": "integer",
       "required": true,
       "default": "1"
      },
      {
       "name": "late_fee_grace_days",
       "type": "integer"
      },
      {
       "name": "late_fee_amount",
       "type": "numeric(10,2)"
      },
      {
       "name": "status",
       "type": "text",
       "required": true,
       "default": "'draft'::text"
      },
      {
       "name": "signed_at",
       "type": "timestamptz"
      },
      {
       "name": "moved_in_on",
       "type": "date"
      },
      {
       "name": "moved_out_on",
       "type": "date"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      },
      {
       "name": "updated_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK (((rent_due_day >= 1) AND (rent_due_day <= 28)))",
      "CHECK ((lease_type = ANY (ARRAY['fixed_term'::text, 'month_to_month'::text])))",
      "CHECK ((rent_amount > (0)::numeric))",
      "CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'ended'::text])))",
      "CHECK (((end_date IS NULL) OR (end_date > start_date)))",
      "CHECK (((lease_type = 'month_to_month'::text) OR (end_date IS NOT NULL)))"
     ]
    },
    {
     "name": "lease_tenant",
     "description": "Many-to-many people-on-lease with roles: primary, co-tenant, occupant, guarantor.",
     "primary_key": "PRIMARY KEY (lease_id, tenant_id)",
     "columns": [
      {
       "name": "lease_id",
       "type": "uuid",
       "required": true,
       "references": "lease.id"
      },
      {
       "name": "tenant_id",
       "type": "uuid",
       "required": true,
       "references": "tenant.id"
      },
      {
       "name": "role",
       "type": "text",
       "required": true,
       "default": "'primary'::text"
      }
     ],
     "check_constraints": [
      "CHECK ((role = ANY (ARRAY['primary'::text, 'co_tenant'::text, 'occupant'::text, 'guarantor'::text])))"
     ]
    },
    {
     "name": "rent_ledger_entry",
     "description": "Append-only money history for a lease. Charges positive, money in negative, enforced by CHECK; balance is SUM(amount). deposit_applied is how unpaid rent meets the deposit.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "lease_id",
       "type": "uuid",
       "required": true,
       "references": "lease.id"
      },
      {
       "name": "entry_type",
       "type": "text",
       "required": true
      },
      {
       "name": "amount",
       "type": "numeric(10,2)",
       "required": true
      },
      {
       "name": "effective_on",
       "type": "date",
       "required": true
      },
      {
       "name": "method",
       "type": "text"
      },
      {
       "name": "memo",
       "type": "text"
      },
      {
       "name": "created_by",
       "type": "uuid",
       "references": "staff.id"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((amount <> (0)::numeric))",
      "CHECK ((entry_type = ANY (ARRAY['rent_charge'::text, 'other_charge'::text, 'late_fee'::text, 'payment'::text, 'credit'::text, 'deposit_applied'::text, 'adjustment'::text])))",
      "CHECK ((method = ANY (ARRAY['ach'::text, 'check'::text, 'card'::text, 'cash'::text, 'money_order'::text, 'other'::text])))",
      "CHECK ((((entry_type = ANY (ARRAY['rent_charge'::text, 'other_charge'::text, 'late_fee'::text])) AND (amount > (0)::numeric)) OR ((entry_type = ANY (ARRAY['payment'::text, 'credit'::text, 'deposit_applied'::text])) AND (amount < (0)::numeric)) OR (entry_type = 'adjustment'::text)))"
     ]
    },
    {
     "name": "notice",
     "description": "Dated legal facts: notice to vacate, non-renewal, violations, entry, rent increases. served_on starts clocks.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "lease_id",
       "type": "uuid",
       "required": true,
       "references": "lease.id"
      },
      {
       "name": "notice_type",
       "type": "text",
       "required": true
      },
      {
       "name": "served_on",
       "type": "date",
       "required": true
      },
      {
       "name": "effective_on",
       "type": "date"
      },
      {
       "name": "delivery_method",
       "type": "text"
      },
      {
       "name": "document_uri",
       "type": "text"
      },
      {
       "name": "note",
       "type": "text"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((delivery_method = ANY (ARRAY['hand'::text, 'mail'::text, 'certified_mail'::text, 'email'::text, 'posting'::text])))",
      "CHECK (((effective_on IS NULL) OR (effective_on >= served_on)))",
      "CHECK ((notice_type = ANY (ARRAY['tenant_notice_to_vacate'::text, 'landlord_non_renewal'::text, 'lease_violation'::text, 'entry_notice'::text, 'rent_increase'::text, 'termination'::text])))"
     ]
    }
   ]
  },
  {
   "layer": "Inspections",
   "description": "The same checklist, run twice. A move-out inspection points at its move-in baseline; items are keyed (room_label, item) so the two runs join row for row; damaged or missing items require photos by CHECK.",
   "tables": [
    {
     "name": "inspection",
     "description": "A documented walkthrough: move_in, move_out, or routine. A move_out points at its move_in via baseline_inspection_id so damage is a delta between two documented states.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "unit_id",
       "type": "uuid",
       "required": true,
       "references": "unit.id"
      },
      {
       "name": "lease_id",
       "type": "uuid",
       "references": "lease.id"
      },
      {
       "name": "inspection_type",
       "type": "text",
       "required": true
      },
      {
       "name": "baseline_inspection_id",
       "type": "uuid",
       "references": "inspection.id"
      },
      {
       "name": "inspected_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      },
      {
       "name": "inspected_by",
       "type": "uuid",
       "references": "staff.id"
      },
      {
       "name": "tenant_present",
       "type": "boolean"
      },
      {
       "name": "summary",
       "type": "text"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK (((baseline_inspection_id IS NULL) OR (inspection_type = ANY (ARRAY['move_out'::text, 'routine'::text]))))",
      "CHECK ((inspection_type = ANY (ARRAY['move_in'::text, 'move_out'::text, 'routine'::text])))"
     ]
    },
    {
     "name": "inspection_item",
     "description": "One checklist line: room, item, condition, evidence. UNIQUE (inspection_id, room_label, item) is the pairing mechanism; damaged or missing requires photo_count > 0.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "inspection_id",
       "type": "uuid",
       "required": true,
       "references": "inspection.id"
      },
      {
       "name": "room_label",
       "type": "text",
       "required": true
      },
      {
       "name": "item",
       "type": "text",
       "required": true
      },
      {
       "name": "condition_rating",
       "type": "text",
       "required": true
      },
      {
       "name": "notes",
       "type": "text"
      },
      {
       "name": "photo_count",
       "type": "integer",
       "required": true,
       "default": "0"
      },
      {
       "name": "appliance_id",
       "type": "uuid",
       "references": "appliance.id"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "unique_constraints": [
      "UNIQUE (inspection_id, room_label, item)"
     ],
     "check_constraints": [
      "CHECK ((condition_rating = ANY (ARRAY['new'::text, 'good'::text, 'fair'::text, 'worn'::text, 'damaged'::text, 'missing'::text])))",
      "CHECK ((photo_count >= 0))",
      "CHECK (((condition_rating <> ALL (ARRAY['damaged'::text, 'missing'::text])) OR (photo_count > 0)))"
     ]
    }
   ]
  },
  {
   "layer": "The deposit",
   "description": "A clock, a ledger, and documentation. Statutory return deadlines are seeded data in state_deposit_rule; every deduction must reference the inspection_item that documents it; unpaid rent settles in the rent ledger, never as a deduction.",
   "tables": [
    {
     "name": "state_deposit_rule",
     "description": "The deposit-return clock as seeded, verified data: deadline days, cap, documentation requirements, and the statute, per state.",
     "primary_key": "PRIMARY KEY (state_code)",
     "columns": [
      {
       "name": "state_code",
       "type": "text",
       "required": true
      },
      {
       "name": "return_deadline_days",
       "type": "integer",
       "required": true
      },
      {
       "name": "claim_notice_deadline_days",
       "type": "integer"
      },
      {
       "name": "deadline_note",
       "type": "text"
      },
      {
       "name": "deposit_cap",
       "type": "text"
      },
      {
       "name": "documentation_note",
       "type": "text"
      },
      {
       "name": "statute",
       "type": "text",
       "required": true
      },
      {
       "name": "verified_on",
       "type": "date",
       "required": true
      }
     ],
     "check_constraints": [
      "CHECK ((char_length(state_code) = 2))"
     ]
    },
    {
     "name": "security_deposit",
     "description": "One deposit per lease, enforced UNIQUE. status is generated from the return facts; disposition_sent_on is what the statutory clock judges.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "lease_id",
       "type": "uuid",
       "required": true,
       "references": "lease.id"
      },
      {
       "name": "amount",
       "type": "numeric(10,2)",
       "required": true
      },
      {
       "name": "collected_on",
       "type": "date"
      },
      {
       "name": "held_in",
       "type": "text"
      },
      {
       "name": "disposition_sent_on",
       "type": "date"
      },
      {
       "name": "returned_on",
       "type": "date"
      },
      {
       "name": "amount_returned",
       "type": "numeric(10,2)"
      },
      {
       "name": "status",
       "type": "text",
       "generated": true,
       "generation_expression": " CASE WHEN ((returned_on IS NULL) AND (disposition_sent_on IS NULL)) THEN 'held'::text WHEN ((amount_returned IS NULL) OR (amount_returned = (0)::numeric)) THEN 'fully_withheld'::text WHEN (amount_returned >= amount) THEN 'returned_in_full'::text ELSE 'partially_withheld'::text END"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "unique_constraints": [
      "UNIQUE (lease_id)"
     ],
     "check_constraints": [
      "CHECK ((amount >= (0)::numeric))",
      "CHECK (((returned_on IS NULL) OR (amount_returned IS NOT NULL)))",
      "CHECK (((amount_returned IS NULL) OR ((amount_returned >= (0)::numeric) AND (amount_returned <= amount))))"
     ]
    },
    {
     "name": "deposit_deduction",
     "description": "A charge against the deposit. inspection_item_id is NOT NULL by design: no documented condition finding, no deduction. Unpaid rent never appears here.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "security_deposit_id",
       "type": "uuid",
       "required": true,
       "references": "security_deposit.id"
      },
      {
       "name": "inspection_item_id",
       "type": "uuid",
       "required": true,
       "references": "inspection_item.id"
      },
      {
       "name": "category",
       "type": "text",
       "required": true
      },
      {
       "name": "amount",
       "type": "numeric(10,2)",
       "required": true
      },
      {
       "name": "description",
       "type": "text",
       "required": true
      },
      {
       "name": "vendor_id",
       "type": "uuid",
       "references": "vendor.id"
      },
      {
       "name": "receipt_uri",
       "type": "text"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((category = ANY (ARRAY['damage'::text, 'cleaning_beyond_normal'::text, 'missing_item'::text, 'unreturned_keys'::text, 'other'::text])))",
      "CHECK ((amount > (0)::numeric))"
     ]
    }
   ]
  },
  {
   "layer": "The turn",
   "description": "Notice to new lease as a state machine. unit_turn.status is a generated projection of milestone dates; the legal edges are seeded in unit_turn_transition; tasks carry their provenance back to the inspection finding that raised them.",
   "tables": [
    {
     "name": "unit_turn",
     "description": "The make-ready lifecycle from notice to new lease. status is a generated projection of milestone dates; CHECKs force the milestones to arrive in order.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "unit_id",
       "type": "uuid",
       "required": true,
       "references": "unit.id"
      },
      {
       "name": "vacating_lease_id",
       "type": "uuid",
       "required": true,
       "references": "lease.id"
      },
      {
       "name": "notice_id",
       "type": "uuid",
       "references": "notice.id"
      },
      {
       "name": "new_lease_id",
       "type": "uuid",
       "references": "lease.id"
      },
      {
       "name": "notice_received_on",
       "type": "date",
       "required": true
      },
      {
       "name": "expected_move_out_on",
       "type": "date"
      },
      {
       "name": "moved_out_on",
       "type": "date"
      },
      {
       "name": "move_out_inspection_id",
       "type": "uuid",
       "references": "inspection.id"
      },
      {
       "name": "make_ready_started_on",
       "type": "date"
      },
      {
       "name": "market_ready_on",
       "type": "date"
      },
      {
       "name": "status",
       "type": "text",
       "generated": true,
       "generation_expression": " CASE WHEN (new_lease_id IS NOT NULL) THEN 'leased'::text WHEN (market_ready_on IS NOT NULL) THEN 'market_ready'::text WHEN (make_ready_started_on IS NOT NULL) THEN 'make_ready'::text WHEN (moved_out_on IS NOT NULL) THEN 'move_out_inspection'::text ELSE 'notice_given'::text END"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      },
      {
       "name": "updated_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK (((make_ready_started_on IS NULL) OR (moved_out_on IS NOT NULL)))",
      "CHECK (((moved_out_on IS NULL) OR (moved_out_on >= notice_received_on)))",
      "CHECK (((market_ready_on IS NULL) OR (make_ready_started_on IS NOT NULL)))",
      "CHECK (((market_ready_on IS NULL) OR (make_ready_started_on IS NULL) OR (market_ready_on >= make_ready_started_on)))",
      "CHECK (((new_lease_id IS NULL) OR (market_ready_on IS NOT NULL)))"
     ]
    },
    {
     "name": "unit_turn_transition",
     "description": "The legal edges of the turn lifecycle, seeded, so an application or agent validates a proposed move with one lookup.",
     "primary_key": "PRIMARY KEY (from_status, to_status)",
     "columns": [
      {
       "name": "from_status",
       "type": "text",
       "required": true
      },
      {
       "name": "to_status",
       "type": "text",
       "required": true
      },
      {
       "name": "trigger_event",
       "type": "text",
       "required": true
      }
     ],
     "check_constraints": [
      "CHECK ((from_status = ANY (ARRAY['notice_given'::text, 'move_out_inspection'::text, 'make_ready'::text, 'market_ready'::text, 'leased'::text])))",
      "CHECK ((to_status = ANY (ARRAY['notice_given'::text, 'move_out_inspection'::text, 'make_ready'::text, 'market_ready'::text, 'leased'::text])))"
     ]
    },
    {
     "name": "unit_turn_task",
     "description": "Sequenced work within a turn. source plus inspection_item_id answer who pays: tenant-charged damage or owner-funded refresh.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "unit_turn_id",
       "type": "uuid",
       "required": true,
       "references": "unit_turn.id"
      },
      {
       "name": "seq",
       "type": "integer",
       "required": true
      },
      {
       "name": "task_type",
       "type": "text",
       "required": true
      },
      {
       "name": "title",
       "type": "text",
       "required": true
      },
      {
       "name": "source",
       "type": "text",
       "required": true,
       "default": "'standard_turn'::text"
      },
      {
       "name": "inspection_item_id",
       "type": "uuid",
       "references": "inspection_item.id"
      },
      {
       "name": "vendor_id",
       "type": "uuid",
       "references": "vendor.id"
      },
      {
       "name": "assigned_staff_id",
       "type": "uuid",
       "references": "staff.id"
      },
      {
       "name": "status",
       "type": "text",
       "required": true,
       "default": "'pending'::text"
      },
      {
       "name": "scheduled_for",
       "type": "date"
      },
      {
       "name": "completed_at",
       "type": "timestamptz"
      },
      {
       "name": "actual_cost",
       "type": "numeric(10,2)"
      },
      {
       "name": "note",
       "type": "text"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "unique_constraints": [
      "UNIQUE (unit_turn_id, seq)"
     ],
     "check_constraints": [
      "CHECK ((task_type = ANY (ARRAY['cleaning'::text, 'painting'::text, 'flooring'::text, 'repair'::text, 'appliance'::text, 'landscaping'::text, 'rekey'::text, 'punch_list'::text, 'final_walk'::text, 'other'::text])))",
      "CHECK ((source = ANY (ARRAY['move_out_inspection'::text, 'standard_turn'::text, 'ad_hoc'::text])))",
      "CHECK ((status = ANY (ARRAY['pending'::text, 'scheduled'::text, 'in_progress'::text, 'done'::text, 'verified'::text, 'skipped'::text])))"
     ]
    }
   ]
  },
  {
   "layer": "Maintenance",
   "description": "What breaks between turns. Requests are triage; work orders are dispatch; source records whether work was reactive, found by inspection, or part of a turn.",
   "tables": [
    {
     "name": "maintenance_request",
     "description": "The tenant-reported issue and its triage: priority, habitability flag, entry permission.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "unit_id",
       "type": "uuid",
       "required": true,
       "references": "unit.id"
      },
      {
       "name": "lease_id",
       "type": "uuid",
       "references": "lease.id"
      },
      {
       "name": "reported_by_tenant_id",
       "type": "uuid",
       "references": "tenant.id"
      },
      {
       "name": "channel",
       "type": "text",
       "required": true,
       "default": "'portal'::text"
      },
      {
       "name": "category",
       "type": "text",
       "required": true,
       "default": "'other'::text"
      },
      {
       "name": "priority",
       "type": "text",
       "required": true,
       "default": "'routine'::text"
      },
      {
       "name": "is_habitability",
       "type": "boolean",
       "required": true,
       "default": "false"
      },
      {
       "name": "entry_permission",
       "type": "boolean"
      },
      {
       "name": "description",
       "type": "text",
       "required": true
      },
      {
       "name": "status",
       "type": "text",
       "required": true,
       "default": "'open'::text"
      },
      {
       "name": "reported_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      },
      {
       "name": "completed_at",
       "type": "timestamptz"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((channel = ANY (ARRAY['portal'::text, 'phone'::text, 'email'::text, 'text'::text, 'inspection'::text, 'staff'::text])))",
      "CHECK ((priority = ANY (ARRAY['emergency'::text, 'urgent'::text, 'routine'::text])))",
      "CHECK ((status = ANY (ARRAY['open'::text, 'triaged'::text, 'scheduled'::text, 'in_progress'::text, 'completed'::text, 'verified'::text, 'cancelled'::text])))",
      "CHECK ((category = ANY (ARRAY['plumbing'::text, 'electrical'::text, 'hvac'::text, 'appliance'::text, 'pest'::text, 'structural'::text, 'safety'::text, 'cosmetic'::text, 'other'::text])))"
     ]
    },
    {
     "name": "work_order",
     "description": "Dispatched work with provenance: from a request, an inspection finding, a turn task, or preventive. Costs and invoice attach here.",
     "primary_key": "PRIMARY KEY (id)",
     "columns": [
      {
       "name": "id",
       "type": "uuid",
       "required": true,
       "default": "gen_random_uuid()"
      },
      {
       "name": "property_id",
       "type": "uuid",
       "required": true,
       "references": "property.id"
      },
      {
       "name": "unit_id",
       "type": "uuid",
       "references": "unit.id"
      },
      {
       "name": "source",
       "type": "text",
       "required": true,
       "default": "'ad_hoc'::text"
      },
      {
       "name": "maintenance_request_id",
       "type": "uuid",
       "references": "maintenance_request.id"
      },
      {
       "name": "inspection_item_id",
       "type": "uuid",
       "references": "inspection_item.id"
      },
      {
       "name": "unit_turn_task_id",
       "type": "uuid",
       "references": "unit_turn_task.id"
      },
      {
       "name": "appliance_id",
       "type": "uuid",
       "references": "appliance.id"
      },
      {
       "name": "vendor_id",
       "type": "uuid",
       "references": "vendor.id"
      },
      {
       "name": "assigned_staff_id",
       "type": "uuid",
       "references": "staff.id"
      },
      {
       "name": "priority",
       "type": "text",
       "required": true,
       "default": "'routine'::text"
      },
      {
       "name": "status",
       "type": "text",
       "required": true,
       "default": "'open'::text"
      },
      {
       "name": "title",
       "type": "text",
       "required": true
      },
      {
       "name": "description",
       "type": "text"
      },
      {
       "name": "estimate_amount",
       "type": "numeric(10,2)"
      },
      {
       "name": "invoice_amount",
       "type": "numeric(10,2)"
      },
      {
       "name": "invoice_uri",
       "type": "text"
      },
      {
       "name": "scheduled_for",
       "type": "date"
      },
      {
       "name": "completed_at",
       "type": "timestamptz"
      },
      {
       "name": "created_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      },
      {
       "name": "updated_at",
       "type": "timestamptz",
       "required": true,
       "default": "now()"
      }
     ],
     "check_constraints": [
      "CHECK ((source = ANY (ARRAY['maintenance_request'::text, 'routine_inspection'::text, 'move_out_inspection'::text, 'unit_turn'::text, 'preventive'::text, 'ad_hoc'::text])))",
      "CHECK ((priority = ANY (ARRAY['emergency'::text, 'urgent'::text, 'routine'::text, 'low'::text])))",
      "CHECK ((status = ANY (ARRAY['open'::text, 'assigned'::text, 'scheduled'::text, 'in_progress'::text, 'on_hold'::text, 'completed'::text, 'verified'::text, 'cancelled'::text])))"
     ]
    }
   ]
  }
 ],
 "table_count": 22
}
