feat(tariff): complete the progressive ladder — require open-ended last block, hours-based composer
The stepped-block engine already does "first N hrs x X, next N hrs x Y, ...,
24h cap" (ordered blocks, per-block rate, rolling-24h cap). No new axis; this
completes the model and removes its footgun.
- validateTariffStructure (shared) now REQUIRES the last block to be open-ended
(uptoMin: null). A bounded final block silently inherited its own rate past
its bound (a hidden, never-stated price — e.g. the live ALL tariff billed
hour 4+ at the 3rd-hour rate). rateAt() still prices legacy bounded-tail
versions; validation is publish-only, so published immutable versions are
unaffected (no migration).
- TariffComposer edits bands as a DURATION in hours ("first 2 hours, then next
3 hours"), accumulated into the engine's cumulative uptoMin (minutes) on
submit. The last row is a pinned, non-removable "thereafter (open-ended)"
band, so a published card always satisfies the open-ended-last rule.
blocksToForm round-trips stored minutes back to band hours (legacy loads).
- i18n: replaced upToMin/egExample with bandDuration/hoursUnit/egHours (sq+en,
catalog parity green).
Verified: validator rejects bounded-last / accepts open-ended; computeFee
correct at 1/2/3/5/6/24h for a 0-2h@200,2-5h@100,5h+@50 + 1000 cap card. Full
build green. Wiki (tariff.md, log.md) updated.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -200,6 +200,16 @@ export function validateTariffStructure(s: unknown): string[] {
|
||||
}
|
||||
}
|
||||
});
|
||||
// The LAST block must be open-ended (uptoMin null) so the "thereafter" rate is
|
||||
// always explicit. A bounded final block silently inherits its own rate past
|
||||
// its bound (a hidden, never-stated price) — forbidden on publish so the admin
|
||||
// must state what time beyond the ladder costs. See wiki/concepts/tariff.md.
|
||||
// (Read/pricing of already-published versions is unaffected — validation runs
|
||||
// only on publish; rateAt() still gracefully handles legacy bounded tails.)
|
||||
const lastBlock = t.blocks[t.blocks.length - 1];
|
||||
if (lastBlock && lastBlock.uptoMin != null) {
|
||||
errs.push("the last block must be open-ended (uptoMin: null) — the thereafter-rate must be stated explicitly");
|
||||
}
|
||||
}
|
||||
return errs;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user