feat(tariff): lab explains the sum — fee breakdown from the engine walk
"ALL 740 / 3h 2m" gave no derivation. explainFee in @parking/shared runs the EXACT computeFee walk with an optional trace collector — one code path, so Σ line items ≡ the amount by construction (golden V1 regression byte-identical; instrumentation changes no fee). Items: contiguous same-price increment runs (time window · N × unit · tier-card name), window-package occurrences, stepped day totals (top-tier repeat flagged), daily-cap clamps as NEGATIVE adjustments, entry grace. /api/tariff/simulate returns `breakdown` (null when settled); the lab's Outcome panel renders the lined table with a rounding note (raw min → billed min at the increment — answers "why does 3h 2m bill as 4h") and a total row. Works against active/historical versions and drafts alike, so a night-package draft can be verified line by line before publish. Largely delivers the wiki's open "composer price preview" item. 4 new engine tests pin the sum invariant + item shapes (97 shared green). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -3,6 +3,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import { and, desc, eq, isNull, ledgerEvents, siteConfig, tariffDrafts, tariffVersions, tariffs, type Db } from "@parking/db";
|
||||
import {
|
||||
computeFee,
|
||||
explainFee,
|
||||
isTariffV2,
|
||||
priceSession,
|
||||
validateTariffStructure,
|
||||
@@ -188,6 +189,12 @@ export async function tariffRoutes(app: FastifyInstance, db: Db): Promise<void>
|
||||
const payments = Array.isArray(b.payments) ? b.payments : [];
|
||||
const pricing = priceSession(b.enteredAt, b.asOf, structure, payments, b.category);
|
||||
|
||||
// HOW the amount is produced — the same engine walk with a trace collector
|
||||
// (Σ lines ≡ amountMinor by construction). Null when settled (nothing billed).
|
||||
const breakdown = pricing.withinGrace
|
||||
? null
|
||||
: explainFee(pricing.periodStart, b.asOf, structure, b.category);
|
||||
|
||||
// A duration curve from entry: handy to SEE where the cap flattens / windows shift.
|
||||
const SAMPLES_MIN = [30, 60, 120, 180, 360, 720, 1440, 2880, 4320];
|
||||
const enteredMs = Date.parse(b.enteredAt);
|
||||
@@ -196,7 +203,7 @@ export async function tariffRoutes(app: FastifyInstance, db: Db): Promise<void>
|
||||
amountMinor: computeFee(b.enteredAt, new Date(enteredMs + min * 60_000).toISOString(), structure!, b.category),
|
||||
}));
|
||||
|
||||
return { currency, pricing, curve, gracePeriodExitMin: structure.gracePeriodExitMin };
|
||||
return { currency, pricing, breakdown, curve, gracePeriodExitMin: structure.gracePeriodExitMin };
|
||||
});
|
||||
|
||||
// Prefill the lab from a REAL session: fold its ledger into entry + payments so the
|
||||
|
||||
Reference in New Issue
Block a user