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",
|
||||
|
||||
@@ -71,8 +71,8 @@ export const sq = {
|
||||
free: "Vende të lira",
|
||||
lotFull: "● parkimi plot",
|
||||
liveFeed: "Aktiviteti live",
|
||||
events: "ngjarje",
|
||||
noEventsYet: "Asnjë ngjarje ende — hyrjet dhe daljet do të shfaqen këtu.",
|
||||
events: "Evente",
|
||||
noEventsYet: "Asnjë event ende — hyrjet dhe daljet do të shfaqen këtu.",
|
||||
activeSessions: "Sesionet aktive",
|
||||
insideCount: "brenda",
|
||||
noActiveSessions: "Asnjë sesion aktiv.",
|
||||
@@ -111,11 +111,12 @@ export const sq = {
|
||||
lostTicketFee: "Tarifa për biletë të humbur",
|
||||
exitGrace: "Periudha e kthimit në dalje (min)",
|
||||
rateBlocks: "Blloqet tarifore",
|
||||
rateBlocksHint: "Konsumohen me radhë me kalimin e kohës. \"Deri në (min)\" është kufiri i sipërm i bllokut; lëre bosh kufirin e bllokut të fundit për \"më pas\". Çmimi është për interval faturimi.",
|
||||
upToMin: "Deri në (min)",
|
||||
rateBlocksHint: "Çdo brez zgjat një numër orësh dhe faturohet me çmimin e tij; brezat konsumohen me radhë (orët e para, pastaj orët në vijim). Brezi i fundit është \"më pas\" (i hapur) — çmimi i tij zbatohet pas mbarimit të shkallës. Çmimi është për interval faturimi.",
|
||||
bandDuration: "Kohëzgjatja e brezit",
|
||||
hoursUnit: "orë",
|
||||
egHours: "p.sh. 2",
|
||||
pricePerIncrement: "Çmimi / interval",
|
||||
thereafter: "më pas",
|
||||
egExample: "p.sh. 60",
|
||||
thereafter: "më pas (i hapur)",
|
||||
remove: "Hiq",
|
||||
addBlock: "+ Shto bllok",
|
||||
publishNewVersion: "Publiko version të ri",
|
||||
@@ -179,7 +180,7 @@ export const sq = {
|
||||
printCode: "Printo kodin",
|
||||
printedOn: "Kodi u printua te {{printer}}.",
|
||||
confirmRevoke: "Të anulohet abonimi për {{name}}? Do të refuzohet te barriera.",
|
||||
confirmDelete: "Të fshihet abonimi për {{name}}? (Ngjarjet e kaluara ruhen.)",
|
||||
confirmDelete: "Të fshihet abonimi për {{name}}? (t e kaluara ruhen.)",
|
||||
statusActive: "aktiv",
|
||||
statusSuspended: "pezulluar",
|
||||
statusRevoked: "anuluar",
|
||||
|
||||
Reference in New Issue
Block a user