fix(tariff): reject stepped base combined with time/seasonal tiers
A stepped ("up-to") default card prices the whole stay as one total, so the V2
engine short-circuits to steppedFee and NEVER consults windowed cards — any
time/seasonal tiers would silently never fire. Found live: an active tariff had a
stepped base plus weekday-night + weekend tiers, and every 3h stay priced 600 ALL
regardless of hour/day because the tiers were dead.
- validateTariffV2 now rejects a stepped defaultCard combined with windowedCards,
with an actionable message (switch the base to ladder/flat, or remove the tiers).
- Composer shows an inline red warning the moment base mode is stepped and tiers
exist; publishing is blocked server-side regardless.
- ApiError now carries the server's problems[], so the publish error surfaces the
SPECIFIC reason instead of a generic "invalid tariff structure".
- 2 new validation tests (55 pass).
Wiki: tariff, log.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -341,8 +341,8 @@ export function TariffComposer() {
|
||||
setMsg({ kind: "ok", text: t("tariff.publishedOk") });
|
||||
} catch (e) {
|
||||
const text =
|
||||
e instanceof ApiError && (e as ApiError & { problems?: string[] }).problems
|
||||
? `${e.message}: ${((e as ApiError & { problems?: string[] }).problems ?? []).join("; ")}`
|
||||
e instanceof ApiError && e.problems?.length
|
||||
? `${e.message}: ${e.problems.join("; ")}`
|
||||
: (e as Error).message;
|
||||
setMsg({ kind: "err", text });
|
||||
} finally {
|
||||
@@ -404,6 +404,13 @@ export function TariffComposer() {
|
||||
<details className="mt-6" open={form.tiers.length > 0}>
|
||||
<summary className="cursor-pointer text-h6 font-semibold uppercase tracking-wider text-term-text">{t("tariff.tiersAdvanced")}</summary>
|
||||
<p className="hint mt-1.5 mb-2">{t("tariff.tiersHint")}</p>
|
||||
{/* A stepped ("up-to") base rate cannot be combined with time tiers — the
|
||||
engine would ignore them. Warn up-front; publishing is also blocked server-side. */}
|
||||
{form.base.mode === "stepped" && form.tiers.length > 0 && (
|
||||
<p className="mb-3 rounded-term border border-term-red/50 bg-term-red/10 px-3 py-2 text-[12px] text-term-red">
|
||||
{t("tariff.steppedTiersConflict")}
|
||||
</p>
|
||||
)}
|
||||
{form.tiers.map((tr, i) => (
|
||||
<fieldset key={i} className="card mb-3 p-4">
|
||||
<legend className="flex items-center gap-2 px-1">
|
||||
|
||||
Reference in New Issue
Block a user