# 0016. Comparison and offer lists live in the session

- **Status:** Accepted — the offer half is superseded by [0024](0024-quote-request-is-a-sidebar-not-a-cart.md)
- **Date:** 2025-11-16

## Context

Before requesting a quote, a visitor collects storage units: a comparison list to weigh options, and an offer
list of the ones they want quoted. Most visitors are not logged in, and most never submit anything.

## Decision

**Keep both lists in the session.** No database record until the quote request is actually submitted.

Both are backed by [`SessionListService`](../../services/SessionListService.php), with `ComparisonService` and
`ProposalRequestService` (the one in `services/`) on top. The routes are localized and idempotent:

```
{comparisonAdd}/<id>              {comparisonRemove}/<id>              {comparison}
{proposalAdd}/<id>/<from>/<duration>   {proposalRemove}/<id>           {proposal}
```

The offer list carries the term (`from`, `duration`) in the URL — chosen when the unit is added, not at
submission.

## Alternatives

- **A `cart` table keyed by session id.** Rows for every visitor who clicked once, a cleanup job, and no one
  reads them.
- **A cart tied to a user account.** Would require logging in before comparing, which is the opposite of what
  the funnel needs.
- **Browser storage (localStorage).** Survives the session, but the server would need the list anyway to
  render prices and availability, so it would have to be posted back on every page.

## Consequences

- **No database writes for browsing.** The funnel costs nothing until intent is real.
- **The lists do not survive a session expiry** (24 hours) and are invisible to the back office. A visitor
  who loses their session loses the selection, and there is nothing to look up or recover. This is the real
  cost, and it was accepted: an abandoned selection is not a lead.
- **The units are not reserved.** A quoted unit only starts holding occupancy once a `ProposalRequestItem`
  exists, and even then only as `pending`.
- Carrying the term in the URL made the add action self-contained and replaced an earlier calendar picker
  (`Replace proposal request calendar with from/duration dropdowns`).
- `ProposalRequestService` now exists twice — as this service and as the `models/` join record between a
  request and a `Service`. Same base name, different namespaces.
