diff --git a/lib/render-pipeline/deliveries.test.ts b/lib/render-pipeline/deliveries.test.ts index 7c81eb7..2038f27 100644 --- a/lib/render-pipeline/deliveries.test.ts +++ b/lib/render-pipeline/deliveries.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { DELIVERY_CONFIG_DEFAULT, applyTokens, nextPackageName, tidyPath } from "./deliveries"; +import { DELIVERY_CONFIG_DEFAULT, applyTokens, dateTokens, nextPackageName, tidyPath } from "./deliveries"; -const BASE = "260901_Delivery"; +const BASE = "20260901_Delivery"; describe("nextPackageName", () => { it("uses the bare base for the first package of the day", () => { @@ -18,7 +18,7 @@ describe("nextPackageName", () => { }); it("ignores names that only share a prefix", () => { - expect(nextPackageName(BASE, [`${BASE}_final`, "260902_Delivery"])).toBe(BASE); + expect(nextPackageName(BASE, [`${BASE}_final`, "20260902_Delivery"])).toBe(BASE); }); }); @@ -33,13 +33,13 @@ describe("delivery layout", () => { it("separates shows so two shows' episode 101 cannot collide", () => { const acme = applyTokens(DELIVERY_CONFIG_DEFAULT.layout.packageDir, tokens); const other = applyTokens(DELIVERY_CONFIG_DEFAULT.layout.packageDir, { ...tokens, showId: "BRAVO" }); - expect(acme).toBe("//SAN/DELIVERY/ACME/101/260901_Delivery"); + expect(acme).toBe("//SAN/DELIVERY/ACME/101/20260901_Delivery"); expect(other).not.toBe(acme); }); it("puts producer reviews under the show too", () => { expect(applyTokens(DELIVERY_CONFIG_DEFAULT.layout.reviewDir, tokens)).toBe( - "//SAN/DELIVERY/ACME/PRODUCER_REVIEWS/260901_Delivery" + "//SAN/DELIVERY/ACME/PRODUCER_REVIEWS/20260901_Delivery" ); }); }); @@ -52,7 +52,7 @@ describe("tidyPath", () => { episode: "", packageName: BASE, }); - expect(tidyPath(raw)).toBe("//SAN/DELIVERY/ACME/260901_Delivery"); + expect(tidyPath(raw)).toBe("//SAN/DELIVERY/ACME/20260901_Delivery"); }); it("keeps the UNC root intact", () => { @@ -63,3 +63,36 @@ describe("tidyPath", () => { expect(tidyPath("V:/DELIVERY/ACME/101/x")).toBe("V:/DELIVERY/ACME/101/x"); }); }); + +describe("per-episode numbering", () => { + // The scoping itself lives in createDeliveryPackage's query, which passes + // only the names already used by *this* episode. These pin the contract that + // depends on: another episode's delivery must not push this one to _02. + it("gives each episode's first delivery of the day the bare name", () => { + const ep101 = nextPackageName(BASE, [BASE]); + const ep102 = nextPackageName(BASE, []); + expect(ep101).toBe(`${BASE}_02`); + expect(ep102).toBe(BASE); + }); + + it("keeps two episodes' same-named packages in separate folders", () => { + const dirFor = (episode: string, packageName: string) => + tidyPath( + applyTokens(DELIVERY_CONFIG_DEFAULT.layout.packageDir, { + deliveryRoot: "//SAN/DELIVERY", + showId: "ACME", + episode, + packageName, + }) + ); + expect(dirFor("101", BASE)).toBe("//SAN/DELIVERY/ACME/101/20260901_Delivery"); + expect(dirFor("102", BASE)).toBe("//SAN/DELIVERY/ACME/102/20260901_Delivery"); + }); +}); + +describe("package name template", () => { + it("names the day with a four-digit year", () => { + const name = applyTokens(DELIVERY_CONFIG_DEFAULT.packageNameTemplate, dateTokens(new Date(2026, 8, 1))); + expect(name).toBe("20260901_Delivery"); + }); +}); diff --git a/lib/render-pipeline/deliveries.ts b/lib/render-pipeline/deliveries.ts index 7d7cded..5316afe 100644 --- a/lib/render-pipeline/deliveries.ts +++ b/lib/render-pipeline/deliveries.ts @@ -43,7 +43,7 @@ export interface DeliveryConfig { } export const DELIVERY_CONFIG_DEFAULT: DeliveryConfig = { - packageNameTemplate: "{yymmdd}_Delivery", + packageNameTemplate: "{yyyymmdd}_Delivery", reviewNameTemplate: "{yyyymmdd}_Producer_review", layout: { // {showId} is not optional in practice: without it every show's episode 101 @@ -316,9 +316,14 @@ export async function createDeliveryPackage(input: CreateDeliveryInput, userId: // asking for the rest of a sequence — and the date-only template gives it a // name identical to the morning's. Suffix it the way MOV reviews already // are, so it gets its own folder instead of merging into the first one. + // + // Numbering is per episode, not per project: packageDir already separates + // episodes, so three episodes delivering today are each the day's first + // delivery and each get the bare name. Only a repeat delivery of the *same* + // episode collides, and only that one suffixes to _02. const baseName = applyTokens(config.packageNameTemplate, tokens); const sameBase = await tx.deliveryPackage.findMany({ - where: { projectId: project.id, kind: "EXR_DELIVERY", name: { startsWith: baseName } }, + where: { projectId: project.id, episode, kind: "EXR_DELIVERY", name: { startsWith: baseName } }, select: { name: true }, }); tokens.packageName = input.options?.name ?? nextPackageName(baseName, sameBase.map((p) => p.name)); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0fe172d..152fdc8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1112,7 +1112,7 @@ model DeliveryPackage { projectId String project Project @relation(fields: [projectId], references: [id]) episode String // "101" — empty string on standard (non-episodic) shows, and on reviews spanning several - name String // "260805_Delivery" | "20260828_Producer_review_02" + name String // "20260805_Delivery" | "20260828_Producer_review_02" kind DeliveryKind @default(EXR_DELIVERY) packageNumber Int // sequential per project → "Package #15" status DeliveryStatus @default(DRAFT)