docs(wiki): per-increment vs per-hour tariff gotcha + composer UX idea

priceMinorPerIncrement is per BILLING INCREMENT, not per hour. Documented
the effective-hourly formula (price x 60/incrementMin) as a callout after
recurring "Lab is wrong" confusion (weekend 3h=600 not 300 was correct),
and filed a per-hour-preview composer UX candidate under Open.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-20 15:44:12 +02:00
parent 9a1feeeb20
commit b0c9ba0f8c
+14 -1
View File
@@ -68,8 +68,18 @@ code. All amounts are **integer minor units** in the tariff's currency.
A card's body is **one of three mutually-exclusive shapes** — `flatMinor`, `blocks`, or `steps`: A card's body is **one of three mutually-exclusive shapes** — `flatMinor`, `blocks`, or `steps`:
1. **Hourly ladder (`blocks`)** — the model above: a **marginal per-increment** rate that the engine 1. **Hourly ladder (`blocks`)** — the model above: a **marginal per-increment** rate that the engine
*sums* across increments. "Each next hour costs X." Daily-cap and multi-day reset apply. *sums* across increments. "Each next **increment** costs X." Daily-cap and multi-day reset apply.
2. **Flat (`flatMinor`)** — one rate per increment (a one-block ladder). 2. **Flat (`flatMinor`)** — one rate per increment (a one-block ladder).
> **⚠ `priceMinorPerIncrement` is PER BILLING INCREMENT, not per hour.** The effective hourly rate is
> `price × (60 / incrementMin)`. So with `incrementMin: 30`, a block priced `100` charges **100 every
> half-hour = 200/hour** → a 3h stay costs `100 × 6 = 600`, not 300. The example below uses
> `incrementMin: 60`, where per-increment happens to equal per-hour — which hides the distinction.
> This has caused repeated "the Lab is wrong" confusion (2026-06-20); the engine was correct each
> time, the *rate was per 30-min increment*. To bill 100/hour at a 30-min increment, set the price to
> `50`; or set `incrementMin: 60`. The composer column is labelled "Price / increment" and the
> billing increment is a separate top-level field — see Open (a per-hour preview is a candidate UX
> fix).
3. **Stepped / "up-to" (`steps`)** — *added 2026-06-20.* A **total-by-duration** table the owner 3. **Stepped / "up-to" (`steps`)** — *added 2026-06-20.* A **total-by-duration** table the owner
enters verbatim — the opposite of marginal: each row is the **cumulative TOTAL** for a stay within enters verbatim — the opposite of marginal: each row is the **cumulative TOTAL** for a stay within
that tier. Needed because owners think in totals, and many real cards (flat-day, airport) are that tier. Needed because owners think in totals, and many real cards (flat-day, airport) are
@@ -308,5 +318,8 @@ Grounded in [[parksql2017-legacy-schema|the legacy schema]] + external research:
- The **actual rate cards** are owner-authored at runtime — nothing to confirm at build time; the - The **actual rate cards** are owner-authored at runtime — nothing to confirm at build time; the
composer UI + validation (sane blocks, non-negative, ordered `uptoMin`) is the work. composer UI + validation (sane blocks, non-negative, ordered `uptoMin`) is the work.
- **Blank-tariff policy** — free vs. gated until a rate card is published (operator policy). - **Blank-tariff policy** — free vs. gated until a rate card is published (operator policy).
- **Per-hour preview in the composer** (UX, candidate) — "Price / increment" is repeatedly misread as
per-hour (see the ⚠ note above). Showing the computed effective per-hour rate beside each ladder
price (`price × 60/incrementMin`), or a small live fee preview, would prevent it. No engine change.
- **In-progress version-boundary** — entry-version (decided) vs. pro-rate (revisit if needed). - **In-progress version-boundary** — entry-version (decided) vs. pro-rate (revisit if needed).
- **FX** — exchange-rate system, offline rate source, base currency ([[open-questions]]). - **FX** — exchange-rate system, offline rate source, base currency ([[open-questions]]).