Files
CapaKraken/packages/application/src/__tests__/estimate.test.ts
T
Hartmut cd78f72f33 chore: full technical rename planarchy → capakraken
Complete rename of all technical identifiers across the codebase:

Package names (11 packages):
- @planarchy/* → @capakraken/* in all package.json, tsconfig, imports

Import statements: 277 files, 548 occurrences replaced

Database & Docker:
- PostgreSQL user/db: planarchy → capakraken
- Docker volumes: planarchy_pgdata → capakraken_pgdata
- Connection strings updated in docker-compose, .env, CI

CI/CD:
- GitHub Actions workflow: all filter commands updated
- Test database credentials updated

Infrastructure:
- Redis channel: planarchy:sse → capakraken:sse
- Logger service name: planarchy-api → capakraken-api
- Anonymization seed updated
- Start/stop/restart scripts updated

Test data:
- Seed emails: @planarchy.dev → @capakraken.dev
- E2E test credentials: all 11 spec files updated
- Email defaults: @planarchy.app → @capakraken.app
- localStorage keys: planarchy_* → capakraken_*

Documentation: 30+ .md files updated

Verification:
- pnpm install: workspace resolution works
- TypeScript: only pre-existing TS2589 (no new errors)
- Engine: 310/310 tests pass
- Staffing: 37/37 tests pass

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-27 13:18:09 +01:00

1111 lines
31 KiB
TypeScript

import {
AllocationStatus,
EstimateExportFormat,
EstimateStatus,
EstimateVersionStatus,
} from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import {
approveEstimateVersion,
createEstimate,
createEstimateExport,
createEstimatePlanningHandoff,
createEstimateRevision,
listEstimates,
submitEstimateVersion,
} from "../index.js";
describe("estimate use-cases", () => {
it("creates an estimate with an initial version payload", async () => {
const create = vi.fn().mockResolvedValue({ id: "est_1", versions: [{ id: "ver_1" }] });
const db = {
estimate: {
create,
},
};
const result = await createEstimate(db as never, {
name: "CGI Estimate",
baseCurrency: "EUR",
status: EstimateStatus.DRAFT,
assumptions: [
{
category: "commercial",
key: "pricingStructure",
label: "Pricing Structure",
valueType: "string",
value: "fixed-bid",
sortOrder: 0,
},
],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
});
expect(result).toEqual({ id: "est_1", versions: [{ id: "ver_1" }] });
expect(create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
name: "CGI Estimate",
latestVersionNumber: 1,
versions: {
create: expect.objectContaining({
versionNumber: 1,
assumptions: {
create: [
expect.objectContaining({
key: "pricingStructure",
value: "fixed-bid",
}),
],
},
}),
},
}),
}),
);
});
it("lists estimates with optional query filters", async () => {
const findMany = vi.fn().mockResolvedValue([]);
const db = {
estimate: {
findMany,
},
};
await listEstimates(db as never, {
status: EstimateStatus.DRAFT,
query: "cgi",
});
expect(findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
status: EstimateStatus.DRAFT,
OR: expect.any(Array),
}),
}),
);
});
it("submits the working estimate version and moves the estimate into review", async () => {
const estimateId = "est_1";
const workingVersionId = "ver_working";
const existing = {
id: estimateId,
status: EstimateStatus.DRAFT,
latestVersionNumber: 1,
versions: [
{
id: workingVersionId,
versionNumber: 1,
status: EstimateVersionStatus.WORKING,
lockedAt: null,
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [],
projectSnapshot: {},
},
{
id: "ver_old_submit",
versionNumber: 0,
status: EstimateVersionStatus.SUBMITTED,
lockedAt: new Date("2026-03-01"),
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [],
projectSnapshot: {},
},
],
};
const refreshed = {
...existing,
status: EstimateStatus.IN_REVIEW,
versions: [
{
...existing.versions[0],
status: EstimateVersionStatus.SUBMITTED,
lockedAt: new Date("2026-03-13"),
},
{
...existing.versions[1],
status: EstimateVersionStatus.SUPERSEDED,
},
],
};
const findUnique = vi.fn().mockResolvedValueOnce(existing).mockResolvedValueOnce(refreshed);
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
const updateVersion = vi.fn().mockResolvedValue({});
const updateEstimate = vi.fn().mockResolvedValue({});
const db = {
estimate: {
findUnique,
update: updateEstimate,
},
estimateVersion: {
updateMany,
update: updateVersion,
},
$transaction: vi.fn(async (callback) =>
callback({
estimateVersion: {
updateMany,
update: updateVersion,
},
estimate: {
update: updateEstimate,
},
}),
),
};
const result = await submitEstimateVersion(db as never, { estimateId });
expect(result.status).toBe(EstimateStatus.IN_REVIEW);
expect(updateMany).toHaveBeenCalledWith({
where: { id: { in: ["ver_old_submit"] } },
data: { status: EstimateVersionStatus.SUPERSEDED },
});
expect(updateVersion).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: workingVersionId },
data: expect.objectContaining({
status: EstimateVersionStatus.SUBMITTED,
lockedAt: expect.any(Date),
}),
}),
);
expect(updateEstimate).toHaveBeenCalledWith({
where: { id: estimateId },
data: { status: EstimateStatus.IN_REVIEW },
});
});
it("creates a new working revision cloned from the selected locked version", async () => {
const estimateId = "est_2";
const sourceScopeId = "scope_old";
const existing = {
id: estimateId,
status: EstimateStatus.APPROVED,
latestVersionNumber: 2,
versions: [
{
id: "ver_approved",
versionNumber: 2,
label: "Approved",
status: EstimateVersionStatus.APPROVED,
lockedAt: new Date("2026-03-13"),
notes: "approved baseline",
projectSnapshot: { shortCode: "PRJ" },
assumptions: [
{
id: "assumption_1",
category: "commercial",
key: "pricing",
label: "Pricing",
valueType: "string",
value: "fixed",
sortOrder: 0,
notes: null,
},
],
scopeItems: [
{
id: sourceScopeId,
sequenceNo: 1,
scopeType: "SHOT",
packageCode: null,
name: "Shot 010",
description: null,
scene: null,
page: null,
location: null,
assumptionCategory: null,
technicalSpec: {},
frameCount: null,
itemCount: null,
unitMode: null,
internalComments: null,
externalComments: null,
sortOrder: 0,
metadata: {},
},
],
demandLines: [
{
id: "line_1",
scopeItemId: sourceScopeId,
roleId: null,
resourceId: null,
lineType: "LABOR",
name: "Comp",
chapter: null,
hours: 40,
days: null,
fte: null,
rateSource: null,
costRateCents: 5000,
billRateCents: 8000,
currency: "EUR",
costTotalCents: 200000,
priceTotalCents: 320000,
monthlySpread: {},
staffingAttributes: {},
metadata: {},
},
],
resourceSnapshots: [
{
id: "snap_1",
resourceId: null,
sourceEid: null,
displayName: "Alex Artist",
chapter: null,
roleId: null,
currency: "EUR",
lcrCents: 5000,
ucrCents: 8000,
fte: null,
location: null,
country: null,
level: null,
workType: null,
attributes: {},
},
],
metrics: [
{
id: "metric_1",
key: "total_hours",
label: "Total Hours",
metricGroup: "summary",
valueDecimal: 40,
valueCents: null,
currency: null,
metadata: {},
},
],
exports: [],
},
],
};
const refreshed = {
...existing,
status: EstimateStatus.DRAFT,
latestVersionNumber: 3,
versions: [
{
...existing.versions[0],
},
{
id: "ver_new",
versionNumber: 3,
label: "Revision 3",
status: EstimateVersionStatus.WORKING,
lockedAt: null,
notes: "Revision created from v2",
projectSnapshot: { shortCode: "PRJ" },
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [],
},
],
};
const findUnique = vi.fn().mockResolvedValueOnce(existing).mockResolvedValueOnce(refreshed);
const createVersion = vi.fn().mockResolvedValue({ id: "ver_new" });
const createAssumptions = vi.fn().mockResolvedValue({ count: 1 });
const createScopeItem = vi.fn().mockResolvedValue({ id: "scope_new" });
const createDemandLines = vi.fn().mockResolvedValue({ count: 1 });
const createSnapshots = vi.fn().mockResolvedValue({ count: 1 });
const createMetrics = vi.fn().mockResolvedValue({ count: 1 });
const updateEstimate = vi.fn().mockResolvedValue({});
const db = {
estimate: {
findUnique,
update: updateEstimate,
},
estimateVersion: {
create: createVersion,
},
estimateAssumption: {
createMany: createAssumptions,
},
scopeItem: {
create: createScopeItem,
},
estimateDemandLine: {
createMany: createDemandLines,
},
resourceCostSnapshot: {
createMany: createSnapshots,
},
estimateMetric: {
createMany: createMetrics,
},
$transaction: vi.fn(async (callback) =>
callback({
estimateVersion: {
create: createVersion,
},
estimateAssumption: {
createMany: createAssumptions,
},
scopeItem: {
create: createScopeItem,
},
estimateDemandLine: {
createMany: createDemandLines,
},
resourceCostSnapshot: {
createMany: createSnapshots,
},
estimateMetric: {
createMany: createMetrics,
},
estimate: {
update: updateEstimate,
},
}),
),
};
const result = await createEstimateRevision(db as never, { estimateId });
expect(result.latestVersionNumber).toBe(3);
expect(createVersion).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
estimateId,
versionNumber: 3,
status: EstimateVersionStatus.WORKING,
}),
}),
);
expect(createDemandLines).toHaveBeenCalledWith({
data: [
expect.objectContaining({
estimateVersionId: "ver_new",
scopeItemId: "scope_new",
name: "Comp",
}),
],
});
expect(updateEstimate).toHaveBeenCalledWith({
where: { id: estimateId },
data: {
latestVersionNumber: 3,
status: EstimateStatus.DRAFT,
},
});
});
it("creates an export artifact for the selected estimate version", async () => {
const createdAt = new Date("2026-03-13T08:00:00.000Z");
const findUnique = vi
.fn()
.mockResolvedValueOnce({
id: "est_3",
name: "CGI Estimate",
baseCurrency: "EUR",
createdAt,
updatedAt: createdAt,
status: EstimateStatus.APPROVED,
latestVersionNumber: 1,
project: {
id: "project_1",
shortCode: "CGI-001",
name: "CGI Project",
status: "ACTIVE",
},
versions: [
{
id: "ver_1",
versionNumber: 1,
status: EstimateVersionStatus.APPROVED,
lockedAt: new Date("2026-03-13"),
createdAt,
updatedAt: createdAt,
assumptions: [],
scopeItems: [],
demandLines: [
{
id: "line_1",
scopeItemId: null,
roleId: "role_1",
resourceId: "resource_1",
lineType: "LABOR",
name: "Comp Artist",
chapter: "Compositing",
hours: 40,
days: 5,
fte: 1,
rateSource: "resource",
costRateCents: 5000,
billRateCents: 8000,
currency: "EUR",
costTotalCents: 200000,
priceTotalCents: 320000,
monthlySpread: { "2026-03": 40 },
staffingAttributes: {},
metadata: {},
createdAt,
updatedAt: createdAt,
},
],
resourceSnapshots: [],
metrics: [
{
id: "metric_1",
key: "total_hours",
label: "Total Hours",
metricGroup: "summary",
valueDecimal: 40,
valueCents: null,
currency: null,
metadata: {},
createdAt,
updatedAt: createdAt,
},
],
exports: [],
projectSnapshot: {
startDate: "2026-03-01T00:00:00.000Z",
endDate: "2026-04-01T00:00:00.000Z",
},
},
],
})
.mockResolvedValueOnce({
id: "est_3",
name: "CGI Estimate",
baseCurrency: "EUR",
createdAt,
updatedAt: createdAt,
status: EstimateStatus.APPROVED,
latestVersionNumber: 1,
project: {
id: "project_1",
shortCode: "CGI-001",
name: "CGI Project",
status: "ACTIVE",
},
versions: [
{
id: "ver_1",
versionNumber: 1,
status: EstimateVersionStatus.APPROVED,
lockedAt: new Date("2026-03-13"),
createdAt,
updatedAt: createdAt,
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [
{
id: "exp_1",
format: EstimateExportFormat.JSON,
fileName: "cgi-estimate-v1.json",
payload: {},
},
],
projectSnapshot: {},
},
],
});
const createExport = vi.fn().mockResolvedValue({ id: "exp_1" });
const db = {
estimate: {
findUnique,
},
estimateExport: {
create: createExport,
},
};
await createEstimateExport(db as never, {
estimateId: "est_3",
format: EstimateExportFormat.JSON,
});
expect(createExport).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
estimateVersionId: "ver_1",
format: EstimateExportFormat.JSON,
fileName: "cgi-estimate-v1.json",
payload: expect.objectContaining({
format: EstimateExportFormat.JSON,
encoding: "utf8",
mimeType: "application/json; charset=utf-8",
content: expect.stringContaining('"estimateId": "est_3"'),
summary: expect.objectContaining({
estimateId: "est_3",
projectName: "CGI Project",
totalHours: 40,
demandLineCount: 1,
}),
}),
}),
}),
);
});
it("approves the submitted estimate version and marks the estimate approved", async () => {
const findUnique = vi
.fn()
.mockResolvedValueOnce({
id: "est_4",
status: EstimateStatus.IN_REVIEW,
latestVersionNumber: 2,
versions: [
{
id: "ver_submit",
versionNumber: 2,
status: EstimateVersionStatus.SUBMITTED,
lockedAt: new Date("2026-03-13"),
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [],
projectSnapshot: {},
},
{
id: "ver_approved_old",
versionNumber: 1,
status: EstimateVersionStatus.APPROVED,
lockedAt: new Date("2026-03-01"),
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [],
projectSnapshot: {},
},
],
})
.mockResolvedValueOnce({
id: "est_4",
status: EstimateStatus.APPROVED,
latestVersionNumber: 2,
versions: [],
});
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
const updateVersion = vi.fn().mockResolvedValue({});
const updateEstimate = vi.fn().mockResolvedValue({});
const db = {
estimate: {
findUnique,
update: updateEstimate,
},
estimateVersion: {
updateMany,
update: updateVersion,
},
$transaction: vi.fn(async (callback) =>
callback({
estimateVersion: {
updateMany,
update: updateVersion,
},
estimate: {
update: updateEstimate,
},
}),
),
};
const result = await approveEstimateVersion(db as never, {
estimateId: "est_4",
});
expect(result.status).toBe(EstimateStatus.APPROVED);
expect(updateMany).toHaveBeenCalledWith({
where: { id: { in: ["ver_approved_old"] } },
data: { status: EstimateVersionStatus.SUPERSEDED },
});
expect(updateVersion).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: "ver_submit" },
data: expect.objectContaining({
status: EstimateVersionStatus.APPROVED,
}),
}),
);
});
it("creates planning allocations from an approved estimate version", async () => {
const estimate = {
id: "est_plan",
projectId: "project_1",
status: EstimateStatus.APPROVED,
latestVersionNumber: 2,
versions: [
{
id: "ver_approved",
versionNumber: 2,
status: EstimateVersionStatus.APPROVED,
lockedAt: new Date("2026-03-13"),
assumptions: [],
scopeItems: [],
demandLines: [
{
id: "line_resource",
scopeItemId: null,
roleId: "role_comp",
resourceId: "resource_1",
lineType: "LABOR",
name: "Senior Comp",
chapter: "Comp",
hours: 80,
days: 10,
fte: 1,
rateSource: "RESOURCE",
costRateCents: 5000,
billRateCents: 9000,
currency: "EUR",
costTotalCents: 400000,
priceTotalCents: 720000,
monthlySpread: {},
staffingAttributes: {},
metadata: {},
},
{
id: "line_placeholder",
scopeItemId: null,
roleId: "role_fx",
resourceId: null,
lineType: "LABOR",
name: "FX Artist",
chapter: "FX",
hours: 160,
days: 20,
fte: 2,
rateSource: "ROLE",
costRateCents: 4500,
billRateCents: 8000,
currency: "EUR",
costTotalCents: 720000,
priceTotalCents: 1280000,
monthlySpread: {},
staffingAttributes: {},
metadata: {},
},
],
resourceSnapshots: [],
metrics: [],
exports: [],
projectSnapshot: {},
},
],
};
const project = {
id: "project_1",
shortCode: "PRJ",
name: "Project One",
status: "ACTIVE",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-27"),
orderType: "CHARGEABLE",
allocationType: "EXT",
winProbability: 100,
budgetCents: 0,
responsiblePerson: null,
};
const estimateFindUnique = vi.fn().mockResolvedValue(estimate);
const projectFindUnique = vi.fn().mockResolvedValue(project);
const resourceFindMany = vi.fn().mockResolvedValue([
{
id: "resource_1",
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
},
]);
const allocationFindMany = vi.fn().mockResolvedValue([]);
const demandRequirementFindMany = vi.fn().mockResolvedValue([]);
const demandRequirementCreate = vi
.fn()
.mockResolvedValueOnce({
id: "demand_resource",
projectId: "project_1",
startDate: project.startDate,
endDate: project.endDate,
hoursPerDay: 8,
percentage: 100,
role: "Compositor",
roleId: "role_comp",
headcount: 1,
status: AllocationStatus.PROPOSED,
metadata: {},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
})
.mockResolvedValueOnce({
id: "demand_placeholder",
projectId: "project_1",
startDate: project.startDate,
endDate: project.endDate,
hoursPerDay: 4,
percentage: 50,
role: "FX Artist",
roleId: "role_fx",
headcount: 2,
status: AllocationStatus.PROPOSED,
metadata: {},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" },
});
const demandRequirementUpdate = vi.fn().mockResolvedValue({});
const resourceFindUnique = vi.fn().mockResolvedValue({
id: "resource_1",
lcrCents: 5000,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
});
const assignmentFindMany = vi.fn().mockResolvedValue([]);
const assignmentCreate = vi.fn().mockResolvedValue({
id: "assignment_1",
projectId: "project_1",
demandRequirementId: "demand_resource",
resourceId: "resource_1",
startDate: project.startDate,
endDate: project.endDate,
hoursPerDay: 8,
percentage: 100,
role: "Compositor",
roleId: "role_comp",
dailyCostCents: 40000,
status: AllocationStatus.PROPOSED,
metadata: {},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000 },
project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
demandRequirement: {
id: "demand_resource",
projectId: "project_1",
startDate: project.startDate,
endDate: project.endDate,
hoursPerDay: 8,
percentage: 100,
role: "Compositor",
roleId: "role_comp",
headcount: 1,
status: AllocationStatus.PROPOSED,
},
});
const assignmentUpdate = vi.fn().mockResolvedValue({});
const auditLogCreate = vi.fn().mockResolvedValue({});
const vacationFindMany = vi.fn().mockResolvedValue([]);
const db = {
estimate: {
findUnique: estimateFindUnique,
},
project: {
findUnique: projectFindUnique,
},
allocation: {
findMany: allocationFindMany,
},
demandRequirement: {
findMany: demandRequirementFindMany,
},
assignment: {
findMany: assignmentFindMany,
},
resource: {
findMany: resourceFindMany,
},
$transaction: vi.fn(async (callback) =>
callback({
project: {
findUnique: projectFindUnique,
},
resource: {
findUnique: resourceFindUnique,
},
allocation: {
findMany: allocationFindMany,
},
demandRequirement: {
create: demandRequirementCreate,
findUnique: vi.fn().mockResolvedValue({
id: "demand_resource",
projectId: "project_1",
}),
update: demandRequirementUpdate,
},
assignment: {
findMany: assignmentFindMany,
create: assignmentCreate,
update: assignmentUpdate,
},
auditLog: {
create: auditLogCreate,
},
vacation: {
findMany: vacationFindMany,
},
}),
),
};
const result = await createEstimatePlanningHandoff(db as never, {
estimateId: "est_plan",
});
expect(result).toMatchObject({
estimateId: "est_plan",
estimateVersionId: "ver_approved",
projectId: "project_1",
createdCount: 2,
assignedCount: 1,
placeholderCount: 1,
fallbackPlaceholderCount: 0,
});
expect(demandRequirementCreate).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
data: expect.objectContaining({
roleId: "role_comp",
headcount: 1,
metadata: expect.objectContaining({
estimateHandoff: expect.objectContaining({
estimateDemandLineId: "line_resource",
handoffMode: "resource",
}),
}),
}),
}),
);
expect(demandRequirementCreate).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
data: expect.objectContaining({
roleId: "role_fx",
headcount: 2,
metadata: expect.objectContaining({
estimateHandoff: expect.objectContaining({
estimateDemandLineId: "line_placeholder",
handoffMode: "placeholder",
}),
}),
}),
}),
);
expect(assignmentCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
demandRequirementId: "demand_resource",
resourceId: "resource_1",
roleId: "role_comp",
metadata: expect.objectContaining({
estimateHandoff: expect.objectContaining({
estimateDemandLineId: "line_resource",
handoffMode: "resource",
}),
}),
}),
}),
);
expect(demandRequirementUpdate).not.toHaveBeenCalled();
expect(assignmentUpdate).not.toHaveBeenCalled();
});
it("blocks duplicate planning handoff for the same approved version", async () => {
const estimateFindUnique = vi.fn().mockResolvedValue({
id: "est_plan_dup",
projectId: "project_dup",
status: EstimateStatus.APPROVED,
latestVersionNumber: 1,
versions: [
{
id: "ver_dup",
versionNumber: 1,
status: EstimateVersionStatus.APPROVED,
lockedAt: new Date("2026-03-13"),
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [],
projectSnapshot: {},
},
],
});
const projectFindUnique = vi.fn().mockResolvedValue({
id: "project_dup",
shortCode: "PRJ",
name: "Project Dup",
status: "ACTIVE",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-27"),
orderType: "CHARGEABLE",
allocationType: "EXT",
winProbability: 100,
budgetCents: 0,
responsiblePerson: null,
});
const demandRequirementFindMany = vi.fn().mockResolvedValue([
{
id: "demand_existing",
projectId: "project_dup",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-27"),
hoursPerDay: 8,
percentage: 100,
role: "Developer",
roleId: null,
headcount: 1,
status: "ACTIVE",
metadata: { estimateHandoff: { estimateVersionId: "ver_dup" } },
createdAt: new Date(),
updatedAt: new Date(),
},
]);
const assignmentFindMany = vi.fn().mockResolvedValue([]);
const db = {
estimate: {
findUnique: estimateFindUnique,
},
project: {
findUnique: projectFindUnique,
},
demandRequirement: {
findMany: demandRequirementFindMany,
},
assignment: {
findMany: assignmentFindMany,
},
resource: {
findMany: vi.fn().mockResolvedValue([]),
},
};
await expect(
createEstimatePlanningHandoff(db as never, { estimateId: "est_plan_dup" }),
).rejects.toThrow("Planning handoff already exists for this approved version");
});
it("blocks duplicate planning handoff when only an explicit assignment row remains", async () => {
const estimateFindUnique = vi.fn().mockResolvedValue({
id: "est_plan_assignment_dup",
projectId: "project_dup",
status: EstimateStatus.APPROVED,
latestVersionNumber: 1,
versions: [
{
id: "ver_dup_assignment",
versionNumber: 1,
status: EstimateVersionStatus.APPROVED,
lockedAt: new Date("2026-03-13"),
assumptions: [],
scopeItems: [],
demandLines: [],
resourceSnapshots: [],
metrics: [],
exports: [],
projectSnapshot: {},
},
],
});
const projectFindUnique = vi.fn().mockResolvedValue({
id: "project_dup",
shortCode: "PRJ",
name: "Project Dup",
status: "ACTIVE",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-27"),
orderType: "CHARGEABLE",
allocationType: "EXT",
winProbability: 100,
budgetCents: 0,
responsiblePerson: null,
});
const db = {
estimate: {
findUnique: estimateFindUnique,
},
project: {
findUnique: projectFindUnique,
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
id: "assignment_existing",
demandRequirementId: null,
resourceId: "resource_1",
projectId: "project_dup",
startDate: new Date("2026-03-16"),
endDate: new Date("2026-03-27"),
hoursPerDay: 8,
percentage: 100,
role: "Comp",
roleId: "role_comp",
dailyCostCents: 32000,
status: AllocationStatus.PROPOSED,
metadata: {
estimateHandoff: {
estimateVersionId: "ver_dup_assignment",
},
},
createdAt: new Date("2026-03-13"),
updatedAt: new Date("2026-03-13"),
},
]),
},
resource: {
findMany: vi.fn().mockResolvedValue([]),
},
};
await expect(
createEstimatePlanningHandoff(db as never, {
estimateId: "est_plan_assignment_dup",
}),
).rejects.toThrow("Planning handoff already exists for this approved version");
});
});