diff --git a/apps/server/src/routes/tariffs.ts b/apps/server/src/routes/tariffs.ts index 8b7b433..d43947e 100644 --- a/apps/server/src/routes/tariffs.ts +++ b/apps/server/src/routes/tariffs.ts @@ -62,12 +62,37 @@ export async function tariffRoutes(app: FastifyInstance, db: Db): Promise if (problems.length) { return reply.code(400).send({ error: "invalid tariff structure", problems }); } + + // effectiveFrom must NOT be in the past. A version is selected by + // "latest effectiveFrom <= entry time", so a backdated effectiveFrom would + // retroactively reprice already-entered sessions — exactly the immutability + // the versioning exists to prevent (wiki/concepts/tariff.md). So we forbid + // backdating: a new version applies only from publish (now) forward; a future + // effectiveFrom (scheduling a price change) is allowed. A small skew tolerance + // absorbs client/server clock drift + request round-trip. Once a car has + // entered, no later publish can reprice it (no effectiveFrom can predate it). + const now = Date.now(); + const SKEW_MS = 60_000; // 1 min: clock skew + round-trip slack + let effective = new Date().toISOString(); + if (effectiveFrom != null) { + const t = Date.parse(effectiveFrom); + if (Number.isNaN(t)) { + return reply.code(400).send({ error: "effectiveFrom must be a valid ISO-8601 timestamp" }); + } + if (t < now - SKEW_MS) { + return reply.code(400).send({ + error: "effectiveFrom cannot be in the past — backdating a tariff would retroactively reprice entered sessions", + }); + } + effective = new Date(t).toISOString(); + } + const tariffId = ensureSiteTariff(); const id = randomUUID(); const row = { id, tariffId, - effectiveFrom: effectiveFrom ?? new Date().toISOString(), + effectiveFrom: effective, currency, structure: structure as unknown as Record, createdBy: req.user?.username ?? null,