fix(delivery): number same-day packages per episode, not per project
Deploy / deploy (push) Successful in 7m31s

Numbering was scoped to the project, so the day's first delivery of
episode 102 was named _02 purely because episode 101 had already gone out
that morning. The count is meant to distinguish repeat deliveries of the
same content, and packageDir already separates episodes — three episodes
delivering today each land in their own folder and each are that folder's
first delivery. Adding `episode` to the lookup makes the suffix mean what
the folder layout already implies: only a second delivery of the *same*
episode becomes _02. The [projectId, episode] index already exists, so
the extra filter costs nothing.

packageNameTemplate also moves from {yymmdd} to {yyyymmdd}. The AE panel
has always built its local delivery folders with a four-digit year, so
this is the server matching the connector rather than a new convention.
The {yymmdd} token stays in TokenValues: a project carrying its own
template in deliveryConfig is unaffected either way.

Existing packages are untouched — buildDeliveryJobManifest reads rootPath
off the record. On the day of the changeover an episode that already
delivered as 260903_Delivery starts again at the bare 20260903_Delivery,
which is a different name and a different folder, so nothing merges.

Producer reviews keep their per-project numbering: a review can span
several episodes and stores episode as "", so scoping it the same way
would put every such review in one bucket.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-09-03 10:07:52 +02:00
parent 04e5eb123a
commit a073f92da3
3 changed files with 47 additions and 9 deletions
+39 -6
View File
@@ -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");
});
});
+7 -2
View File
@@ -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));
+1 -1
View File
@@ -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)