f30acef101
Deploy / deploy (push) Successful in 5m17s
A built package used to become DELIVERED on one click with nothing behind it, and nothing recorded what a client had asked for. - Marking delivered is now two recorded steps: Record upload (the link on the client's portal, contents confirmed) then Share link (who it was sent to). The second is refused until the first is on record. - Delivery requests log a client's ask by episode, pasted shot codes or a Shot Status selection. A line closes only when its shot goes out with evidence after the request was logged; a request can otherwise only be cancelled with a reason. - Delivered Outside Pipeline takes an upload link and recipient, required for shots on an open request. - The dashboard gains an Awaiting delivery section listing open requests and built packages the client does not have yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
createRequestSchema,
|
|
matchShotCodes,
|
|
parseShotCodes,
|
|
resolveLineStage,
|
|
waitingSeverity,
|
|
} from "./delivery-request-state";
|
|
|
|
describe("parseShotCodes", () => {
|
|
it("takes codes out of a pasted message whatever separates them", () => {
|
|
expect(parseShotCodes("UNG_116_010, UNG_116_020\nUNG_116_030; 116-A-0040")).toEqual([
|
|
"UNG_116_010",
|
|
"UNG_116_020",
|
|
"UNG_116_030",
|
|
"116-A-0040",
|
|
]);
|
|
});
|
|
|
|
it("drops repeats regardless of case", () => {
|
|
expect(parseShotCodes("ung_116_010 UNG_116_010")).toEqual(["ung_116_010"]);
|
|
});
|
|
});
|
|
|
|
describe("matchShotCodes", () => {
|
|
const shots = [
|
|
{ id: "a", shotCode: "UNG_116_010", clientCode: "116_A_0010" },
|
|
{ id: "b", shotCode: "UNG_116_020", clientCode: null },
|
|
];
|
|
|
|
it("matches our code or the client's, in any case", () => {
|
|
const { matched, unmatched } = matchShotCodes(["116_a_0010", "ung_116_020"], shots);
|
|
expect(matched.map((s) => s.id)).toEqual(["a", "b"]);
|
|
expect(unmatched).toEqual([]);
|
|
});
|
|
|
|
it("reports what it could not match instead of dropping it", () => {
|
|
expect(matchShotCodes(["UNG_116_010", "UNG_116_999"], shots).unmatched).toEqual(["UNG_116_999"]);
|
|
});
|
|
|
|
it("counts a shot once when both of its codes are given", () => {
|
|
expect(matchShotCodes(["UNG_116_010", "116_A_0010"], shots).matched).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("resolveLineStage", () => {
|
|
const logged = new Date("2026-09-30T14:00:00Z");
|
|
const before = new Date("2026-09-20T10:00:00Z");
|
|
const after = new Date("2026-10-01T10:00:00Z");
|
|
const base = {
|
|
requestCreatedAt: logged,
|
|
packages: [],
|
|
manual: { deliveredAt: null, url: null, sharedWith: null },
|
|
exportStatuses: [],
|
|
};
|
|
|
|
it("walks a shot from not ready through to delivered", () => {
|
|
expect(resolveLineStage(base)).toBe("NOT_READY");
|
|
expect(resolveLineStage({ ...base, exportStatuses: ["READY_FOR_QC"] })).toBe("IN_QC");
|
|
expect(resolveLineStage({ ...base, exportStatuses: ["READY_FOR_QC", "READY_FOR_DELIVERY"] })).toBe("READY");
|
|
const pkg = { status: "BUILDING", uploadedAt: null, deliveredAt: null };
|
|
expect(resolveLineStage({ ...base, packages: [pkg] })).toBe("BUILDING");
|
|
expect(resolveLineStage({ ...base, packages: [{ ...pkg, status: "READY" }] })).toBe("BUILT");
|
|
expect(resolveLineStage({ ...base, packages: [{ ...pkg, status: "READY", uploadedAt: after }] })).toBe("UPLOADED");
|
|
expect(
|
|
resolveLineStage({ ...base, packages: [{ status: "DELIVERED", uploadedAt: after, deliveredAt: after }] })
|
|
).toBe("DELIVERED");
|
|
});
|
|
|
|
it("does not let a delivery from before the request close it", () => {
|
|
expect(
|
|
resolveLineStage({ ...base, packages: [{ status: "DELIVERED", uploadedAt: before, deliveredAt: before }] })
|
|
).toBe("NOT_READY");
|
|
});
|
|
|
|
it("closes on a manual delivery only with a link and a recipient", () => {
|
|
const sameDay = new Date("2026-09-30T12:00:00Z");
|
|
const manual = { deliveredAt: sameDay, url: "https://x.example/f", sharedWith: "Sam" };
|
|
expect(resolveLineStage({ ...base, manual })).toBe("DELIVERED");
|
|
expect(resolveLineStage({ ...base, manual: { ...manual, url: null } })).toBe("NOT_READY");
|
|
expect(resolveLineStage({ ...base, manual: { ...manual, sharedWith: null } })).toBe("NOT_READY");
|
|
expect(resolveLineStage({ ...base, manual: { ...manual, deliveredAt: before } })).toBe("NOT_READY");
|
|
});
|
|
});
|
|
|
|
describe("waitingSeverity", () => {
|
|
const now = new Date("2026-09-30T12:00:00Z").getTime();
|
|
const hoursAgo = (h: number) => new Date(now - h * 3600_000);
|
|
|
|
it("escalates with age", () => {
|
|
expect(waitingSeverity(hoursAgo(2), null, now)).toBe("normal");
|
|
expect(waitingSeverity(hoursAgo(30), null, now)).toBe("warning");
|
|
expect(waitingSeverity(hoursAgo(50), null, now)).toBe("critical");
|
|
});
|
|
|
|
it("is critical once past the due date, however new", () => {
|
|
expect(waitingSeverity(hoursAgo(2), hoursAgo(1), now)).toBe("critical");
|
|
});
|
|
});
|
|
|
|
describe("createRequestSchema", () => {
|
|
const base = { projectId: "p", deliverable: "EXR", requestedBy: "Sam" };
|
|
|
|
it("needs exactly one way of naming the shots", () => {
|
|
expect(createRequestSchema.safeParse(base).success).toBe(false);
|
|
expect(createRequestSchema.safeParse({ ...base, episode: "116" }).success).toBe(true);
|
|
expect(createRequestSchema.safeParse({ ...base, episode: "116", shotCodes: "UNG_116_010" }).success).toBe(false);
|
|
});
|
|
|
|
it("needs to know who asked", () => {
|
|
expect(createRequestSchema.safeParse({ ...base, episode: "116", requestedBy: " " }).success).toBe(false);
|
|
});
|
|
});
|