import { describe, expect, it, vi } from "vitest"; import { createEstimate } from "../use-cases/estimate/create-estimate.js"; import { cloneEstimate } from "../use-cases/estimate/clone-estimate.js"; import { listEstimates } from "../use-cases/estimate/list-estimates.js"; import { submitEstimateVersion, approveEstimateVersion, createEstimateRevision, } from "../use-cases/estimate/version-actions.js"; // --------------------------------------------------------------------------- // Shared fixtures // --------------------------------------------------------------------------- const BASE_VERSION = { id: "ver_1", estimateId: "est_1", versionNumber: 1, label: "v1", status: "WORKING", lockedAt: null, notes: null, projectSnapshot: {}, createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-01"), assumptions: [], scopeItems: [], demandLines: [], resourceSnapshots: [], metrics: [], exports: [], }; const BASE_ESTIMATE = { id: "est_1", name: "Test Estimate", projectId: "proj_1", opportunityId: null, baseCurrency: "USD", status: "DRAFT", latestVersionNumber: 1, createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-01"), project: { id: "proj_1", shortCode: "TP", name: "Test Project", status: "ACTIVE", startDate: new Date(), endDate: new Date(), }, versions: [BASE_VERSION], }; // --------------------------------------------------------------------------- // createEstimate // --------------------------------------------------------------------------- describe("createEstimate", () => { function makeDb(createdEstimate = BASE_ESTIMATE) { return { project: { findUnique: vi.fn().mockResolvedValue({ id: "proj_1", shortCode: "TP", name: "Test Project", status: "ACTIVE", startDate: new Date("2026-01-01"), endDate: new Date("2026-12-31"), orderType: "FIXED", allocationType: "FULL", winProbability: null, budgetCents: null, responsiblePerson: null, }), }, estimate: { create: vi.fn().mockResolvedValue(createdEstimate), }, }; } const minimalInput = { projectId: "proj_1", name: "My Estimate", baseCurrency: "USD", status: "DRAFT" as const, assumptions: [], scopeItems: [], demandLines: [], resourceSnapshots: [], metrics: [], }; it("creates an estimate and returns the full detail object", async () => { const db = makeDb(); const result = await createEstimate(db as never, minimalInput); expect(db.estimate.create).toHaveBeenCalledOnce(); expect(result.id).toBe("est_1"); expect(result.name).toBe("Test Estimate"); }); it("passes projectId to the estimate.create call", async () => { const db = makeDb(); await createEstimate(db as never, minimalInput); const createData = db.estimate.create.mock.calls[0][0].data; expect(createData.projectId).toBe("proj_1"); }); it("returns an estimate when no projectId is provided", async () => { const db = makeDb({ ...BASE_ESTIMATE, projectId: null as never, project: null as never }); db.project.findUnique.mockResolvedValue(null); const inputWithoutProject = { ...minimalInput, projectId: undefined }; const result = await createEstimate(db as never, inputWithoutProject); expect(result).toBeDefined(); expect(db.estimate.create).toHaveBeenCalledOnce(); }); }); // --------------------------------------------------------------------------- // cloneEstimate // --------------------------------------------------------------------------- describe("cloneEstimate", () => { const SOURCE_VERSION = { ...BASE_VERSION, id: "ver_src", status: "WORKING", lockedAt: new Date("2026-01-15"), }; const SOURCE_ESTIMATE = { ...BASE_ESTIMATE, id: "est_src", name: "Original", versions: [SOURCE_VERSION], }; const CLONED_ESTIMATE = { ...BASE_ESTIMATE, id: "est_clone", name: "Copy of Original", status: "DRAFT", }; function makeDb(source = SOURCE_ESTIMATE as typeof SOURCE_ESTIMATE | null) { return { project: { findUnique: vi.fn().mockResolvedValue({ id: "proj_1", shortCode: "TP", name: "Test Project", status: "ACTIVE", startDate: new Date("2026-01-01"), endDate: new Date("2026-12-31"), orderType: "FIXED", allocationType: "FULL", winProbability: null, budgetCents: null, responsiblePerson: null, }), }, estimate: { findUnique: vi.fn().mockResolvedValue(source), create: vi.fn().mockResolvedValue(CLONED_ESTIMATE), }, }; } it("clones an estimate with a default name", async () => { const db = makeDb(); const result = await cloneEstimate(db as never, { sourceEstimateId: "est_src" }); expect(db.estimate.create).toHaveBeenCalledOnce(); const createData = db.estimate.create.mock.calls[0][0].data; expect(createData.name).toBe("Copy of Original"); expect(result.id).toBe("est_clone"); }); it("uses the provided name override", async () => { const db = makeDb(); await cloneEstimate(db as never, { sourceEstimateId: "est_src", name: "Custom Clone" }); const createData = db.estimate.create.mock.calls[0][0].data; expect(createData.name).toBe("Custom Clone"); }); it("throws when the source estimate does not exist", async () => { const db = makeDb(null); await expect(cloneEstimate(db as never, { sourceEstimateId: "nonexistent" })).rejects.toThrow( "Source estimate not found", ); }); it("throws when the source estimate has no versions", async () => { const db = makeDb({ ...SOURCE_ESTIMATE, versions: [] }); await expect(cloneEstimate(db as never, { sourceEstimateId: "est_src" })).rejects.toThrow( "Source estimate has no versions", ); }); }); // --------------------------------------------------------------------------- // listEstimates // --------------------------------------------------------------------------- describe("listEstimates", () => { const LIST_ITEMS = [ { ...BASE_ESTIMATE, project: { id: "proj_1", shortCode: "TP", name: "Test Project", status: "ACTIVE" }, versions: [ { id: "ver_1", versionNumber: 1, label: "v1", status: "WORKING", updatedAt: new Date() }, ], }, ]; function makeDb(items = LIST_ITEMS) { return { estimate: { findMany: vi.fn().mockResolvedValue(items), }, }; } it("returns all estimates when no filters are given", async () => { const db = makeDb(); const result = await listEstimates(db as never); expect(db.estimate.findMany).toHaveBeenCalledOnce(); expect(result).toHaveLength(1); }); it("returns an empty array when there are no estimates", async () => { const db = makeDb([]); const result = await listEstimates(db as never, {}); expect(result).toHaveLength(0); }); it("passes projectId filter to findMany", async () => { const db = makeDb(); await listEstimates(db as never, { projectId: "proj_1" }); const where = db.estimate.findMany.mock.calls[0][0].where; expect(where.projectId).toBe("proj_1"); }); it("passes status filter to findMany", async () => { const db = makeDb([]); await listEstimates(db as never, { status: "APPROVED" as never }); const where = db.estimate.findMany.mock.calls[0][0].where; expect(where.status).toBe("APPROVED"); }); it("builds an OR query when a search query is provided", async () => { const db = makeDb(); await listEstimates(db as never, { query: "alpha" }); const where = db.estimate.findMany.mock.calls[0][0].where; expect(where.OR).toBeDefined(); expect(where.OR).toHaveLength(2); }); }); // --------------------------------------------------------------------------- // submitEstimateVersion // --------------------------------------------------------------------------- describe("submitEstimateVersion", () => { function makeTransactionDb( estimateBeforeUpdate = BASE_ESTIMATE, estimateAfterUpdate = BASE_ESTIMATE, ) { const txMock = { estimateVersion: { updateMany: vi.fn().mockResolvedValue({}), update: vi.fn().mockResolvedValue({}), }, estimate: { update: vi.fn().mockResolvedValue({}), }, }; return { estimate: { findUnique: vi .fn() .mockResolvedValueOnce(estimateBeforeUpdate) .mockResolvedValueOnce(estimateAfterUpdate), }, $transaction: vi .fn() .mockImplementation((cb: (tx: typeof txMock) => Promise) => cb(txMock)), }; } const WORKING_ESTIMATE = { ...BASE_ESTIMATE, versions: [{ ...BASE_VERSION, status: "WORKING" }], }; const SUBMITTED_ESTIMATE = { ...BASE_ESTIMATE, status: "IN_REVIEW", versions: [{ ...BASE_VERSION, status: "SUBMITTED" }], }; it("transitions a WORKING version to SUBMITTED and returns updated estimate", async () => { const db = makeTransactionDb(WORKING_ESTIMATE, SUBMITTED_ESTIMATE); const result = await submitEstimateVersion(db as never, { estimateId: "est_1" }); expect(db.$transaction).toHaveBeenCalledOnce(); expect(result.status).toBe("IN_REVIEW"); }); it("throws when the estimate does not exist", async () => { const db = { estimate: { findUnique: vi.fn().mockResolvedValue(null) }, $transaction: vi.fn(), }; await expect(submitEstimateVersion(db as never, { estimateId: "nonexistent" })).rejects.toThrow( "Estimate not found", ); }); it("throws when there is no WORKING version", async () => { const estimateNoWorking = { ...BASE_ESTIMATE, versions: [{ ...BASE_VERSION, status: "SUBMITTED" }], }; const db = { estimate: { findUnique: vi.fn().mockResolvedValue(estimateNoWorking) }, $transaction: vi.fn(), }; await expect(submitEstimateVersion(db as never, { estimateId: "est_1" })).rejects.toThrow( "Estimate has no working version", ); }); }); // --------------------------------------------------------------------------- // approveEstimateVersion // --------------------------------------------------------------------------- describe("approveEstimateVersion", () => { function makeTransactionDb( estimateBeforeUpdate = BASE_ESTIMATE, estimateAfterUpdate = BASE_ESTIMATE, ) { const txMock = { estimateVersion: { updateMany: vi.fn().mockResolvedValue({}), update: vi.fn().mockResolvedValue({}), }, estimate: { update: vi.fn().mockResolvedValue({}), }, }; return { estimate: { findUnique: vi .fn() .mockResolvedValueOnce(estimateBeforeUpdate) .mockResolvedValueOnce(estimateAfterUpdate), }, $transaction: vi .fn() .mockImplementation((cb: (tx: typeof txMock) => Promise) => cb(txMock)), }; } const SUBMITTED_ESTIMATE = { ...BASE_ESTIMATE, status: "IN_REVIEW", versions: [{ ...BASE_VERSION, status: "SUBMITTED", lockedAt: new Date("2026-01-10") }], }; const APPROVED_ESTIMATE = { ...BASE_ESTIMATE, status: "APPROVED", versions: [{ ...BASE_VERSION, status: "APPROVED", lockedAt: new Date("2026-01-10") }], }; it("transitions a SUBMITTED version to APPROVED and returns updated estimate", async () => { const db = makeTransactionDb(SUBMITTED_ESTIMATE, APPROVED_ESTIMATE); const result = await approveEstimateVersion(db as never, { estimateId: "est_1" }); expect(db.$transaction).toHaveBeenCalledOnce(); expect(result.status).toBe("APPROVED"); }); it("throws when the estimate does not exist", async () => { const db = { estimate: { findUnique: vi.fn().mockResolvedValue(null) }, $transaction: vi.fn(), }; await expect( approveEstimateVersion(db as never, { estimateId: "nonexistent" }), ).rejects.toThrow("Estimate not found"); }); it("throws when there is no SUBMITTED version", async () => { const estimateNoSubmitted = { ...BASE_ESTIMATE, versions: [{ ...BASE_VERSION, status: "WORKING" }], }; const db = { estimate: { findUnique: vi.fn().mockResolvedValue(estimateNoSubmitted) }, $transaction: vi.fn(), }; await expect(approveEstimateVersion(db as never, { estimateId: "est_1" })).rejects.toThrow( "Estimate has no submitted version", ); }); }); // --------------------------------------------------------------------------- // createEstimateRevision // --------------------------------------------------------------------------- describe("createEstimateRevision", () => { function makeTransactionDb( estimateBeforeUpdate = BASE_ESTIMATE, estimateAfterUpdate = BASE_ESTIMATE, ) { const txMock = { estimateVersion: { create: vi .fn() .mockResolvedValue({ id: "ver_new", ...BASE_VERSION, versionNumber: 2, status: "WORKING", }), }, estimateAssumption: { createMany: vi.fn().mockResolvedValue({}) }, scopeItem: { create: vi.fn().mockResolvedValue({ id: "scope_new" }) }, estimateDemandLine: { createMany: vi.fn().mockResolvedValue({}) }, resourceCostSnapshot: { createMany: vi.fn().mockResolvedValue({}) }, estimateMetric: { createMany: vi.fn().mockResolvedValue({}) }, estimate: { update: vi.fn().mockResolvedValue({}) }, }; return { estimate: { findUnique: vi .fn() .mockResolvedValueOnce(estimateBeforeUpdate) .mockResolvedValueOnce(estimateAfterUpdate), }, $transaction: vi .fn() .mockImplementation((cb: (tx: typeof txMock) => Promise) => cb(txMock)), }; } const LOCKED_VERSION = { ...BASE_VERSION, id: "ver_locked", status: "SUBMITTED", lockedAt: new Date("2026-01-10"), assumptions: [], scopeItems: [], demandLines: [], resourceSnapshots: [], metrics: [], projectSnapshot: {}, }; const APPROVED_ESTIMATE = { ...BASE_ESTIMATE, status: "APPROVED", latestVersionNumber: 1, versions: [LOCKED_VERSION], }; const REVISED_ESTIMATE = { ...BASE_ESTIMATE, status: "DRAFT", latestVersionNumber: 2, versions: [ { ...BASE_VERSION, id: "ver_new", versionNumber: 2, status: "WORKING" }, LOCKED_VERSION, ], }; it("creates a new WORKING revision from a locked version", async () => { const db = makeTransactionDb(APPROVED_ESTIMATE, REVISED_ESTIMATE); const result = await createEstimateRevision(db as never, { estimateId: "est_1" }); expect(db.$transaction).toHaveBeenCalledOnce(); expect(result.latestVersionNumber).toBe(2); expect(result.status).toBe("DRAFT"); }); it("throws when the estimate does not exist", async () => { const db = { estimate: { findUnique: vi.fn().mockResolvedValue(null) }, $transaction: vi.fn(), }; await expect( createEstimateRevision(db as never, { estimateId: "nonexistent" }), ).rejects.toThrow("Estimate not found"); }); it("throws when a WORKING version already exists", async () => { const db = { estimate: { findUnique: vi.fn().mockResolvedValue({ ...BASE_ESTIMATE, versions: [{ ...BASE_VERSION, status: "WORKING" }], }), }, $transaction: vi.fn(), }; await expect(createEstimateRevision(db as never, { estimateId: "est_1" })).rejects.toThrow( "Estimate already has a working version", ); }); it("throws when there is no locked version to revise", async () => { const db = { estimate: { findUnique: vi.fn().mockResolvedValue({ ...BASE_ESTIMATE, versions: [], }), }, $transaction: vi.fn(), }; await expect(createEstimateRevision(db as never, { estimateId: "est_1" })).rejects.toThrow( "Estimate has no locked version to revise", ); }); });