refactor(api): extract estimate read procedures
This commit is contained in:
@@ -245,6 +245,187 @@ describe("estimate router", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("listVersions", () => {
|
||||
it("returns estimate versions ordered from newest to oldest", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "est_1",
|
||||
name: "Test Estimate",
|
||||
status: EstimateStatus.DRAFT,
|
||||
latestVersionNumber: 2,
|
||||
versions: [
|
||||
{
|
||||
id: "ver_2",
|
||||
versionNumber: 2,
|
||||
label: "v2",
|
||||
status: EstimateVersionStatus.SUBMITTED,
|
||||
notes: null,
|
||||
lockedAt: new Date("2026-03-14"),
|
||||
createdAt: new Date("2026-03-14"),
|
||||
updatedAt: new Date("2026-03-14"),
|
||||
_count: {
|
||||
assumptions: 1,
|
||||
scopeItems: 2,
|
||||
demandLines: 3,
|
||||
resourceSnapshots: 4,
|
||||
exports: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
const result = await caller.listVersions({ estimateId: "est_1" });
|
||||
|
||||
expect(result.versions).toHaveLength(1);
|
||||
expect(result.versions[0]?.id).toBe("ver_2");
|
||||
expect(findUnique).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: "est_1" },
|
||||
select: expect.objectContaining({
|
||||
versions: expect.objectContaining({
|
||||
orderBy: { versionNumber: "desc" },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND when the estimate does not exist", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue(null);
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
await expect(caller.listVersions({ estimateId: "missing" })).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
code: "NOT_FOUND",
|
||||
message: "Estimate not found",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVersionSnapshot", () => {
|
||||
it("returns aggregate counts and totals for the selected version", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "est_1",
|
||||
name: "Test Estimate",
|
||||
status: EstimateStatus.DRAFT,
|
||||
baseCurrency: "EUR",
|
||||
versions: [
|
||||
{
|
||||
id: "ver_2",
|
||||
versionNumber: 2,
|
||||
label: "Revision 2",
|
||||
status: EstimateVersionStatus.SUBMITTED,
|
||||
notes: "Ready",
|
||||
lockedAt: new Date("2026-03-14"),
|
||||
createdAt: new Date("2026-03-14"),
|
||||
updatedAt: new Date("2026-03-15"),
|
||||
assumptions: [
|
||||
{ id: "a_1", category: "delivery", key: "onsite", label: "Onsite" },
|
||||
{ id: "a_2", category: "delivery", key: "travel", label: "Travel" },
|
||||
{ id: "a_3", category: "commercial", key: "buffer", label: "Buffer" },
|
||||
],
|
||||
scopeItems: [
|
||||
{ id: "s_1", scopeType: "FEATURE", sequenceNo: 1, name: "Alpha" },
|
||||
{ id: "s_2", scopeType: "FEATURE", sequenceNo: 2, name: "Beta" },
|
||||
{ id: "s_3", scopeType: "SERVICE", sequenceNo: 3, name: "Gamma" },
|
||||
],
|
||||
demandLines: [
|
||||
{
|
||||
id: "d_1",
|
||||
name: "Lead",
|
||||
chapter: "Delivery",
|
||||
hours: 10,
|
||||
costTotalCents: 100_00,
|
||||
priceTotalCents: 150_00,
|
||||
currency: "EUR",
|
||||
},
|
||||
{
|
||||
id: "d_2",
|
||||
name: "QA",
|
||||
chapter: null,
|
||||
hours: 5,
|
||||
costTotalCents: 50_00,
|
||||
priceTotalCents: 90_00,
|
||||
currency: "EUR",
|
||||
},
|
||||
],
|
||||
resourceSnapshots: [
|
||||
{
|
||||
id: "r_1",
|
||||
displayName: "Alice",
|
||||
chapter: "Delivery",
|
||||
currency: "EUR",
|
||||
lcrCents: 10_000,
|
||||
ucrCents: 15_000,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
{
|
||||
id: "x_1",
|
||||
format: EstimateExportFormat.XLSX,
|
||||
fileName: "estimate.xlsx",
|
||||
createdAt: new Date("2026-03-16"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
const result = await caller.getVersionSnapshot({ estimateId: "est_1" });
|
||||
|
||||
expect(result.counts).toEqual({
|
||||
assumptions: 3,
|
||||
scopeItems: 3,
|
||||
demandLines: 2,
|
||||
resourceSnapshots: 1,
|
||||
exports: 1,
|
||||
});
|
||||
expect(result.totals).toMatchObject({
|
||||
hours: 15,
|
||||
costTotalCents: 15000,
|
||||
priceTotalCents: 24000,
|
||||
});
|
||||
expect(result.chapterBreakdown).toEqual([
|
||||
expect.objectContaining({ chapter: "Delivery", lineCount: 1, hours: 10 }),
|
||||
expect.objectContaining({ chapter: "Unassigned", lineCount: 1, hours: 5 }),
|
||||
]);
|
||||
expect(result.scopeTypeBreakdown).toEqual([
|
||||
{ scopeType: "FEATURE", count: 2 },
|
||||
{ scopeType: "SERVICE", count: 1 },
|
||||
]);
|
||||
expect(result.assumptionCategoryBreakdown).toEqual([
|
||||
{ category: "delivery", count: 2 },
|
||||
{ category: "commercial", count: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws NOT_FOUND when no matching version can be resolved", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "est_1",
|
||||
name: "Test Estimate",
|
||||
status: EstimateStatus.DRAFT,
|
||||
baseCurrency: "EUR",
|
||||
versions: [],
|
||||
});
|
||||
const db = { estimate: { findUnique } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
await expect(
|
||||
caller.getVersionSnapshot({ estimateId: "est_1", versionId: "missing_version" }),
|
||||
).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
code: "NOT_FOUND",
|
||||
message: "Estimate version not found",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── create ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("create", () => {
|
||||
|
||||
Reference in New Issue
Block a user