import { describe, expect, it } from "vitest"; import { CreateRateCardSchema, CreateRateCardLineSchema, UpdateRateCardSchema, } from "../schemas/rate-card.schema.js"; import { CreateAllocationBaseSchema, CreateDemandRequirementBaseSchema, CreateAssignmentBaseSchema, CreateAllocationSchema, CreateDemandRequirementSchema, CreateAssignmentSchema, FillDemandRequirementSchema, ShiftProjectSchema, UpdateAllocationHoursSchema, } from "../schemas/allocation.schema.js"; import { CreateEstimateSchema, EstimateDemandLineSchema, ScopeItemSchema, EstimateAssumptionSchema, ResourceCostSnapshotSchema, EstimateMetricSchema, EstimateExportSummarySchema, } from "../schemas/estimate.schema.js"; import { AllocationStatus, EstimateStatus, EstimateVersionStatus, EstimateExportFormat } from "../types/enums.js"; // ─── Rate Card Schemas ────────────────────────────────────────────────────── describe("CreateRateCardSchema", () => { it("accepts valid input with defaults", () => { const result = CreateRateCardSchema.parse({ name: "Standard Rates" }); expect(result.name).toBe("Standard Rates"); expect(result.currency).toBe("EUR"); expect(result.lines).toEqual([]); }); it("accepts full input with lines and clientId", () => { const result = CreateRateCardSchema.parse({ name: "Client Rate Card", currency: "USD", clientId: "client-1", effectiveFrom: "2026-01-01", effectiveTo: "2026-12-31", source: "Import", lines: [{ costRateCents: 5000 }], }); expect(result.clientId).toBe("client-1"); expect(result.lines).toHaveLength(1); expect(result.effectiveFrom).toBeInstanceOf(Date); }); it("rejects empty name", () => { expect(() => CreateRateCardSchema.parse({ name: "" })).toThrow(); }); it("rejects invalid currency length", () => { expect(() => CreateRateCardSchema.parse({ name: "Test", currency: "EURO" }), ).toThrow(); }); }); describe("CreateRateCardLineSchema", () => { it("accepts minimal input", () => { const result = CreateRateCardLineSchema.parse({ costRateCents: 8000 }); expect(result.costRateCents).toBe(8000); expect(result.attributes).toEqual({}); }); it("accepts full input with all optional fields", () => { const result = CreateRateCardLineSchema.parse({ roleId: "role-1", chapter: "3D", location: "Munich", seniority: "Senior", workType: "Production", serviceGroup: "CGI", costRateCents: 8000, billRateCents: 12000, machineRateCents: 500, attributes: { tier: "A" }, }); expect(result.chapter).toBe("3D"); expect(result.billRateCents).toBe(12000); }); it("rejects negative cost rate", () => { expect(() => CreateRateCardLineSchema.parse({ costRateCents: -1 })).toThrow(); }); it("rejects non-integer cost rate", () => { expect(() => CreateRateCardLineSchema.parse({ costRateCents: 100.5 })).toThrow(); }); }); describe("UpdateRateCardSchema", () => { it("accepts partial updates", () => { const result = UpdateRateCardSchema.parse({ name: "Updated" }); expect(result.name).toBe("Updated"); expect(result.currency).toBeUndefined(); }); it("accepts nullable clientId", () => { const result = UpdateRateCardSchema.parse({ clientId: null }); expect(result.clientId).toBeNull(); }); it("accepts nullable effectiveFrom", () => { const result = UpdateRateCardSchema.parse({ effectiveFrom: null }); expect(result.effectiveFrom).toBeNull(); }); }); // ─── Allocation Schemas ───────────────────────────────────────────────────── describe("CreateDemandRequirementSchema", () => { const validDemand = { projectId: "proj-1", startDate: "2026-03-01", endDate: "2026-03-31", hoursPerDay: 8, percentage: 100, }; it("accepts valid demand requirement", () => { const result = CreateDemandRequirementSchema.parse(validDemand); expect(result.projectId).toBe("proj-1"); expect(result.headcount).toBe(1); expect(result.status).toBe(AllocationStatus.PROPOSED); expect(result.metadata).toEqual({}); }); it("rejects end date before start date", () => { expect(() => CreateDemandRequirementSchema.parse({ ...validDemand, startDate: "2026-03-31", endDate: "2026-03-01", }), ).toThrow("End date must be after start date"); }); it("rejects hoursPerDay > 24", () => { expect(() => CreateDemandRequirementBaseSchema.parse({ ...validDemand, hoursPerDay: 25 }), ).toThrow(); }); it("rejects percentage > 100", () => { expect(() => CreateDemandRequirementBaseSchema.parse({ ...validDemand, percentage: 101 }), ).toThrow(); }); }); describe("CreateAssignmentSchema", () => { const validAssignment = { resourceId: "res-1", projectId: "proj-1", startDate: "2026-03-01", endDate: "2026-03-31", hoursPerDay: 8, percentage: 100, }; it("accepts valid assignment", () => { const result = CreateAssignmentSchema.parse(validAssignment); expect(result.resourceId).toBe("res-1"); expect(result.status).toBe(AllocationStatus.PROPOSED); }); it("requires resourceId", () => { const { resourceId: _, ...withoutResource } = validAssignment; expect(() => CreateAssignmentBaseSchema.parse(withoutResource)).toThrow(); }); it("rejects end date before start date", () => { expect(() => CreateAssignmentSchema.parse({ ...validAssignment, startDate: "2026-03-31", endDate: "2026-03-01", }), ).toThrow("End date must be after start date"); }); }); describe("FillDemandRequirementSchema", () => { it("accepts valid fill input", () => { const result = FillDemandRequirementSchema.parse({ demandRequirementId: "demand-1", resourceId: "res-1", }); expect(result.demandRequirementId).toBe("demand-1"); expect(result.hoursPerDay).toBeUndefined(); }); it("rejects hoursPerDay below 0.5", () => { expect(() => FillDemandRequirementSchema.parse({ demandRequirementId: "d-1", resourceId: "r-1", hoursPerDay: 0.3, }), ).toThrow(); }); }); describe("ShiftProjectSchema", () => { it("accepts valid shift", () => { const result = ShiftProjectSchema.parse({ projectId: "proj-1", newStartDate: "2026-04-01", newEndDate: "2026-06-30", }); expect(result.projectId).toBe("proj-1"); expect(result.newStartDate).toBeInstanceOf(Date); }); it("rejects end before start", () => { expect(() => ShiftProjectSchema.parse({ projectId: "proj-1", newStartDate: "2026-06-30", newEndDate: "2026-04-01", }), ).toThrow("New end date must be after new start date"); }); }); // ─── Estimate Schemas ─────────────────────────────────────────────────────── describe("CreateEstimateSchema", () => { it("accepts minimal input", () => { const result = CreateEstimateSchema.parse({ name: "Q2 Estimate" }); expect(result.name).toBe("Q2 Estimate"); expect(result.baseCurrency).toBe("EUR"); expect(result.status).toBe(EstimateStatus.DRAFT); expect(result.assumptions).toEqual([]); expect(result.scopeItems).toEqual([]); expect(result.demandLines).toEqual([]); expect(result.resourceSnapshots).toEqual([]); expect(result.metrics).toEqual([]); }); it("rejects empty name", () => { expect(() => CreateEstimateSchema.parse({ name: "" })).toThrow(); }); }); describe("EstimateDemandLineSchema", () => { it("accepts minimal demand line", () => { const result = EstimateDemandLineSchema.parse({ name: "3D Artist", hours: 160, }); expect(result.name).toBe("3D Artist"); expect(result.hours).toBe(160); expect(result.lineType).toBe("LABOR"); expect(result.currency).toBe("EUR"); expect(result.costRateCents).toBe(0); expect(result.monthlySpread).toEqual({}); expect(result.metadata).toEqual({}); }); it("accepts full demand line with metadata", () => { const result = EstimateDemandLineSchema.parse({ name: "Senior 3D Artist", hours: 320, days: 40, fte: 1, roleId: "role-1", resourceId: "res-1", chapter: "3D Visualization", costRateCents: 8000, billRateCents: 12000, costTotalCents: 256_000_0, priceTotalCents: 384_000_0, monthlySpread: { "2026-03": 80, "2026-04": 80 }, metadata: { calculation: { costRateMode: "resource", billRateMode: "manual", }, }, }); expect(result.chapter).toBe("3D Visualization"); expect(result.monthlySpread["2026-03"]).toBe(80); expect(result.metadata.calculation?.costRateMode).toBe("resource"); }); it("rejects negative hours", () => { expect(() => EstimateDemandLineSchema.parse({ name: "Test", hours: -1 }), ).toThrow(); }); }); describe("ScopeItemSchema", () => { it("accepts valid scope item", () => { const result = ScopeItemSchema.parse({ sequenceNo: 1, scopeType: "shot", name: "Hero shot exterior", }); expect(result.sequenceNo).toBe(1); expect(result.technicalSpec).toEqual({}); expect(result.metadata).toEqual({}); }); it("rejects negative sequenceNo", () => { expect(() => ScopeItemSchema.parse({ sequenceNo: -1, scopeType: "shot", name: "Test" }), ).toThrow(); }); }); describe("ResourceCostSnapshotSchema", () => { it("accepts valid snapshot", () => { const result = ResourceCostSnapshotSchema.parse({ displayName: "John Doe", lcrCents: 6000, ucrCents: 4000, }); expect(result.displayName).toBe("John Doe"); expect(result.currency).toBe("EUR"); }); it("rejects negative LCR", () => { expect(() => ResourceCostSnapshotSchema.parse({ displayName: "Test", lcrCents: -100, ucrCents: 0, }), ).toThrow(); }); }); describe("EstimateExportSummarySchema", () => { it("accepts valid export summary", () => { const result = EstimateExportSummarySchema.parse({ estimateId: "est-1", estimateName: "Test Estimate", versionId: "ver-1", versionNumber: 1, versionStatus: EstimateVersionStatus.APPROVED, baseCurrency: "EUR", assumptionCount: 5, scopeItemCount: 10, demandLineCount: 8, resourceSnapshotCount: 3, totalHours: 1200, totalCostCents: 600_000_00, totalPriceCents: 900_000_00, marginCents: 300_000_00, marginPercent: 33.33, }); expect(result.marginPercent).toBeCloseTo(33.33); }); });