feat(tariff-lab): DB-backed draft tariffs + named published versions
Experimenting used to mean publishing — churning the immutable version
history and risking real tickets pricing against a half-baked card while
the admin iterated. The lab is now a true sandbox:
- tariff_drafts table (migration 0021): MUTABLE by design — the one
exception to "editing publishes a version"; a draft prices nothing and
signs nothing. Drafts are validated + tz-stamped on save exactly like a
publish, so a saved draft always simulates and never fails at publish.
- CRUD under /api/tariff/drafts (list tariff:read, mutations
tariff:update); publishing a draft goes through the normal immutable
POST /api/tariff/versions path.
- Lab UI rebuilt: sidebar lists lab drafts AND the full published history
(click any to price against it); main pane cut to pure entry/exit
(ticket loader, payment, category inputs dropped); the composer form is
extracted to TariffEditorForm.tsx and reused in a modal (new drafts
prefill from the active card); per-draft Publish with confirm.
- tariff_versions.name (migration 0022): optional label stamped at
publish — carried from the lab draft, or typed in the composer's new
optional field — so history reads "Winter 2027", not UUID prefixes.
- Includes the composer UI + sq/en labels for the package mode (engine
landed in d9e6c13) and the "Flat price / hour" relabel.
5 new server integration tests (RBAC, roundtrip, validation, tz-stamp +
simulate + publish w/ name); server suite 288 green.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
-- Tariff-lab drafts (2026-07-05). A mutable scratchpad for the lab: the admin composes
|
||||
-- experimental rate cards here, simulates them against hypothetical stays, and only
|
||||
-- PUBLISHES (normal immutable tariff_versions path) when satisfied. Deliberately mutable —
|
||||
-- a draft prices nothing and signs nothing; experimenting through real publishes would
|
||||
-- churn permanent versions and risk a wrong card going live. See wiki/concepts/tariff.md.
|
||||
CREATE TABLE `tariff_drafts` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`currency` text NOT NULL,
|
||||
`structure` text NOT NULL,
|
||||
`created_by` text,
|
||||
`created_at` text DEFAULT (current_timestamp) NOT NULL,
|
||||
`updated_at` text DEFAULT (current_timestamp) NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Optional name on published tariff versions (2026-07-05). The lab's draft workflow gave
|
||||
-- rate cards human names; published versions were only tellable apart by effective date +
|
||||
-- UUID prefix. The name is stamped at publish (carried from the lab draft, or typed in the
|
||||
-- composer) and is immutable like the rest of the row. Nullable — old versions and unnamed
|
||||
-- publishes are fine.
|
||||
ALTER TABLE `tariff_versions` ADD `name` text;
|
||||
@@ -148,6 +148,20 @@
|
||||
"when": 1781886300000,
|
||||
"tag": "0020_entry_presence_bypass",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 21,
|
||||
"version": "6",
|
||||
"when": 1781886400000,
|
||||
"tag": "0021_tariff_drafts",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "6",
|
||||
"when": 1781886500000,
|
||||
"tag": "0022_tariff_version_name",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -316,6 +316,10 @@ export const tariffs = sqliteTable("tariffs", {
|
||||
export const tariffVersions = sqliteTable("tariff_versions", {
|
||||
id: text("id").primaryKey(),
|
||||
tariffId: text("tariff_id").notNull(),
|
||||
// Optional human label ("Winter 2027", carried from the lab draft it was published
|
||||
// from). Stamped at publish, immutable like the rest of the row — versions are
|
||||
// told apart in the UI by name, not UUID prefix.
|
||||
name: text("name"),
|
||||
// The version is in force from this instant (latest with effectiveFrom ≤ entry wins).
|
||||
effectiveFrom: text("effective_from").notNull(),
|
||||
// ISO 4217; selectable. Money everywhere is { minorUnits, currency }, never a float.
|
||||
@@ -329,6 +333,29 @@ export const tariffVersions = sqliteTable("tariff_versions", {
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// A LAB DRAFT rate card — the tariff-lab scratchpad. MUTABLE by design (the one
|
||||
// exception to "editing publishes a version"): a draft prices nothing and signs
|
||||
// nothing — it exists so the admin can experiment in the lab without churning real
|
||||
// tariff_versions (each publish is permanent; experimenting through publishes would
|
||||
// bury the history in noise and risk a wrong card going live). Publishing a draft
|
||||
// goes through the normal POST /api/tariff/versions path (validated, tz-stamped,
|
||||
// immutable). See wiki/concepts/tariff.md (Tariff Lab).
|
||||
export const tariffDrafts = sqliteTable("tariff_drafts", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
currency: text("currency").notNull(),
|
||||
// Same TariffStructure shape as tariff_versions.structure; validated on save so
|
||||
// the lab can always simulate it.
|
||||
structure: text("structure", { mode: "json" }).notNull().$type<Record<string, unknown>>(),
|
||||
createdBy: text("created_by"),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// --- Subscriptions --------------------------------------------------------
|
||||
// A subscriber: a known holder who parks on a recurring plan (e.g. 10,000 ALL /
|
||||
// month) instead of paying per stay. Mutable master data; every USE still produces a
|
||||
@@ -513,6 +540,7 @@ export type SetupStateRow = typeof setupState.$inferSelect;
|
||||
export type SiteConfigRow = typeof siteConfig.$inferSelect;
|
||||
export type TariffRow = typeof tariffs.$inferSelect;
|
||||
export type TariffVersionRow = typeof tariffVersions.$inferSelect;
|
||||
export type TariffDraftRow = typeof tariffDrafts.$inferSelect;
|
||||
export type SubscriptionRow = typeof subscriptions.$inferSelect;
|
||||
export type SubscriptionPlanRow = typeof subscriptionPlans.$inferSelect;
|
||||
export type SubscriptionCredentialRow = typeof subscriptionCredentials.$inferSelect;
|
||||
|
||||
Reference in New Issue
Block a user