# 12. The portfolio property-list resolves descriptors through the landlord-override precedence

Date: 2026-07-10

## Status

Accepted

## Context

The portfolio property-list's **Property Type**, **Built Form**, **Year Built**
and **Main Fuel** columns and filters read raw `property` row columns (and, for
main fuel, the legacy `property_details_epc` table). Those columns are NULL for
**new-approach** properties (`property.updated_at >= 2026-06-01`, the EPC-graph
cutoff — see `epcSources.ts`), so for any new portfolio the columns render blank
and the filters match nothing. The already-migrated columns (current/lodged EPC,
floor area, CO₂, provenance, expiry) route through `epcSources` fragments and
work; these four were simply never migrated.

Separately, [ADR-0008](./0008-modelling-runs-filters-to-distributor.md)
established the resolution precedence for the modelling **Run filter**:
**landlord-override fact → EPC-derived value (new-approach graph, lodged over
predicted, RdSAP codes mapped; legacy property-row fallback) → Unknown**. It is
implemented in-app inline in `previewModellingRun` and mirrored by the
distributor; divergence between the two is defined as a bug.

## Decision

The portfolio list resolves Property Type, Built Form, Construction Age and Main
Fuel through the **same override precedence as the Run filter**, so the browse
surface agrees with what a modelling run would actually select. Concretely:

- **Shared fragments, one home for the rule.** `epcSources.ts` gains
  `overrideJoins` (LEFT JOINs of `property_overrides` aliased per component at
  `building_part = 0`) and `resolvedPropertyTypeSql` / `resolvedBuiltFormSql` /
  `resolvedConstructionBandSql` / `resolvedMainFuelSql`. Both the list query and
  `previewModellingRun` consume them; the inline COALESCE in
  `previewModellingRun` is refactored onto the shared fragments.
- **Never-null.** Each fragment is `COALESCE(override, epcSql, 'Unknown')`. Every
  property resolves to a concrete, filterable string; the **Unknown** filter chip
  matches "no resolvable value at all" — character-for-character the semantics
  CONTEXT.md already gives the Run filter's Unknown. The list columns render an
  explicit **Unknown** pill instead of a blank `—`.
- **Construction Age is band-native, not a year.** Override and EPC both store
  the RdSAP band **letter** (`A..M`, `Unknown`); the field resolves in letter
  space and displays the band **label** ("before 1900", "1900–1929", … "2023
  onwards"). A numeric year cannot represent an `Unknown` override and misreads a
  band as a precise year. The column is **renamed "Year Built" → "Construction
  Age"** and its filter options become the bands, not years; legacy raw years are
  bucketed into bands.
- **Main Fuel is override-only for new-approach.** The EPC graph exposes no clean
  main-fuel string (a documented `epcSources` gap → NULL), so for new-approach
  properties the override is the *only* source; unoverridden properties read
  **Unknown**. The override vocabulary (`"mains gas"`, `"LPG (bulk)"`, …) is
  folded into the existing collapsed filter labels (Mains Gas, LPG, …).
- **`tenure` is removed from the list**, not fixed. It has no override component,
  and its `epc_property.tenure` values don't cleanly reconcile with the filter's
  `dbValues`. It is dropped from the list column and filter only;
  `resolvePropertyMeta`/`resolvePropertyDescriptors` still resolve it for the
  per-property building passport, and `property.tenure` is retained.

## Alternatives considered

- **EPC-derived only, no overrides.** Smaller, and consistent with how the other
  already-migrated columns behave. Rejected: the list would then disagree with
  the modelling Run filter — e.g. show "Flat" for a property a landlord override
  makes (and modelling treats as) "Maisonette" — reintroducing exactly the
  browse-vs-model mismatch this work set out to remove.
- **Construction Age as a numeric year.** Familiar and a smaller diff. Rejected:
  cannot represent an `Unknown` override (collapses to `—`, breaking the
  never-null contract) and misrepresents a band as a point year.
- **Keep the precedence rule inline in `previewModellingRun` and duplicate it in
  the list.** Avoids touching a working feature. Rejected: it re-creates the
  two-homes divergence ADR-0008 explicitly warns against — now within the app.

## Consequences

- The count query (`getPropertiesCount`) must join `property_overrides` when any
  of these fields is filtered — `EPC_JOIN_FILTER_FIELDS` (or a sibling set) is
  extended. Joins are perf-sensitive (the count path already brushes Vercel's
  15s limit), so the joins stay conditional on an active filter.
- Every new-approach property without a main-fuel override shows **Unknown** — a
  lot of Unknown pills, but an honest signal of the EPC gap rather than a blank
  implying "not applicable".
- Filter option lists grow: Property Type gains **Park home** + **Unknown**;
  Built Form gains **Unknown** (keeping **Not Recorded** for the explicit
  override); Main Fuel folds the override vocab into existing labels.
- `previewModellingRun` and the portfolio list are now provably in sync — the
  shared fragment is the single reference implementation ADR-0008 asked for.
