feat(tariff-lab): DB-backed draft tariffs + named published versions

Experimenting used to mean publishing — churning the immutable version
history and risking real tickets pricing against a half-baked card while
the admin iterated. The lab is now a true sandbox:

- tariff_drafts table (migration 0021): MUTABLE by design — the one
  exception to "editing publishes a version"; a draft prices nothing and
  signs nothing. Drafts are validated + tz-stamped on save exactly like a
  publish, so a saved draft always simulates and never fails at publish.
- CRUD under /api/tariff/drafts (list tariff:read, mutations
  tariff:update); publishing a draft goes through the normal immutable
  POST /api/tariff/versions path.
- Lab UI rebuilt: sidebar lists lab drafts AND the full published history
  (click any to price against it); main pane cut to pure entry/exit
  (ticket loader, payment, category inputs dropped); the composer form is
  extracted to TariffEditorForm.tsx and reused in a modal (new drafts
  prefill from the active card); per-draft Publish with confirm.
- tariff_versions.name (migration 0022): optional label stamped at
  publish — carried from the lab draft, or typed in the composer's new
  optional field — so history reads "Winter 2027", not UUID prefixes.
- Includes the composer UI + sq/en labels for the package mode (engine
  landed in d9e6c13) and the "Flat price / hour" relabel.

5 new server integration tests (RBAC, roundtrip, validation, tz-stamp +
simulate + publish w/ name); server suite 288 green.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-07-05 14:31:42 +02:00
parent 52a89bfa56
commit fd9885e9ec
14 changed files with 1457 additions and 782 deletions
+38
View File
@@ -2282,3 +2282,41 @@ ISO-8601 UTC + level names (pinoDbStream hardened to accept both encodings so th
silently break). Rotation: docker json-file caps in docker-compose.prod.yml resized from 10m×3
(≈30 MB!) to ≈2 months by volume (server 20m×30, vision 20m×10, proxy 10m×5; json-file rotates by
SIZE — time-based isn't a driver feature). app_logs retention default aligned 30→60 days.
## [2026-07-05] update | Window-package tariff mode (packageMinor) + honest flat labels
Tariff-lab verification of a 1,850 ALL bill exposed a field misread: the V2 card "flat price" is
PER INCREMENT (400/h), not per window — park-buzi's "night 400" card billed each night hour 400.
Built the missing concept on [[tariff-time-tiers]]: `packageMinor`, a whole-window package
("any presence in 20:00–07:00 = 400 total"). Operator decisions: per-occurrence repeat (two nights
= two charges), any-touch-pays-full, windowed-cards-only (base "price per day" = a 1-row up-to
table). Engine charges once per contiguous run of increments the card wins, tracked across
rolling-day segments (a night crossing the 24h boundary charges once). Validator: exclusive with
flat/blocks/steps, no per-card cap, defaultCard forbidden. Composer offers the mode on tier cards;
flat relabeled "Flat price / hour" (sq+en). Also flagged from the same session: windowed-card
dailyCapMinor is inert by design (only the base card's cap clamps a day) — park-buzi's weekend
card carries a dead 1000 cap. Tests: shared 93 green (6 new), server 283 green.
## [2026-07-05] update | Tariff Lab redesign: DB-backed drafts, sidebar, composer modal
The lab previously simulated only against PUBLISHED versions, so experimenting meant publishing —
churning the immutable history and risking real tickets pricing against a half-baked card while
the admin iterated (operator: "we risk taking tickets with a grossly wrong version"). Redesign on
[[tariff]] (Tariff Lab section): new mutable `tariff_drafts` table (migration 0021 — the one
deliberate exception to "editing publishes a version"; a draft prices/signs nothing, only the
normal publish path makes it real), drafts validated + tz-stamped on save exactly like a publish,
CRUD under /api/tariff/drafts (list tariff:read, mutations tariff:update). UI rebuilt: sidebar
lists active card + drafts (click to price against), main pane cut to pure entry/exit (ticket
loader, payment, category inputs dropped), composer form extracted to TariffEditorForm.tsx and
reused in a modal (new drafts prefill from the active card), per-draft Publish with confirm.
Simulation passes the draft's stored structure inline to the existing /api/tariff/simulate.
Tests: server 288 green (5 new: RBAC, roundtrip, validation, tz-stamp + simulate + publish flow).
## [2026-07-05] update | Published tariff versions get optional names + lab sidebar lists history
Follow-up to the lab redesign (same session): the sidebar now also lists the PUBLISHED versions
(active first, then the immutable history; click to price against by tariffVersionId), and
`tariff_versions` gained a nullable `name` (migration 0022) — stamped at publish, immutable like
the row. Publishing a lab draft carries the draft's name onto the version; the composer page grew
an optional version-name field (never prefilled — republishing a tweak under last season's name
would mislabel history). Details on [[tariff]] (Tariff Lab section).