@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
// Mock Prisma db so the pure deriveShotStatus function can be tested in isolation
|
||||
vi.mock("@/lib/db", () => ({ db: {} }));
|
||||
|
||||
import { deriveShotStatus } from "./shot-status";
|
||||
import type { TaskStatus } from "@prisma/client";
|
||||
|
||||
// Helper to build task arrays quickly
|
||||
const tasks = (...statuses: TaskStatus[]) => statuses.map((status) => ({ status }));
|
||||
|
||||
describe("deriveShotStatus", () => {
|
||||
// ── WAITING ───────────────────────────────────────────────────────────────
|
||||
|
||||
it("returns WAITING when there are no tasks", () => {
|
||||
expect(deriveShotStatus([], "PENDING", false)).toBe("WAITING");
|
||||
});
|
||||
|
||||
it("returns WAITING when no tasks and internally approved (edge case: no tasks)", () => {
|
||||
// With INTERNALLY_APPROVED + no tasks the approval fields take precedence
|
||||
expect(deriveShotStatus([], "INTERNALLY_APPROVED", false)).toBe("READY_FOR_CLIENT");
|
||||
});
|
||||
|
||||
// ── IN_PROGRESS ───────────────────────────────────────────────────────────
|
||||
|
||||
it("returns IN_PROGRESS when a task is TODO", () => {
|
||||
expect(deriveShotStatus(tasks("TODO"), "PENDING", false)).toBe("IN_PROGRESS");
|
||||
});
|
||||
|
||||
it("returns IN_PROGRESS when a task is IN_PROGRESS", () => {
|
||||
expect(deriveShotStatus(tasks("IN_PROGRESS"), "PENDING", false)).toBe("IN_PROGRESS");
|
||||
});
|
||||
|
||||
it("returns IN_PROGRESS when tasks are mixed TODO and DONE", () => {
|
||||
expect(deriveShotStatus(tasks("TODO", "DONE"), "PENDING", false)).toBe("IN_PROGRESS");
|
||||
});
|
||||
|
||||
it("returns IN_PROGRESS for INTERNAL_REVIEW task status (task in internal review)", () => {
|
||||
// Task is in INTERNAL_REVIEW — not TODO/IN_PROGRESS/CHANGES → falls through to INTERNAL_REVIEW shot status
|
||||
expect(deriveShotStatus(tasks("INTERNAL_REVIEW"), "PENDING", false)).toBe("INTERNAL_REVIEW");
|
||||
});
|
||||
|
||||
// ── REVISIONS (highest priority) ─────────────────────────────────────────
|
||||
|
||||
it("returns REVISIONS when any task is CHANGES", () => {
|
||||
expect(deriveShotStatus(tasks("CHANGES"), "PENDING", false)).toBe("REVISIONS");
|
||||
});
|
||||
|
||||
it("returns REVISIONS even when some tasks are DONE", () => {
|
||||
expect(deriveShotStatus(tasks("DONE", "CHANGES"), "PENDING", false)).toBe("REVISIONS");
|
||||
});
|
||||
|
||||
it("returns REVISIONS even when shot is INTERNALLY_APPROVED", () => {
|
||||
// If somehow a task is in CHANGES after approval, REVISIONS wins
|
||||
expect(deriveShotStatus(tasks("CHANGES"), "INTERNALLY_APPROVED", false)).toBe("REVISIONS");
|
||||
});
|
||||
|
||||
it("returns REVISIONS even when shot is CLIENT_APPROVED", () => {
|
||||
expect(deriveShotStatus(tasks("CHANGES"), "CLIENT_APPROVED", false)).toBe("REVISIONS");
|
||||
});
|
||||
|
||||
// ── INTERNAL_REVIEW ───────────────────────────────────────────────────────
|
||||
|
||||
it("returns INTERNAL_REVIEW when all tasks are DONE and approval is PENDING", () => {
|
||||
expect(deriveShotStatus(tasks("DONE", "DONE"), "PENDING", false)).toBe("INTERNAL_REVIEW");
|
||||
});
|
||||
|
||||
it("returns INTERNAL_REVIEW when tasks are INTERNAL_REVIEW/CLIENT_REVIEW and approval is PENDING", () => {
|
||||
expect(deriveShotStatus(tasks("INTERNAL_REVIEW", "CLIENT_REVIEW"), "PENDING", false)).toBe("INTERNAL_REVIEW");
|
||||
});
|
||||
|
||||
it("returns INTERNAL_REVIEW when all tasks are DONE, regardless of sharedWithClient", () => {
|
||||
// sharedWithClient has no effect when approval is still PENDING
|
||||
expect(deriveShotStatus(tasks("DONE"), "PENDING", true)).toBe("INTERNAL_REVIEW");
|
||||
});
|
||||
|
||||
// ── READY_FOR_CLIENT ──────────────────────────────────────────────────────
|
||||
|
||||
it("returns READY_FOR_CLIENT when internally approved and not shared", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "INTERNALLY_APPROVED", false)).toBe("READY_FOR_CLIENT");
|
||||
});
|
||||
|
||||
it("returns READY_FOR_CLIENT when internally approved and not shared (no tasks)", () => {
|
||||
expect(deriveShotStatus([], "INTERNALLY_APPROVED", false)).toBe("READY_FOR_CLIENT");
|
||||
});
|
||||
|
||||
// ── CLIENT_REVIEW ─────────────────────────────────────────────────────────
|
||||
|
||||
it("returns CLIENT_REVIEW when internally approved and shared", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "INTERNALLY_APPROVED", true)).toBe("CLIENT_REVIEW");
|
||||
});
|
||||
|
||||
it("returns CLIENT_REVIEW when internally approved, shared, and tasks still in progress", () => {
|
||||
// Approval field takes precedence over task statuses (except CHANGES)
|
||||
expect(deriveShotStatus(tasks("TODO"), "INTERNALLY_APPROVED", true)).toBe("CLIENT_REVIEW");
|
||||
});
|
||||
|
||||
// ── COMPLETE ──────────────────────────────────────────────────────────────
|
||||
|
||||
it("returns COMPLETE when client approved", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "CLIENT_APPROVED", true)).toBe("COMPLETE");
|
||||
});
|
||||
|
||||
it("returns COMPLETE when client approved even if not shared anymore", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "CLIENT_APPROVED", false)).toBe("COMPLETE");
|
||||
});
|
||||
|
||||
// ── State transition scenarios ────────────────────────────────────────────
|
||||
|
||||
describe("full workflow transitions", () => {
|
||||
it("Artist work → tasks TODO → IN_PROGRESS", () => {
|
||||
expect(deriveShotStatus(tasks("TODO"), "PENDING", false)).toBe("IN_PROGRESS");
|
||||
});
|
||||
|
||||
it("Tasks in internal review → INTERNAL_REVIEW shot status", () => {
|
||||
expect(deriveShotStatus(tasks("INTERNAL_REVIEW"), "PENDING", false)).toBe("INTERNAL_REVIEW");
|
||||
});
|
||||
|
||||
it("All tasks done → INTERNAL_REVIEW (awaiting supervisor)", () => {
|
||||
expect(deriveShotStatus(tasks("DONE", "DONE", "DONE"), "PENDING", false)).toBe("INTERNAL_REVIEW");
|
||||
});
|
||||
|
||||
it("Supervisor approves → READY_FOR_CLIENT", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "INTERNALLY_APPROVED", false)).toBe("READY_FOR_CLIENT");
|
||||
});
|
||||
|
||||
it("Producer shares → CLIENT_REVIEW", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "INTERNALLY_APPROVED", true)).toBe("CLIENT_REVIEW");
|
||||
});
|
||||
|
||||
it("Producer unshares → READY_FOR_CLIENT", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "INTERNALLY_APPROVED", false)).toBe("READY_FOR_CLIENT");
|
||||
});
|
||||
|
||||
it("Client approves → COMPLETE", () => {
|
||||
expect(deriveShotStatus(tasks("DONE"), "CLIENT_APPROVED", false)).toBe("COMPLETE");
|
||||
});
|
||||
|
||||
it("Client requests changes → approval reset to PENDING + task CHANGES → REVISIONS", () => {
|
||||
// When changes are requested: shotApprovalStatus → PENDING, sharedWithClient → false, task → CHANGES
|
||||
expect(deriveShotStatus(tasks("CHANGES"), "PENDING", false)).toBe("REVISIONS");
|
||||
});
|
||||
});
|
||||
});
|
||||
+46
-20
@@ -1,4 +1,4 @@
|
||||
import { ShotStatus, TaskStatus } from "@prisma/client";
|
||||
import { ShotStatus, ShotApprovalStatus, TaskStatus } from "@prisma/client";
|
||||
import { db } from "@/lib/db";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
@@ -8,30 +8,48 @@ type TxClient = Omit<
|
||||
>;
|
||||
|
||||
/**
|
||||
* Derive the shot status from its tasks.
|
||||
* Derive shot status from tasks + shot-level approval fields.
|
||||
*
|
||||
* Priority order (highest → lowest):
|
||||
* CHANGES on any task → REVISIONS
|
||||
* INTERNAL_REVIEW / CLIENT_REVIEW on any task → IN_REVIEW
|
||||
* TODO / IN_PROGRESS on any task → IN_PROGRESS
|
||||
* All tasks DONE → COMPLETE
|
||||
* No tasks → WAITING
|
||||
* 1. REVISIONS — any task is CHANGES
|
||||
* 2. COMPLETE — shotApprovalStatus === CLIENT_APPROVED
|
||||
* 3. CLIENT_REVIEW — shotApprovalStatus === INTERNALLY_APPROVED && sharedWithClient
|
||||
* 4. READY_FOR_CLIENT — shotApprovalStatus === INTERNALLY_APPROVED && !sharedWithClient
|
||||
* 5. IN_PROGRESS — any task is TODO or IN_PROGRESS
|
||||
* 6. INTERNAL_REVIEW — tasks exist but none blocking, approval still PENDING
|
||||
* 7. WAITING — no tasks
|
||||
*/
|
||||
export function deriveShotStatus(
|
||||
tasks: { status: TaskStatus }[]
|
||||
tasks: { status: TaskStatus }[],
|
||||
shotApprovalStatus: ShotApprovalStatus,
|
||||
sharedWithClient: boolean
|
||||
): ShotStatus {
|
||||
if (tasks.length === 0) return "WAITING";
|
||||
// 1. Changes requested — highest priority
|
||||
if (tasks.some((t) => t.status === "CHANGES")) return "REVISIONS";
|
||||
if (tasks.some((t) => t.status === "INTERNAL_REVIEW" || t.status === "CLIENT_REVIEW"))
|
||||
return "IN_REVIEW";
|
||||
if (tasks.some((t) => t.status === "TODO" || t.status === "IN_PROGRESS"))
|
||||
|
||||
// 2. Client approved — terminal completion state
|
||||
if (shotApprovalStatus === "CLIENT_APPROVED") return "COMPLETE";
|
||||
|
||||
// 3 & 4. Internally approved — client-facing states
|
||||
if (shotApprovalStatus === "INTERNALLY_APPROVED") {
|
||||
return sharedWithClient ? "CLIENT_REVIEW" : "READY_FOR_CLIENT";
|
||||
}
|
||||
|
||||
// 5. Active work in progress (approval still PENDING)
|
||||
if (tasks.some((t) => t.status === "TODO" || t.status === "IN_PROGRESS")) {
|
||||
return "IN_PROGRESS";
|
||||
if (tasks.every((t) => t.status === "DONE")) return "COMPLETE";
|
||||
}
|
||||
|
||||
// 6. All work done, awaiting internal approval
|
||||
if (tasks.length > 0) return "INTERNAL_REVIEW";
|
||||
|
||||
// 7. No tasks yet
|
||||
return "WAITING";
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all tasks for a shot, derive the correct ShotStatus, and persist it.
|
||||
* Accepts an optional Prisma transaction client (tx) for use inside transactions.
|
||||
* Query all tasks + approval fields for a shot, derive the correct ShotStatus,
|
||||
* and persist it. Accepts an optional Prisma transaction client (tx).
|
||||
*/
|
||||
export async function recalcShotStatus(
|
||||
shotId: string,
|
||||
@@ -39,12 +57,20 @@ export async function recalcShotStatus(
|
||||
): Promise<void> {
|
||||
const client = tx ?? db;
|
||||
|
||||
const tasks = await client.task.findMany({
|
||||
where: { shotId },
|
||||
select: { status: true },
|
||||
});
|
||||
const [tasks, shot] = await Promise.all([
|
||||
client.task.findMany({
|
||||
where: { shotId },
|
||||
select: { status: true },
|
||||
}),
|
||||
client.shot.findUnique({
|
||||
where: { id: shotId },
|
||||
select: { shotApprovalStatus: true, sharedWithClient: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const newStatus = deriveShotStatus(tasks);
|
||||
if (!shot) return;
|
||||
|
||||
const newStatus = deriveShotStatus(tasks, shot.shotApprovalStatus, shot.sharedWithClient);
|
||||
|
||||
await client.shot.update({
|
||||
where: { id: shotId },
|
||||
|
||||
Reference in New Issue
Block a user