feat(tariff): Tariff Lab — pure session-pricing simulator
Test rates "in time" (overnight windows, daily caps, overstay) in seconds against any tariff version, instead of waiting hours/days. No real ledger writes. - Extract priceSession() into @parking/shared: the grace/overstay wrapper over computeFee (unpaid -> entry..now; within-grace -> settled 0; grace-expired -> overstay, a fresh period from grace-expiry). PayStation.quote() now calls it so the booth and the lab can never diverge. - API (tariffs.ts, tariff:read, read-only): POST /api/tariff/simulate prices a hypothetical session (active/any version/inline structure) and returns the priceSession outcome + a 30m..3d duration curve (see where the daily cap flattens); GET /api/tariff/simulate/session/:identity prefills from a real ledger session. - UI TariffLab.tsx at Setup -> "Tariff Lab": version picker, entry/asOf times, optional payment+grace, category, and load-a-real-ticket. Admin-gated, available on-site (useful to quote a dispute). - 4 new priceSession unit tests incl. the ticket-1245791632490 overstay-not-zero regression (40 pass). i18n lab.* + nav.tariffLab (sq+en). Verified live via the UI. Wiki: tariff (priceSession + Tariff Lab as-built), log. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -21,6 +21,7 @@ import { StatusDot } from "./ui/StatusDot.js";
|
||||
import { BoothScreen } from "./BoothScreen.js";
|
||||
import { SetupWizard } from "./SetupWizard.js";
|
||||
import { TariffComposer } from "./TariffComposer.js";
|
||||
import { TariffLab } from "./TariffLab.js";
|
||||
import { SubscriptionManager } from "./SubscriptionManager.js";
|
||||
import { ShiftControl } from "./ShiftControl.js";
|
||||
import { SiteSettings } from "./SiteSettings.js";
|
||||
@@ -80,6 +81,7 @@ function SetupLayout() {
|
||||
<nav className="mb-4 flex flex-wrap items-center gap-1 border-b border-term-border">
|
||||
{show("site:update") && <SetupTab to="/setup" label={t("nav.devices")} exact />}
|
||||
{show("tariff:read") && <SetupTab to="/setup/tariff" label={t("nav.tariff")} />}
|
||||
{show("tariff:read") && <SetupTab to="/setup/tariff-lab" label={t("nav.tariffLab")} />}
|
||||
{show("subscription:read") && <SetupTab to="/setup/subscriptions" label={t("nav.subscriptions")} />}
|
||||
{show("site:read") && <SetupTab to="/setup/site" label={t("nav.site")} />}
|
||||
{show("user:read") && <SetupTab to="/setup/users" label={t("nav.users")} />}
|
||||
@@ -395,6 +397,12 @@ const tariffRoute = createRoute({
|
||||
beforeLoad: ({ context }) => requirePerm("tariff:read")(context),
|
||||
component: () => <TariffComposer />,
|
||||
});
|
||||
const tariffLabRoute = createRoute({
|
||||
getParentRoute: () => setupRoute,
|
||||
path: "tariff-lab",
|
||||
beforeLoad: ({ context }) => requirePerm("tariff:read")(context),
|
||||
component: () => <TariffLab />,
|
||||
});
|
||||
const subscriptionsRoute = createRoute({
|
||||
getParentRoute: () => setupRoute,
|
||||
path: "subscriptions",
|
||||
@@ -456,6 +464,7 @@ const routeTree = rootRoute.addChildren([
|
||||
setupRoute.addChildren([
|
||||
setupDevicesRoute,
|
||||
tariffRoute,
|
||||
tariffLabRoute,
|
||||
subscriptionsRoute,
|
||||
siteRoute,
|
||||
usersRoute,
|
||||
|
||||
Reference in New Issue
Block a user