fix(tariff): forbid backdated effectiveFrom — versioning was retroactive
Version selection is "latest tariff_version with effectiveFrom <= entry time", but the publish handler accepted ANY effectiveFrom (defaulting to now). So an admin could publish a version with a backdated effectiveFrom and silently reprice sessions that had already entered — the retroactive rewrite the versioning exists to prevent. Pricing itself was sound (quote resolves by entry time; payment records tariffVersionId, freezing completed sessions); the leak was the publish side only. Reject effectiveFrom earlier than now (60s skew tolerance); future-dated (scheduling a price change) stays allowed; bad ISO -> 400. Combined with entry-time selection this is structural: once a car has entered, no later publish can reprice it. Did not pin tariffVersionId onto vehicle_entry (not needed). Verified 5/5 via inject against a copy of the live DB. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -62,12 +62,37 @@ export async function tariffRoutes(app: FastifyInstance, db: Db): Promise<void>
|
||||
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<string, unknown>,
|
||||
createdBy: req.user?.username ?? null,
|
||||
|
||||
Reference in New Issue
Block a user