0118 — Internal transfers get their own bucket (disclosure, not a rate term)

Context

A live audit of Camellia's trending occupancy on 2026-07-28 reconciled every term of the ADR-0099 formula against the AppFolio Reports API (unit_directory, tenant_directory, rental_applications). Every number tied out: 120 units, 100 occupied, 5 pre-leased, 6 on notice, 2 approved units, 0 pending — (100 + 5 + 2 + 0 − 6) / 120 = 84.17%.

The audit surfaced something the numbers were hiding. Of the 6 units on notice, two are internal transfers — residents moving to another Camellia unit:

From (on notice) To (pre-leased) Residents
315 322 Lila Dummerly
120 208 Wren Fictor + Kester Mockridge

A transfer subtracts as a notice and adds back as a pre-leased unit, so the rate was never wrong — the two terms cancel. But "6 on notice" reads as six units of churn risk when only four residents are leaving the property, and "5 moving in" implies five new households when three are new and two are relocating.

Fede, 2026-07-28: "I'd prefer we count transfers in a separate bucket."

Decision

Add LeasingStats.transferUnits — in-flight transfers, counted as from→to unit PAIRS — and split the display buckets off it. noticesGiven and pendingLeases keep their existing meaning and their full counts.

movingIn = pendingLeases − transferUnits
leaving  = noticesGiven  − transferUnits
transfers = transferUnits          (rendered WITHOUT a sign)

Camellia today: 3 moving in · 4 leaving · 2 transferring, where it used to read 5 moving in · 6 notice.

The ADR-0099 formula is unchanged. trending-occupancy.ts is not touched and does not import any of this. A transfer contributes +1 to vacantLeased and −1 via noticeUnits; netting it out of both yields the identical rate, so re-terming the formula would buy nothing and add a fourth way for the surfaces to drift. Verified against prod: full terms 84.17%, split terms 84.17%.

A useful property falls out — the signed buckets now sum to the trending numerator exactly (occupied + movingIn + approved + pending − leaving), so the caption a PM reads reconciles to the percentage next to it.

Why the underlying counts stay whole

noticesGiven is the NAA NTV count. It drives the turnovers card and the maintenance page, and a transfer-out unit genuinely does turn over — the resident vacates it and it must be made ready. Netting transfers out of the stored count would understate real maintenance work. Same for available = vacant + onNotice in the leasing section: a transfer-out unit does become available. So transferUnits is a disclosure reported alongside, and the split happens at the display layer through one shared helper (splitTransferBuckets) that all three occupancy surfaces call.

Detection: unit pairs, person-or-name join

A transfer is a (fromUnit, toUnit) pair where a resident holds a live occupancy in fromUnit (status active/mtm, unit flagged NOTICE) and a future one in toUnit (status pending, unit flagged VACANT_RENTED), in the same property. Both sides are bounded by the unit statuses, so a pair can only ever name units genuinely inside noticesGiven / pendingLeases.

Two deliberate choices, each forced by live data:

The detector needs the active|mtm|pending occupancy view (getSignedOccupancyTenants); the active-only view holds no transfer-in side at all.

Asymmetric transfers stay plain notices

A resident who gave notice but whose destination unit is not yet pre-leased leaves an unfilled vacancy from the property's point of view. Only a matched pair is a transfer. Camellia has none of these today.

Consequences

Alternatives considered