using QuestionsApi.Models; namespace QuestionsApi.Services; public static class CcsCaseValidator { private static readonly HashSet CommandTypes = ["exam", "order", "cancel_order", "location", "advance", "interval_history", "acknowledge_opening", "end_case", "exit_case"]; public static List ValidateImport(CcsImportRequest request, IReadOnlySet existingOrderCodes) { var errors = new List(); var orderCodes = existingOrderCodes.Concat(request.Orders.Select(order => order.Code.Trim())).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var order in request.Orders) { var prefix = $"order {order.Code}: "; if (string.IsNullOrWhiteSpace(order.Code)) errors.Add(prefix + "code required"); if (string.IsNullOrWhiteSpace(order.CanonicalName)) errors.Add(prefix + "canonicalName required"); if (string.IsNullOrWhiteSpace(order.Category)) errors.Add(prefix + "category required"); if (order.DefaultTurnaroundMinutes < 0) errors.Add(prefix + "defaultTurnaroundMinutes cannot be negative"); if (!string.IsNullOrWhiteSpace(order.ResultSection) && !CcsChartCategories.ResultSections.Contains(order.ResultSection)) errors.Add(prefix + "resultSection is invalid"); } if (request.Orders.Select(order => order.Code).Distinct(StringComparer.OrdinalIgnoreCase).Count() != request.Orders.Count) errors.Add("order codes must be unique within an import"); if (request.Cases.Select(item => item.ExternalId).Distinct(StringComparer.OrdinalIgnoreCase).Count() != request.Cases.Count) errors.Add("case externalIds must be unique within an import"); foreach (var item in request.Cases) { var prefix = $"case {item.ExternalId}: "; if (string.IsNullOrWhiteSpace(item.ExternalId)) errors.Add(prefix + "externalId required"); if (string.IsNullOrWhiteSpace(item.Title)) errors.Add(prefix + "title required"); if (string.IsNullOrWhiteSpace(item.System)) errors.Add(prefix + "system required"); if (string.IsNullOrWhiteSpace(item.PresentingLocation)) errors.Add(prefix + "presentingLocation required"); if (item.RealTimeMinutes is not (10 or 20)) errors.Add(prefix + "realTimeMinutes must be 10 or 20"); if (item.Difficulty is < 1 or > 5) errors.Add(prefix + "difficulty must be between 1 and 5"); if (string.IsNullOrWhiteSpace(item.InitialChart.PatientSummary) || string.IsNullOrWhiteSpace(item.InitialChart.ReasonForEncounter)) errors.Add(prefix + "initial patient summary and reason are required"); if (!item.AllowedLocations.Contains(item.PresentingLocation, StringComparer.OrdinalIgnoreCase)) errors.Add(prefix + "allowedLocations must include presentingLocation"); if (item.IsPublished) { var referenceError = QuestionValidator.ValidateReferences(item.References, true); if (referenceError is not null) errors.Add(prefix + referenceError); if (item.ScoringCriteria.Count == 0) errors.Add(prefix + "published cases require scoringCriteria"); } var examCodes = item.Exams.Select(exam => exam.Code).ToList(); if (examCodes.Any(string.IsNullOrWhiteSpace) || examCodes.Distinct(StringComparer.OrdinalIgnoreCase).Count() != examCodes.Count) errors.Add(prefix + "exam codes must be present and unique"); var eventIds = item.Events.Select(value => value.Id).ToList(); if (eventIds.Any(string.IsNullOrWhiteSpace) || eventIds.Distinct(StringComparer.OrdinalIgnoreCase).Count() != eventIds.Count) errors.Add(prefix + "event ids must be present and unique"); foreach (var response in item.OrderResponses) { if (!orderCodes.Contains(response.OrderCode)) errors.Add(prefix + $"unknown order code {response.OrderCode}"); if (!string.IsNullOrWhiteSpace(response.ResultSection) && !CcsChartCategories.ResultSections.Contains(response.ResultSection)) errors.Add(prefix + $"order response {response.OrderCode} resultSection is invalid"); } foreach (var code in item.InitialChart.InitialOrderCodes.Concat(item.Events.SelectMany(value => value.RequiresOrderCodes.Concat(value.RequiresAnyOrderCodes)))) if (!orderCodes.Contains(code)) errors.Add(prefix + $"unknown order code {code}"); var criteriaIds = item.ScoringCriteria.Select(value => value.Id).ToList(); if (criteriaIds.Any(string.IsNullOrWhiteSpace) || criteriaIds.Distinct(StringComparer.OrdinalIgnoreCase).Count() != criteriaIds.Count) errors.Add(prefix + "scoring criterion ids must be present and unique"); foreach (var criterion in item.ScoringCriteria) { if (string.IsNullOrWhiteSpace(criterion.Title)) errors.Add(prefix + $"criterion {criterion.Id} requires a title"); var targets = criterion.TargetCodes.Count > 0 ? criterion.TargetCodes : [criterion.TargetCode]; if (criterion.Points == 0) errors.Add(prefix + $"criterion {criterion.Id} points cannot be zero"); if (targets.Any(string.IsNullOrWhiteSpace)) errors.Add(prefix + $"criterion {criterion.Id} requires a target code"); foreach (var target in targets) { if (criterion.ActionType == "order" && !orderCodes.Contains(target)) errors.Add(prefix + $"criterion {criterion.Id} references unknown order {target}"); if (criterion.ActionType == "exam" && !examCodes.Contains(target, StringComparer.OrdinalIgnoreCase)) errors.Add(prefix + $"criterion {criterion.Id} references unknown exam {target}"); if (criterion.ActionType == "location" && !item.AllowedLocations.Contains(target, StringComparer.OrdinalIgnoreCase)) errors.Add(prefix + $"criterion {criterion.Id} references unknown location {target}"); } } } return errors; } public static string? ValidateCreate(CreateCcsAttemptRequest request) { if (!request.Mode.Equals(CcsAttemptModes.Timed, StringComparison.OrdinalIgnoreCase)) return "new CCS attempts must be timed"; return null; } public static string? ValidateCommand(CcsCommandRequest request) { if (string.IsNullOrWhiteSpace(request.CommandId)) return "commandId required"; if (request.ExpectedRevision < 0) return "expectedRevision cannot be negative"; if (!CommandTypes.Contains(request.Type)) return "unsupported command type"; if ((request.Type is "exam" or "order" or "cancel_order" or "location") && string.IsNullOrWhiteSpace(request.Code)) return "code required"; if (request.Type == "advance" && request.Minutes is < 1 or > 525600) return "advance minutes must be between 1 and 525600"; return null; } public static string? ValidateBatch(CcsBatchCommandRequest request) { if (string.IsNullOrWhiteSpace(request.CommandId)) return "commandId required"; if (request.ExpectedRevision < 0) return "expectedRevision cannot be negative"; if (request.Commands.Count is < 1 or > 50) return "commands must contain between 1 and 50 items"; foreach (var item in request.Commands) { var error = ValidateCommand(new CcsCommandRequest { CommandId = request.CommandId, ExpectedRevision = request.ExpectedRevision, Type = item.Type, Code = item.Code, Minutes = item.Minutes }); if (error is not null) return error; if (item.Type is not ("exam" or "interval_history" or "order" or "cancel_order")) return "batch commands support history/exams, orders, and order cancellations only"; } var hasClinicalAssessment = request.Commands.Any(value => value.Type is "exam" or "interval_history"); if (hasClinicalAssessment && request.Commands.Any(value => value.Type is not ("exam" or "interval_history"))) return "history and examinations cannot be mixed with order changes in one batch"; return null; } }