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:
@@ -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