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:
@@ -109,11 +109,12 @@ export const en: Catalog = {
|
||||
lostTicketFee: "Lost-ticket fee",
|
||||
exitGrace: "Exit walk-back grace (min)",
|
||||
rateBlocks: "Rate blocks",
|
||||
rateBlocksHint: "Consumed in order as time accrues. \"Up to (min)\" is the block's upper bound; leave the last block's bound blank for \"thereafter\". Price is per billing increment.",
|
||||
upToMin: "Up to (min)",
|
||||
rateBlocksHint: "Each band lasts a number of hours and bills at its own price; bands are consumed in order (the first hours, then the next hours). The last band is \"thereafter\" (open-ended) — its price applies once the ladder is exhausted. Price is per billing increment.",
|
||||
bandDuration: "Band duration",
|
||||
hoursUnit: "hours",
|
||||
egHours: "e.g. 2",
|
||||
pricePerIncrement: "Price / increment",
|
||||
thereafter: "thereafter",
|
||||
egExample: "e.g. 60",
|
||||
thereafter: "thereafter (open-ended)",
|
||||
remove: "Remove",
|
||||
addBlock: "+ Add block",
|
||||
publishNewVersion: "Publish new version",
|
||||
|
||||
Reference in New Issue
Block a user