Simplified Show + Occurrences System Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Simplify the show system by removing batch generation, denormalizing prices, and adding occurrence codes (e.g., TMTL260521N).
Architecture: Two-level model: shows (landing page content) → occurrences (buyable product instances with unique codes). Each admin manually creates or duplicates occurrences. No batch generation.
Tech Stack: Convex + Zod validation, Next.js frontend
Background
User Requirements:
- Shows have a short code (e.g.,
TMTLfor "The Mentalist") - Each occurrence gets a unique code:
{showCode}{YYMMDD}{L|N}(e.g.,TMTL260521N) - No batch generation — admin manually creates or duplicates each occurrence
- Price denormalization — no override fields
File Structure
packages/backend/convex/
├── schema.ts # Rename tables, update fields
├── lib/
│ └── zod.ts # Already exists
├── domains/
│ └── shows/
│ ├── shows.ts # Show template CRUD (rename from shows.ts)
│ └── occurrences.ts # Occurrence CRUD (simplified)
└── http/
└── onepay.ts # Unchanged
apps/frontend/
├── components/home/show-schedule-preview.tsx # Uses api.occurrences.listForSchedulePreview
├── app/booking/
│ └── page.tsx # Uses show picker (update to new schema)
└── app/admin/
└── (existing pages) # Update to new schemaTask 1: Update Schema — Rename Tables and Simplify Fields
Files:
-
Modify:
packages/backend/convex/schema.ts -
Step 1: Read current schema
Read packages/backend/convex/schema.ts to understand current structure.
- Step 2: Rename
showTemplatestoshows
shows: defineTable({
code: v.string(), // e.g., "TMTL"
title: v.string(),
tagline: v.string(),
description: v.string(),
embeddedVideo: v.optional(v.string()),
gallery: v.array(v.string()),
supportedTicketTypes: v.array(v.union(
v.literal("DINNER_THEATRE"),
v.literal("SHOW_ONLY")
)),
defaultDinnerPrice: v.number(),
defaultShowOnlyPrice: v.number(),
defaultCapacity: v.number(),
status: v.union(v.literal("DRAFT"), v.literal("ACTIVE"), v.literal("ARCHIVED")),
slug: v.string(),
})
.index("by_status", ["status"])
.index("by_slug", ["slug"])
.index("by_code", ["code"]),- Step 3: Rename
showOccurrencestooccurrencesand simplify
occurrences: defineTable({
showId: v.id("shows"), // Reference to shows (was templateId)
code: v.string(), // e.g., "TMTL260521N" (unique product ID)
date: v.string(), // "YYYY-MM-DD"
time: v.string(), // "19:30"
dinnerPrice: v.number(), // Denormalized — no override
showOnlyPrice: v.number(), // Denormalized — no override
showOnlyEnabled: v.boolean(),
actualCapacity: v.number(),
bookedCount: v.number(),
status: v.union(
v.literal("SCHEDULED"),
v.literal("CANCELLED"),
v.literal("SOLD_OUT")
),
assignedTables: v.array(v.any()),
})
.index("by_show", ["showId"])
.index("by_date", ["date"])
.index("by_show_date", ["showId", "date"])
.index("by_date_status", ["date", "status"])
.index("by_code", ["code"]),- Step 4: Update
reservationstable reference
reservations: defineTable({
occurrenceId: v.id("occurrences"), // Was showOccurrences
// ... rest unchanged
});- Step 5: Commit
git add packages/backend/convex/schema.ts
git commit -m "refactor: rename showTemplates→shows, showOccurrences→occurrences, denormalize prices"Task 2: Create Occurrence Code Generator
Files:
-
Create:
packages/backend/convex/domains/shows/code-generator.ts -
Step 1: Write occurrence code generator
// packages/backend/convex/domains/shows/code-generator.ts
export type ShowTime = "L" | "N"; // Lunch or Night
/**
* Generate unique occurrence code: {showCode}{YYMMDD}{L|N}
* e.g., TMTL260521N
*/
export function generateOccurrenceCode(
showCode: string,
date: string, // "YYYY-MM-DD"
time: ShowTime,
): string {
const datePart = date.replace(/-/g, ""); // "2026-05-21" → "260521"
return `${showCode}${datePart}${time}`;
}
/**
* Parse occurrence code back to components
*/
export function parseOccurrenceCode(code: string): {
showCode: string;
date: string;
time: ShowTime;
} | null {
const match = code.match(/^([A-Z]+)(\d{6})([LN])$/);
if (!match) return null;
const [, showCode, datePart, time] = match;
// Reconstruct date: "260521" → "2026-05-21"
const year = "20" + datePart.slice(0, 2);
const month = datePart.slice(2, 4);
const day = datePart.slice(4, 6);
return {
showCode,
date: `${year}-${month}-${day}`,
time: time as ShowTime,
};
}- Step 2: Commit
git add packages/backend/convex/domains/shows/code-generator.ts
git commit -m "feat: add occurrence code generator"Task 3: Update Shows Functions (Rename + Simplify)
Files:
-
Modify:
packages/backend/convex/domains/shows/shows.ts -
Step 1: Read current shows.ts
Read packages/backend/convex/domains/shows/shows.ts.
- Step 2: Update to use new table name
showsand new fields
import { zQuery, zMutation, z } from "../../../lib/zod";
import { zid } from "convex-helpers/server/zod3";
export const listAll = zQuery({
args: {},
returns: z.array(
z.object({
_id: zid("shows"),
code: z.string(),
title: z.string(),
slug: z.string(),
status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]),
}),
),
handler: async (ctx) => {
return await ctx.db.query("shows").collect();
},
});
export const listActive = zQuery({
args: {},
returns: z.array(
z.object({
_id: zid("shows"),
code: z.string(),
title: z.string(),
tagline: z.string(),
slug: z.string(),
gallery: z.array(z.string()),
}),
),
handler: async (ctx) => {
return await ctx.db
.query("shows")
.withIndex("by_status", (q) => q.eq("status", "ACTIVE"))
.collect();
},
});
export const getBySlug = zQuery({
args: { slug: z.string() },
returns: z
.object({
_id: zid("shows"),
code: z.string(),
title: z.string(),
tagline: z.string(),
description: z.string(),
embeddedVideo: z.string().optional(),
gallery: z.array(z.string()),
supportedTicketTypes: z.array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"])),
defaultDinnerPrice: z.number(),
defaultShowOnlyPrice: z.number(),
defaultCapacity: z.number(),
})
.nullable(),
handler: async (ctx, { slug }) => {
return await ctx.db
.query("shows")
.withIndex("by_slug", (q) => q.eq("slug", slug))
.first();
},
});
export const createShow = zMutation({
args: z.object({
code: z.string().min(2).max(10),
title: z.string().min(1).max(200),
slug: z.string().regex(/^[a-z0-9-]+$/),
tagline: z.string(),
description: z.string(),
embeddedVideo: z.string().optional(),
gallery: z.array(z.string()),
supportedTicketTypes: z.array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"])),
defaultDinnerPrice: z.number().nonnegative(),
defaultShowOnlyPrice: z.number().nonnegative(),
defaultCapacity: z.number().int().positive(),
status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]).default("DRAFT"),
}),
handler: async (ctx, args) => {
const id = await ctx.db.insert("shows", {
...args,
createdAt: Date.now(),
updatedAt: Date.now(),
});
return id;
},
});
export const updateShow = zMutation({
args: z.object({
id: zid("shows"),
code: z.string().min(2).max(10).optional(),
title: z.string().min(1).max(200).optional(),
slug: z
.string()
.regex(/^[a-z0-9-]+$/)
.optional(),
tagline: z.string().optional(),
description: z.string().optional(),
embeddedVideo: z.string().optional(),
gallery: z.array(z.string()).optional(),
supportedTicketTypes: z
.array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"]))
.optional(),
defaultDinnerPrice: z.number().nonnegative().optional(),
defaultShowOnlyPrice: z.number().nonnegative().optional(),
defaultCapacity: z.number().int().positive().optional(),
status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]).optional(),
}),
handler: async (ctx, args) => {
const { id, ...updates } = args;
await ctx.db.patch(id, { ...updates, updatedAt: Date.now() });
return id;
},
});
export const archiveShow = zMutation({
args: { id: zid("shows") },
handler: async (ctx, { id }) => {
await ctx.db.patch(id, { status: "ARCHIVED", updatedAt: Date.now() });
return id;
},
});- Step 3: Commit
git add packages/backend/convex/domains/shows/shows.ts
git commit -m "refactor: update shows functions for new schema"Task 4: Simplify Occurrences Functions (Remove Batch, Denormalize)
Files:
-
Modify:
packages/backend/convex/domains/shows/occurrences.ts -
Create:
packages/backend/convex/domains/shows/occurrences.ts(replace existing) -
Step 1: Read current occurrences.ts
Read packages/backend/convex/domains/shows/occurrences.ts (already read in context).
- Step 2: Rewrite with simplified schema
import { zQuery, zMutation, z } from "../../../lib/zod";
import { zid } from "convex-helpers/server/zod3";
import { generateOccurrenceCode, ShowTime } from "./code-generator";
export type ShowScheduleItem = {
date: string;
dayLabel: string;
time: string;
title: string;
subtitle: string;
image: string;
remaining: number;
slug: string;
code: string;
};
function formatTime(time: string): string {
const [hours, minutes] = time.split(":").map(Number);
const period = hours >= 12 ? "PM" : "AM";
const hour12 = hours % 12 || 12;
return `${hour12}:${String(minutes).padStart(2, "0")} ${period}`;
}
function getDayLabel(dateStr: string): string {
const days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const date = new Date(dateStr + "T00:00:00");
return days[date.getDay()];
}
// Get upcoming occurrences for homepage carousel
export const listUpcoming = zQuery({
args: { limit: z.number().optional() },
returns: z.array(
z.object({
_id: zid("occurrences"),
code: z.string(),
date: z.string(),
time: z.string(),
dinnerPrice: z.number(),
showOnlyPrice: z.number(),
showOnlyEnabled: z.boolean(),
actualCapacity: z.number(),
bookedCount: z.number(),
remaining: z.number(),
showTitle: z.string(),
showSlug: z.string(),
showCode: z.string(),
}),
),
handler: async (ctx, { limit = 8 }) => {
const today = new Date().toISOString().split("T")[0];
const occurrences = await ctx.db
.query("occurrences")
.withIndex("by_date_status", (q) => q.gte("date", today))
.collect();
// Filter to scheduled only, join with show, sort by date+time
const scheduled = occurrences.filter((o) => o.status === "SCHEDULED");
// Batch fetch shows
const showIds = [...new Set(scheduled.map((o) => o.showId))];
const shows = await Promise.all(showIds.map((id) => ctx.db.get(id)));
const showMap = new Map(shows.filter(Boolean).map((s) => [s!._id, s!]));
return scheduled
.map((o) => {
const show = showMap.get(o.showId);
if (!show) return null;
return {
_id: o._id,
code: o.code,
date: o.date,
time: o.time,
dinnerPrice: o.dinnerPrice,
showOnlyPrice: o.showOnlyPrice,
showOnlyEnabled: o.showOnlyEnabled,
actualCapacity: o.actualCapacity,
bookedCount: o.bookedCount,
remaining: o.actualCapacity - o.bookedCount,
showTitle: show.title,
showSlug: show.slug,
showCode: show.code,
};
})
.filter(Boolean)
.sort((a, b) => {
const dateCompare = a!.date.localeCompare(b!.date);
if (dateCompare !== 0) return dateCompare;
return a!.time.localeCompare(b!.time);
})
.slice(0, limit);
},
});
// Get all scheduled occurrences for dashboard (today + future, sorted)
export const listForDashboard = zQuery({
args: {},
returns: z.array(
z.object({
_id: zid("occurrences"),
code: z.string(),
showId: zid("shows"),
showTitle: z.string(),
date: z.string(),
time: z.string(),
dinnerPrice: z.number(),
showOnlyPrice: z.number(),
showOnlyEnabled: z.boolean(),
actualCapacity: z.number(),
bookedCount: z.number(),
status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
}),
),
handler: async (ctx) => {
const today = new Date().toISOString().split("T")[0];
const occurrences = await ctx.db
.query("occurrences")
.withIndex("by_date_status", (q) => q.gte("date", today))
.collect();
// Fetch all shows for joining
const showIds = [...new Set(occurrences.map((o) => o.showId))];
const shows = await Promise.all(showIds.map((id) => ctx.db.get(id)));
const showMap = new Map(shows.filter(Boolean).map((s) => [s!._id, s!]));
return occurrences
.filter((o) => o.status === "SCHEDULED")
.map((o) => {
const show = showMap.get(o.showId);
return {
_id: o._id,
code: o.code,
showId: o.showId,
showTitle: show?.title ?? "Unknown Show",
date: o.date,
time: o.time,
dinnerPrice: o.dinnerPrice,
showOnlyPrice: o.showOnlyPrice,
showOnlyEnabled: o.showOnlyEnabled,
actualCapacity: o.actualCapacity,
bookedCount: o.bookedCount,
status: o.status,
};
})
.sort((a, b) => {
const dateCompare = a.date.localeCompare(b.date);
if (dateCompare !== 0) return dateCompare;
return a.time.localeCompare(b.time);
});
},
});
// Get occurrences for a specific show
export const byShow = zQuery({
args: { showId: zid("shows") },
returns: z.array(
z.object({
_id: zid("occurrences"),
code: z.string(),
date: z.string(),
time: z.string(),
dinnerPrice: z.number(),
showOnlyPrice: z.number(),
showOnlyEnabled: z.boolean(),
actualCapacity: z.number(),
bookedCount: z.number(),
status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
}),
),
handler: async (ctx, { showId }) => {
const today = new Date().toISOString().split("T")[0];
return await ctx.db
.query("occurrences")
.withIndex("by_show_date", (q) =>
q.eq("showId", showId).gte("date", today),
)
.collect();
},
});
// Get single occurrence with show info
export const getWithShow = zQuery({
args: { id: zid("occurrences") },
returns: z
.object({
occurrence: z.object({
_id: zid("occurrences"),
code: z.string(),
showId: zid("shows"),
date: z.string(),
time: z.string(),
dinnerPrice: z.number(),
showOnlyPrice: z.number(),
showOnlyEnabled: z.boolean(),
actualCapacity: z.number(),
bookedCount: z.number(),
status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
}),
show: z.object({
_id: zid("shows"),
code: z.string(),
title: z.string(),
tagline: z.string(),
description: z.string(),
gallery: z.array(z.string()),
supportedTicketTypes: z.array(z.enum(["DINNER_THEATRE", "SHOW_ONLY"])),
}),
})
.nullable(),
handler: async (ctx, { id }) => {
const occurrence = await ctx.db.get("occurrences", id);
if (!occurrence) return null;
const show = await ctx.db.get("shows", occurrence.showId);
if (!show) return null;
return { occurrence, show };
},
});
// Create single occurrence (admin manually creates)
export const createOccurrence = zMutation({
args: z.object({
showId: zid("shows"),
date: z.string(), // "YYYY-MM-DD"
time: z.string(), // "19:30"
timeSlot: z.enum(["L", "N"]), // Lunch or Night
dinnerPrice: z.number(),
showOnlyPrice: z.number(),
showOnlyEnabled: z.boolean(),
actualCapacity: z.number(),
}),
returns: z.object({ id: zid("occurrences"), code: z.string() }),
handler: async (ctx, args) => {
const {
showId,
date,
time,
timeSlot,
dinnerPrice,
showOnlyPrice,
showOnlyEnabled,
actualCapacity,
} = args;
// Get show to get the code
const show = await ctx.db.get("shows", showId);
if (!show) throw new Error("Show not found");
// Generate unique code
const code = generateOccurrenceCode(show.code, date, timeSlot);
// Check for duplicate code
const existing = await ctx.db
.query("occurrences")
.withIndex("by_code", (q) => q.eq("code", code))
.first();
if (existing)
throw new Error(`Occurrence with code ${code} already exists`);
const id = await ctx.db.insert("occurrences", {
showId,
code,
date,
time,
dinnerPrice,
showOnlyPrice,
showOnlyEnabled,
actualCapacity,
bookedCount: 0,
status: "SCHEDULED",
assignedTables: [],
createdAt: Date.now(),
updatedAt: Date.now(),
});
return { id, code };
},
});
// Duplicate an existing occurrence (admin creates copy with new date)
export const duplicateOccurrence = zMutation({
args: z.object({
occurrenceId: zid("occurrences"),
newDate: z.string(), // "YYYY-MM-DD"
newTime: z.string(), // "19:30"
newTimeSlot: z.enum(["L", "N"]),
}),
returns: z.object({ id: zid("occurrences"), code: z.string() }),
handler: async (ctx, args) => {
const { occurrenceId, newDate, newTime, newTimeSlot } = args;
const original = await ctx.db.get("occurrences", occurrenceId);
if (!original) throw new Error("Occurrence not found");
// Get show to get the code
const show = await ctx.db.get("shows", original.showId);
if (!show) throw new Error("Show not found");
// Generate new code
const newCode = generateOccurrenceCode(show.code, newDate, newTimeSlot);
// Check for duplicate code
const existing = await ctx.db
.query("occurrences")
.withIndex("by_code", (q) => q.eq("code", newCode))
.first();
if (existing)
throw new Error(`Occurrence with code ${newCode} already exists`);
const id = await ctx.db.insert("occurrences", {
showId: original.showId,
code: newCode,
date: newDate,
time: newTime,
dinnerPrice: original.dinnerPrice,
showOnlyPrice: original.showOnlyPrice,
showOnlyEnabled: original.showOnlyEnabled,
actualCapacity: original.actualCapacity,
bookedCount: 0,
status: "SCHEDULED",
assignedTables: [],
createdAt: Date.now(),
updatedAt: Date.now(),
});
return { id, code: newCode };
},
});
// Update occurrence (admin edits)
export const updateOccurrence = zMutation({
args: z.object({
id: zid("occurrences"),
date: z.string().optional(),
time: z.string().optional(),
dinnerPrice: z.number().optional(),
showOnlyPrice: z.number().optional(),
showOnlyEnabled: z.boolean().optional(),
actualCapacity: z.number().optional(),
status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]).optional(),
}),
handler: async (ctx, args) => {
const { id, ...updates } = args;
await ctx.db.patch(id, { ...updates, updatedAt: Date.now() });
return id;
},
});
// Cancel occurrence
export const cancelOccurrence = zMutation({
args: { id: zid("occurrences") },
handler: async (ctx, { id }) => {
await ctx.db.patch(id, { status: "CANCELLED", updatedAt: Date.now() });
return id;
},
});
// Get availability info for real-time scarcity signals
export const getAvailability = zQuery({
args: { occurrenceId: zid("occurrences") },
returns: z
.object({
occurrenceId: zid("occurrences"),
code: z.string(),
showTitle: z.string(),
date: z.string(),
time: z.string(),
totalCapacity: z.number(),
bookedCount: z.number(),
remaining: z.number(),
status: z.enum(["SCHEDULED", "CANCELLED", "SOLD_OUT"]),
showOnlyEnabled: z.boolean(),
dinnerPrice: z.number(),
showOnlyPrice: z.number(),
badge: z.enum(["AVAILABLE", "FEW_LEFT", "SOLD_OUT"]),
})
.nullable(),
handler: async (ctx, { occurrenceId }) => {
const occ = await ctx.db.get("occurrences", occurrenceId);
if (!occ) return null;
const show = await ctx.db.get("shows", occ.showId);
const remaining = occ.actualCapacity - occ.bookedCount;
return {
occurrenceId: occ._id,
code: occ.code,
showTitle: show?.title ?? "Unknown Show",
date: occ.date,
time: occ.time,
totalCapacity: occ.actualCapacity,
bookedCount: occ.bookedCount,
remaining,
status: occ.status,
showOnlyEnabled: occ.showOnlyEnabled,
dinnerPrice: occ.dinnerPrice,
showOnlyPrice: occ.showOnlyPrice,
badge:
remaining > 10 ? "AVAILABLE" : remaining > 0 ? "FEW_LEFT" : "SOLD_OUT",
};
},
});
// Get occurrences for schedule preview (homepage)
export const listForSchedulePreview = zQuery({
args: { limit: z.number().optional() },
returns: z.array(
z.object({
date: z.string(),
dayLabel: z.string(),
time: z.string(),
title: z.string(),
subtitle: z.string(),
image: z.string(),
remaining: z.number(),
slug: z.string(),
code: z.string(),
}),
),
handler: async (ctx, { limit = 30 }) => {
const today = new Date().toISOString().split("T")[0];
const allOccurrences = await ctx.db
.query("occurrences")
.withIndex("by_date", (q) => q.gte("date", today))
.collect();
const scheduled = allOccurrences
.filter((o) => o.status === "SCHEDULED")
.sort((a, b) => {
const dateCompare = a.date.localeCompare(b.date);
if (dateCompare !== 0) return dateCompare;
return a.time.localeCompare(b.time);
});
// Fetch all unique shows
const showIds = [...new Set(scheduled.map((o) => o.showId))];
const shows = await Promise.all(showIds.map((id) => ctx.db.get(id)));
const showMap = new Map(shows.filter(Boolean).map((s) => [s!._id, s!]));
const items: ShowScheduleItem[] = [];
for (const occ of scheduled) {
const show = showMap.get(occ.showId);
if (!show || show.status !== "ACTIVE") continue;
const image = show.gallery.length > 0 ? show.gallery[0] : "";
items.push({
date: occ.date,
dayLabel: getDayLabel(occ.date),
time: formatTime(occ.time),
title: show.title,
subtitle: show.tagline,
image,
remaining: occ.actualCapacity - occ.bookedCount,
slug: show.slug,
code: occ.code,
});
if (items.length >= limit) break;
}
return items;
},
});- Step 3: Commit
git add packages/backend/convex/domains/shows/occurrences.ts
git commit -m "refactor: simplify occurrences - remove batch generation, denormalize prices, add codes"Task 5: Update Frontend Show Schedule Preview
Files:
-
Modify:
apps/frontend/components/home/show-schedule-preview.tsx -
Step 1: Read current component
cat apps/frontend/components/home/show-schedule-preview.tsx- Step 2: Update to use new API response shape
The component should use api.occurrences.listForSchedulePreview which now returns code field.
- Step 3: Commit
git add apps/frontend/components/home/show-schedule-preview.tsx
git commit -m "refactor: update show-schedule-preview for new occurrence schema"Task 6: Update Booking Flow (Show Picker)
Files:
-
Modify:
apps/frontend/app/booking/page.tsxor relevant components -
Step 1: Read current booking page
-
Step 2: Update to use new schema (
showsinstead ofshowTemplates,occurrenceswith codes) -
Step 3: Commit
git add apps/frontend/app/booking/
git commit -m "refactor: update booking flow for simplified show system"Task 7: Update Admin Dashboard (If Needed)
Files:
-
Modify:
apps/frontend/app/admin/relevant pages -
Step 1: Check admin pages that use shows/occurrences
-
Step 2: Update to new API paths and response shapes
-
Step 3: Commit
git add apps/frontend/app/admin/
git commit -m "refactor: update admin pages for simplified show system"Summary of Changes
| Area | Before | After |
|---|---|---|
| Table: shows | showTemplates | shows |
| Table: occurrences | showOccurrences | occurrences |
| Price fields | defaultDinnerPrice + dinnerPriceOverride | dinnerPrice (denormalized) |
| Price override fields | showOnlyPriceOverride, dinnerPriceOverride | REMOVED |
| Batch generation | generateBatch mutation | REMOVED |
| Occurrence code | None | {showCode}{YYMMDD}{L|N} (e.g., TMTL260521N) |
| Show code | slug only | New code field (e.g., TMTL) |
| Create occurrence | Complex with overrides | createOccurrence with timeSlot (L/N) |
| Duplicate occurrence | N/A | duplicateOccurrence mutation |
API Changes
| Old API | New API |
|---|---|
api.shows.listActive | api.shows.listActive (updated return type) |
api.occurrences.listUpcoming | api.occurrences.listUpcoming (includes code) |
api.occurrences.generateBatch | REMOVED |
api.occurrences.createOccurrence | api.occurrences.createOccurrence (simplified args) |
| N/A | api.occurrences.duplicateOccurrence (new) |
api.occurrences.listForSchedulePreview | api.occurrences.listForSchedulePreview (includes code) |
Testing Checklist
- Create a new show with code (e.g., "TMTL")
- Create an occurrence manually — verify code generation (e.g.,
TMTL260521N) - Duplicate an occurrence — verify new code is generated
- List occurrences — verify codes appear correctly
- Schedule preview on homepage — verify codes displayed
- Booking flow — verify show picker works with new schema
- Admin dashboard — verify occurrence management works
- Verify no batch generation endpoints exist