/** * Typed Zod schemas for scenario simulation data structures. * * The scenario feature does not persist scenarioData in JSONB — it computes * everything on-the-fly from assignments, projects, and resources. These schemas * describe the shapes that flow in and out of the simulation layer so that any * deserialization (e.g. from a future scenario snapshot) can be validated at * the API boundary rather than silently returning wrong-typed data. * * TODO(scenario-snapshots): if we ever persist scenario results to a JSONB * column or a dedicated table, add `.parse()` at the DB read-boundary here. */ import { z } from "zod"; // --------------------------------------------------------------------------- // Input schemas (mirrored from scenario-procedure-support for central reference) // --------------------------------------------------------------------------- export const ScenarioChangeInputSchema = z.object({ assignmentId: z.string().optional(), resourceId: z.string().optional(), roleId: z.string().optional(), startDate: z.coerce.date(), endDate: z.coerce.date(), hoursPerDay: z.number().positive().max(24), remove: z.boolean().optional(), }); export const ScenarioSimulationRequestSchema = z.object({ projectId: z.string(), changes: z.array(ScenarioChangeInputSchema).min(1), }); // --------------------------------------------------------------------------- // Output schemas (describe the shape returned by simulateProjectScenario) // --------------------------------------------------------------------------- const ScenarioSummarySchema = z.object({ totalCostCents: z.number().int(), totalHours: z.number(), headcount: z.number().int(), skillCount: z.number().int(), }); const ScenarioResourceImpactSchema = z.object({ resourceId: z.string(), resourceName: z.string(), chargeabilityTarget: z.number().nullable(), currentUtilization: z.number(), scenarioUtilization: z.number(), utilizationDelta: z.number(), isOverallocated: z.boolean(), }); export const ScenarioSimulationResultSchema = z.object({ baseline: ScenarioSummarySchema, scenario: ScenarioSummarySchema, delta: z.object({ costCents: z.number().int(), hours: z.number(), headcount: z.number().int(), skillCoveragePct: z.number().int(), }), resourceImpacts: z.array(ScenarioResourceImpactSchema), warnings: z.array(z.string()), budgetCents: z.number().int(), }); // --------------------------------------------------------------------------- // Baseline output schema (describe the shape returned by readProjectScenarioBaseline) // --------------------------------------------------------------------------- const ScenarioBaselineAllocationSchema = z.object({ id: z.string(), resourceId: z.string().nullable(), resourceName: z.string(), resourceEid: z.string(), lcrCents: z.number().int(), roleId: z.string().nullable(), roleName: z.string(), roleColor: z.string().nullable(), startDate: z.string(), endDate: z.string(), hoursPerDay: z.number(), status: z.string(), costCents: z.number().int(), totalHours: z.number(), workingDays: z.number(), }); const ScenarioBaselineDemandSchema = z.object({ id: z.string(), roleId: z.string().nullable(), roleName: z.string(), roleColor: z.string().nullable(), startDate: z.string(), endDate: z.string(), hoursPerDay: z.number(), headcount: z.number().int(), status: z.string(), }); export const ScenarioBaselineResultSchema = z.object({ project: z.object({ id: z.string(), name: z.string(), shortCode: z.string().nullable(), startDate: z.date(), endDate: z.date(), budgetCents: z.number().int().nullable(), orderType: z.string().nullable(), }), assignments: z.array(ScenarioBaselineAllocationSchema), demands: z.array(ScenarioBaselineDemandSchema), totalCostCents: z.number().int(), totalHours: z.number(), budgetCents: z.number().int().nullable(), }); // --------------------------------------------------------------------------- // Helper: validate scenario simulation result at the API boundary // // Usage: // const result = parseScenarioSimulationResult(await simulateProjectScenario(db, input)); // --------------------------------------------------------------------------- export function parseScenarioSimulationResult( raw: unknown, ): z.infer { return ScenarioSimulationResultSchema.parse(raw); } export function parseScenarioBaselineResult( raw: unknown, ): z.infer { return ScenarioBaselineResultSchema.parse(raw); } export type ScenarioChangeInput = z.infer; export type ScenarioSimulationRequest = z.infer; export type ScenarioSimulationResult = z.infer; export type ScenarioBaselineResult = z.infer;