using Microsoft.Extensions.Configuration; using MongoDB.Driver; using QuestionsApi.Models; using System.Text.Json; namespace QuestionsApi.Services; public static class CcsPromotionCommand { public static async Task RunAsync(string[] args, TextWriter output) { var apply = args.Contains("--apply", StringComparer.OrdinalIgnoreCase); var sourceIndex = Array.FindIndex(args, value => value.Equals("--source", StringComparison.OrdinalIgnoreCase)); var source = sourceIndex >= 0 && sourceIndex + 1 < args.Length ? args[sourceIndex + 1] : Path.Combine(AppContext.BaseDirectory, "content", "ccs"); var files = File.Exists(source) ? new[] { source } : Directory.Exists(source) ? Directory.EnumerateFiles(source, "*.json", SearchOption.AllDirectories).ToArray() : []; if (files.Length == 0) { await output.WriteLineAsync($"CCS content not found: {source}"); return 2; } var json = new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true }; var request = new CcsImportRequest(); foreach (var file in files) { var parsed = JsonSerializer.Deserialize(await File.ReadAllTextAsync(file), json) ?? throw new InvalidDataException($"Could not parse CCS content: {file}"); request.Orders.AddRange(parsed.Orders); request.Cases.AddRange(parsed.Cases); } var duplicateOrders = request.Orders.GroupBy(value => value.Code, StringComparer.OrdinalIgnoreCase) .Where(value => value.Count() > 1).Select(value => value.Key).ToList(); if (duplicateOrders.Count > 0) throw new InvalidDataException($"Duplicate CCS order codes: {string.Join(", ", duplicateOrders)}"); var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables().Build(); var mongo = new MongoDatabaseProvider(configuration); var orders = mongo.Database.GetCollection("CcsOrderCatalog"); var cases = mongo.Database.GetCollection("CcsCaseRevisions"); var existingCodes = (await orders.Find(Builders.Filter.Empty) .Project(value => value.Code).ToListAsync()).ToHashSet(StringComparer.OrdinalIgnoreCase); var errors = CcsCaseValidator.ValidateImport(request, existingCodes); if (errors.Count > 0) { foreach (var error in errors) await output.WriteLineAsync(error); return 1; } await output.WriteLineAsync($"Validated {request.Cases.Count} CCS case(s) and {request.Orders.Count} order(s)."); if (!apply) { await output.WriteLineAsync("Dry run only. Pass --apply to publish."); return 0; } foreach (var order in request.Orders) { order.Code = order.Code.Trim().ToLowerInvariant(); order.UpdatedUtc = DateTime.UtcNow; await orders.ReplaceOneAsync(value => value.Code == order.Code, order, new ReplaceOptions { IsUpsert = true }); } var created = 0; var unchanged = 0; foreach (var incoming in request.Cases) { var current = await cases.Find(value => value.ExternalId == incoming.ExternalId && value.IsCurrent) .SortByDescending(value => value.Version).FirstOrDefaultAsync(); if (current is not null && CcsStore.CaseContentDocument(current).Equals(CcsStore.CaseContentDocument(incoming))) { unchanged++; continue; } if (current is not null) await cases.UpdateOneAsync(value => value.Id == current.Id, Builders.Update.Set(value => value.IsCurrent, false)); incoming.Id = null; incoming.Version = (current?.Version ?? 0) + 1; incoming.IsCurrent = true; incoming.CreatedUtc = DateTime.UtcNow; await cases.InsertOneAsync(incoming); created++; } await output.WriteLineAsync($"Promotion complete: {created} created, {unchanged} unchanged."); return 0; } }