import { z } from "zod"; import { EstimateExportFormat, EstimateStatus, EstimateVersionStatus, } from "../types/enums.js"; const jsonRecordSchema = z.record(z.string(), z.unknown()); const numericRecordSchema = z.record(z.string(), z.number()); const demandLineRateModeSchema = z.enum(["resource", "manual"]); export const EstimateDemandLineCalculationMetadataSchema = z.object({ costRateMode: demandLineRateModeSchema.default("manual"), billRateMode: demandLineRateModeSchema.default("manual"), totalMode: z.literal("computed").default("computed"), liveCostRateCents: z.number().int().min(0).nullable().optional(), liveBillRateCents: z.number().int().min(0).nullable().optional(), liveCurrency: z.string().length(3).nullable().optional(), }); export const EstimateDemandLineMetadataSchema = z .object({ calculation: EstimateDemandLineCalculationMetadataSchema.optional(), }) .catchall(z.unknown()); export const EstimateAssumptionSchema = z.object({ id: z.string().optional(), category: z.string().min(1).max(100), key: z.string().min(1).max(100), label: z.string().min(1).max(200), valueType: z.string().min(1).max(50).default("json"), value: z.unknown(), sortOrder: z.number().int().min(0).default(0), notes: z.string().max(2_000).optional(), }); export const ScopeItemSchema = z.object({ id: z.string().optional(), sequenceNo: z.number().int().min(0), scopeType: z.string().min(1).max(100), packageCode: z.string().max(100).optional(), name: z.string().min(1).max(500), description: z.string().max(5_000).optional(), scene: z.string().max(200).optional(), page: z.string().max(100).optional(), location: z.string().max(200).optional(), assumptionCategory: z.string().max(100).optional(), technicalSpec: jsonRecordSchema.default({}), frameCount: z.number().int().min(0).optional(), itemCount: z.number().min(0).optional(), unitMode: z.string().max(100).optional(), internalComments: z.string().max(5_000).optional(), externalComments: z.string().max(5_000).optional(), sortOrder: z.number().int().min(0).default(0), metadata: jsonRecordSchema.default({}), }); export const EstimateDemandLineSchema = z.object({ id: z.string().optional(), scopeItemId: z.string().optional(), roleId: z.string().optional(), resourceId: z.string().optional(), lineType: z.string().min(1).max(100).default("LABOR"), name: z.string().min(1).max(500), chapter: z.string().max(200).optional(), hours: z.number().min(0), days: z.number().min(0).optional(), fte: z.number().min(0).optional(), rateSource: z.string().max(200).optional(), costRateCents: z.number().int().min(0).default(0), billRateCents: z.number().int().min(0).default(0), currency: z.string().length(3).default("EUR"), costTotalCents: z.number().int().min(0).default(0), priceTotalCents: z.number().int().min(0).default(0), monthlySpread: numericRecordSchema.default({}), staffingAttributes: jsonRecordSchema.default({}), metadata: EstimateDemandLineMetadataSchema.default({}), }); export const ResourceCostSnapshotSchema = z.object({ id: z.string().optional(), resourceId: z.string().optional(), sourceEid: z.string().max(100).optional(), displayName: z.string().min(1).max(500), chapter: z.string().max(200).optional(), roleId: z.string().optional(), currency: z.string().length(3).default("EUR"), lcrCents: z.number().int().min(0), ucrCents: z.number().int().min(0), fte: z.number().min(0).optional(), location: z.string().max(200).optional(), country: z.string().max(200).optional(), level: z.string().max(100).optional(), workType: z.string().max(100).optional(), attributes: jsonRecordSchema.default({}), }); export const EstimateMetricSchema = z.object({ id: z.string().optional(), key: z.string().min(1).max(100), label: z.string().min(1).max(200), metricGroup: z.string().max(100).optional(), valueDecimal: z.number(), valueCents: z.number().int().optional(), currency: z.string().length(3).optional(), metadata: jsonRecordSchema.default({}), }); export const EstimateExportSummarySchema = z.object({ estimateId: z.string(), estimateName: z.string().min(1).max(500), versionId: z.string(), versionNumber: z.number().int().min(1), versionStatus: z.nativeEnum(EstimateVersionStatus), projectId: z.string().nullable().optional(), projectName: z.string().nullable().optional(), baseCurrency: z.string().length(3), assumptionCount: z.number().int().min(0), scopeItemCount: z.number().int().min(0), demandLineCount: z.number().int().min(0), resourceSnapshotCount: z.number().int().min(0), totalHours: z.number().min(0), totalCostCents: z.number().int(), totalPriceCents: z.number().int(), marginCents: z.number().int(), marginPercent: z.number(), }); export const EstimateExportArtifactPayloadSchema = z.object({ schemaVersion: z.number().int().min(1).default(1), format: z.nativeEnum(EstimateExportFormat), mimeType: z.string().min(1).max(200), encoding: z.enum(["utf8", "base64"]), fileExtension: z.string().min(1).max(20), generatedAt: z.string().datetime(), byteLength: z.number().int().min(0), rowCount: z.number().int().min(0).nullable().optional(), lineCount: z.number().int().min(0).nullable().optional(), sheetNames: z.array(z.string().min(1).max(200)).optional(), previewText: z.string().nullable().optional(), content: z.string(), summary: EstimateExportSummarySchema, }); export const EstimateExportSchema = z.object({ id: z.string().optional(), format: z.nativeEnum(EstimateExportFormat), fileName: z.string().min(1).max(500), storageKey: z.string().max(500).optional(), payload: z.union([EstimateExportArtifactPayloadSchema, jsonRecordSchema]).optional(), }); export const EstimateVersionSchema = z.object({ id: z.string().optional(), versionNumber: z.number().int().min(1).default(1), label: z.string().max(200).optional(), status: z.nativeEnum(EstimateVersionStatus).default( EstimateVersionStatus.WORKING, ), notes: z.string().max(5_000).optional(), lockedAt: z.coerce.date().optional(), projectSnapshot: jsonRecordSchema.default({}), assumptions: z.array(EstimateAssumptionSchema).default([]), scopeItems: z.array(ScopeItemSchema).default([]), demandLines: z.array(EstimateDemandLineSchema).default([]), resourceSnapshots: z.array(ResourceCostSnapshotSchema).default([]), metrics: z.array(EstimateMetricSchema).default([]), exports: z.array(EstimateExportSchema).default([]), }); export const CreateEstimateSchema = z.object({ projectId: z.string().optional(), name: z.string().min(1).max(500), opportunityId: z.string().max(200).optional(), baseCurrency: z.string().length(3).default("EUR"), status: z.nativeEnum(EstimateStatus).default(EstimateStatus.DRAFT), versionLabel: z.string().max(200).optional(), versionNotes: z.string().max(5_000).optional(), assumptions: z.array(EstimateAssumptionSchema).default([]), scopeItems: z.array(ScopeItemSchema).default([]), demandLines: z.array(EstimateDemandLineSchema).default([]), resourceSnapshots: z.array(ResourceCostSnapshotSchema).default([]), metrics: z.array(EstimateMetricSchema).default([]), }); export const UpdateEstimateSchema = CreateEstimateSchema.partial(); export const UpdateEstimateDraftSchema = CreateEstimateSchema.partial().extend({ id: z.string(), assumptions: z.array(EstimateAssumptionSchema).default([]), scopeItems: z.array(ScopeItemSchema).default([]), demandLines: z.array(EstimateDemandLineSchema).default([]), resourceSnapshots: z.array(ResourceCostSnapshotSchema).default([]), metrics: z.array(EstimateMetricSchema).default([]), }); export const SubmitEstimateVersionSchema = z.object({ estimateId: z.string(), versionId: z.string().optional(), }); export const ApproveEstimateVersionSchema = z.object({ estimateId: z.string(), versionId: z.string().optional(), }); export const CreateEstimateRevisionSchema = z.object({ estimateId: z.string(), sourceVersionId: z.string().optional(), label: z.string().max(200).optional(), notes: z.string().max(5_000).optional(), }); export const CreateEstimateExportSchema = z.object({ estimateId: z.string(), versionId: z.string().optional(), format: z.nativeEnum(EstimateExportFormat), }); export const CreateEstimatePlanningHandoffSchema = z.object({ estimateId: z.string(), versionId: z.string().optional(), }); export const CloneEstimateSchema = z.object({ sourceEstimateId: z.string(), name: z.string().min(1).max(500).optional(), projectId: z.string().optional(), }); export const EffortUnitModeSchema = z.enum(["per_frame", "per_item", "flat"]); export const EffortRuleSchema = z.object({ id: z.string().optional(), scopeType: z.string().min(1).max(100), discipline: z.string().min(1).max(200), chapter: z.string().max(200).optional(), unitMode: EffortUnitModeSchema, hoursPerUnit: z.number().min(0), description: z.string().max(1000).optional(), sortOrder: z.number().int().min(0).default(0), }); export const CreateEffortRuleSetSchema = z.object({ name: z.string().min(1).max(200), description: z.string().max(2000).optional(), isDefault: z.boolean().default(false), rules: z.array(EffortRuleSchema).default([]), }); export const UpdateEffortRuleSetSchema = z.object({ id: z.string(), name: z.string().min(1).max(200).optional(), description: z.string().max(2000).optional(), isDefault: z.boolean().optional(), rules: z.array(EffortRuleSchema).optional(), }); export const ApplyEffortRulesSchema = z.object({ estimateId: z.string(), ruleSetId: z.string(), mode: z.enum(["replace", "append"]).default("replace"), }); export type CreateEffortRuleSetInput = z.infer; export type UpdateEffortRuleSetInput = z.infer; export type ApplyEffortRulesInput = z.infer; // ─── Experience Multipliers ────────────────────────────────────────────────── export const ExperienceMultiplierRuleSchema = z.object({ id: z.string().optional(), chapter: z.string().max(200).optional(), location: z.string().max(200).optional(), level: z.string().max(100).optional(), costMultiplier: z.number().min(0).default(1.0), billMultiplier: z.number().min(0).default(1.0), shoringRatio: z.number().min(0).max(1).optional(), additionalEffortRatio: z.number().min(0).optional(), description: z.string().max(1000).optional(), sortOrder: z.number().int().min(0).default(0), }); export const CreateExperienceMultiplierSetSchema = z.object({ name: z.string().min(1).max(200), description: z.string().max(2000).optional(), isDefault: z.boolean().default(false), rules: z.array(ExperienceMultiplierRuleSchema).default([]), }); export const UpdateExperienceMultiplierSetSchema = z.object({ id: z.string(), name: z.string().min(1).max(200).optional(), description: z.string().max(2000).optional(), isDefault: z.boolean().optional(), rules: z.array(ExperienceMultiplierRuleSchema).optional(), }); export const ApplyExperienceMultipliersSchema = z.object({ estimateId: z.string(), multiplierSetId: z.string(), }); export type CreateExperienceMultiplierSetInput = z.infer; export type UpdateExperienceMultiplierSetInput = z.infer; export type ApplyExperienceMultipliersInput = z.infer; export const EstimateListFiltersSchema = z.object({ projectId: z.string().optional(), status: z.nativeEnum(EstimateStatus).optional(), query: z.string().max(200).optional(), }); export const PhasingPatternSchema = z.enum([ "even", "front_loaded", "back_loaded", "custom", ]); export const GenerateWeeklyPhasingSchema = z.object({ estimateId: z.string(), startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), pattern: PhasingPatternSchema.default("even"), }); export const UpdateWeeklyPhasingSchema = z.object({ estimateId: z.string(), demandLineId: z.string(), weeklyHours: z.record(z.string(), z.number().min(0)), }); export type GenerateWeeklyPhasingInput = z.infer; export type UpdateWeeklyPhasingInput = z.infer; // ─── Commercial Terms ─────────────────────────────────────────────────────── export const PricingModelSchema = z.enum(["fixed_price", "time_and_materials", "hybrid"]); export const PaymentMilestoneSchema = z.object({ label: z.string().min(1).max(200), percent: z.number().min(0).max(100), dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(), description: z.string().max(1000).nullable().optional(), }); export const CommercialTermsSchema = z.object({ pricingModel: PricingModelSchema.default("fixed_price"), contingencyPercent: z.number().min(0).max(100).default(0), discountPercent: z.number().min(0).max(100).default(0), paymentTermDays: z.number().int().min(0).max(365).default(30), paymentMilestones: z.array(PaymentMilestoneSchema).default([]), warrantyMonths: z.number().int().min(0).max(60).default(0), notes: z.string().max(5_000).nullable().optional(), }); export const UpdateCommercialTermsSchema = z.object({ estimateId: z.string(), versionId: z.string().optional(), terms: CommercialTermsSchema, }); export type CommercialTermsInput = z.infer; export type UpdateCommercialTermsInput = z.infer; export type CreateEstimateInput = z.infer; export type UpdateEstimateInput = z.infer; export type UpdateEstimateDraftInput = z.infer; export type EstimateListFilters = z.infer; export type SubmitEstimateVersionInput = z.infer< typeof SubmitEstimateVersionSchema >; export type ApproveEstimateVersionInput = z.infer< typeof ApproveEstimateVersionSchema >; export type CreateEstimateRevisionInput = z.infer< typeof CreateEstimateRevisionSchema >; export type CreateEstimateExportInput = z.infer< typeof CreateEstimateExportSchema >; export type CreateEstimatePlanningHandoffInput = z.infer< typeof CreateEstimatePlanningHandoffSchema >; export type CloneEstimateInput = z.infer;