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:
@@ -425,6 +425,54 @@ export function publishTariffVersion(body: {
|
||||
return apiFetch("/api/tariff/versions", { method: "POST", body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
// --- Tariff Lab (simulator) -----------------------------------------------
|
||||
|
||||
export interface SimPayment {
|
||||
paidAt: string;
|
||||
graceExitMin: number | null;
|
||||
}
|
||||
export interface SimSessionPricing {
|
||||
periodStart: string;
|
||||
amountMinor: number;
|
||||
overstay: boolean;
|
||||
withinGrace: boolean;
|
||||
graceExpiresAt: string | null;
|
||||
}
|
||||
export interface SimulateResult {
|
||||
currency: string | null;
|
||||
pricing: SimSessionPricing;
|
||||
curve: { minutes: number; amountMinor: number }[];
|
||||
gracePeriodExitMin: number;
|
||||
}
|
||||
export interface SimulateBody {
|
||||
enteredAt: string;
|
||||
asOf: string;
|
||||
payments?: SimPayment[];
|
||||
category?: string;
|
||||
tariffVersionId?: string;
|
||||
structure?: TariffStructure;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
/** Price a hypothetical session — pure, no ledger write. See Tariff Lab. */
|
||||
export function simulateTariff(body: SimulateBody): Promise<SimulateResult> {
|
||||
return apiFetch("/api/tariff/simulate", { method: "POST", body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
export interface SimSessionLoad {
|
||||
identity: string;
|
||||
enteredAt: string;
|
||||
exitedAt: string | null;
|
||||
payments: SimPayment[];
|
||||
category: string | null;
|
||||
tariffVersionId: string | null;
|
||||
}
|
||||
|
||||
/** Prefill the lab from a real ledger session. */
|
||||
export function loadSimSession(identity: string): Promise<SimSessionLoad> {
|
||||
return apiFetch(`/api/tariff/simulate/session/${encodeURIComponent(identity)}`);
|
||||
}
|
||||
|
||||
// --- Subscriptions --------------------------------------------------------
|
||||
|
||||
export interface SubscriptionCredential {
|
||||
|
||||
Reference in New Issue
Block a user