fix(delivery): number same-day packages per episode, not per project
Deploy / deploy (push) Successful in 7m31s
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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user