# 0033. Occupancy overrides live on the storage, not in the rental table

- **Status:** Accepted
- **Date:** 2026-09-07

## Context

Availability was derived from the rentals and the open quote requests, and from nothing else. That is right
for anything the office records here — but at go-live, three hundred and twenty-six units are rented under
contracts that live in the previous system and were never entered into this one. Without them the site
offers occupied units for rent on day one.

The previous system tracks availability with the Availability Calendars module: one row per unit per day,
`2` free, `3` booked, `4` provisionally booked. Run-length encoding those days gives the bookings, and the
ones covering today are exactly the units that must not be offered.

The obvious move is to import them as `Rent` records. It is also the wrong one:

- **An `ACTIVE` rental generates payments.** `payments/generate-by-rents` walks
  `status = ACTIVE AND isPrepaid = false` daily, and `billingo/invoice` then issues real invoices for what it
  produced. Three hundred fabricated rentals would put three hundred invoices into Billingo.
- **The renter cannot be identified.** The calendar holds no customer. The previous system's Commerce orders
  do, but even with a three-week tolerance on the start date only 68 of the 327 current bookings can be
  matched to one — and a wrong match would put the wrong renter's name on a contract in an ERP.
- **There is nothing to undo.** As the office enters the real contracts, three hundred placeholder rentals
  would have to be found and deleted by hand.

## Decision

**Occupancy can be overridden on the storage unit itself.** `storage.availability` is one of `AUTO`, `FREE`,
`CONDITIONAL` or `OCCUPIED`, and `storage.availabilityUntil` bounds the override. `AUTO` is the default and
means what the application always meant: derive it from the rentals and the quote requests.

The effective state is resolved **on read** — an override whose end date has passed simply reads as `AUTO`,
with no write and nothing to clean up. `AvailabilityService` owns that resolution; see
[0034](0034-the-availability-rule-has-one-owner.md).

The override cannot contradict a contract, in either direction:

- `Storage::rules()` refuses a manual value on a unit that has a rental in a non-ending status whose end date
  is today or later, and the admin form offers only `AUTO` there;
- `Rent::afterSave()` resets the unit to `AUTO` when such a rental is saved.

`import/occupancy` seeds the overrides from the calendar. It is **seeding, not synchronisation**: it refuses
to run when overrides already exist unless `--reset` clears them first, because after go-live the office
maintains occupancy here and a second run would overwrite their work.

The bookings that do match a Commerce order are written to
`data/import/out/occupancy-orders.csv` as a worksheet. Nothing is imported from it.

## Alternatives

- **Import the bookings as rentals.** Rejected for the three reasons above. A fourth flag (`isLegacy`) plus a
  condition in the payment generator would have contained the billing risk, but not the fabricated customers
  or the manual cleanup.
- **`isPrepaid = true` on imported rentals.** Excluded from payment generation without a schema change, but
  the flag means something else in the admin and one click undoes the protection.
- **A separate legacy-occupancy table.** No fabricated rentals, but a fourth place the availability rule has
  to be taught about, and the office would have two screens showing occupancy.
- **Import the 68 matched customers automatically.** Rejected: the match is a heuristic over an interval
  boundary, and the cost of being wrong is a contract in the wrong name.

## Consequences

- **The `rent` table is untouched**, `payments/generate-by-rents` is unchanged, and no fabricated record can
  reach Billingo.
- **The office's workflow needs no extra step.** Entering a real contract clears the override by itself, so
  the overrides drain away as the carried-over rentals are recorded. The admin grid can be filtered on
  `availability` to see what is left.
- **`FREE` is an override too**, for a unit the derivation would show as taken. It cannot mask a contract,
  because a unit with a live rental accepts no override at all.
- **`CONDITIONAL` blocks the unit; an open quote request does not.** That asymmetry is deliberate — a quote
  request is a lead, an override is the office asserting a fact — and it is documented in
  `AvailabilityService`.
- **A stale override is invisible rather than wrong**: it stops applying on its end date without anything
  having to run. The cost is that the admin shows the raw setting, so the effective state and the stored
  value can differ on screen.
- The seeded `availabilityUntil` is the last day the previous system had blocked, not a contract term. It is
  honest about what it is — "blocked until" — and it is what the calendar knows.
