using Server.WatchBody.Core.Dal.Src.Domain; using Server.WatchBody.Core.Dal.Src.Repository; namespace Server.WatchBody.Core.Programs; /// Нарушение правил жизненного цикла программы (спецификации m03). public class ProgramStoreException(string message) : Exception(message); /// /// Единая точка работы с программами атлета (m03): версионный JSON, /// жизненный цикл draft → active → archived, «active — ровно одна». /// 🛡 Версия после создания неизменяема (меняется только статус); /// переходы только вперёд; автоматической смены активной версии нет. /// public class ProgramStore(IProgramRepository programs, IProgramVersionRepository versions) { public static readonly IReadOnlyList AllowedAuthors = ["user", "admin", "rules"]; /// Программа атлета (создаёт при отсутствии; одна на атлета). public Program EnsureProgram(int athleteId, string title) { var existing = programs.Select(athleteId).FirstOrDefault(); if (existing != null) return existing; var id = programs.Insert(new Program { AthleteId = athleteId, Title = title }); return new Program { Id = id, AthleteId = athleteId, Title = title }; } /// /// Новая draft-версия: based_on_version = актуальная максимальная /// (или явная, равная ей); version считает БД (max+1). /// public ProgramVersion CreateDraft( int athleteId, string body, string author, string rationale, int? basedOnVersion = null) { if (string.IsNullOrWhiteSpace(body)) throw new ProgramStoreException("body версии обязателен"); if (!AllowedAuthors.Contains(author)) throw new ProgramStoreException($"author '{author}' не допускается в v1 ({string.Join("/", AllowedAuthors)}; llm — v2)"); var program = EnsureProgram(athleteId, "Программа"); var latest = LatestVersion(athleteId, program.Id); if (basedOnVersion.HasValue && basedOnVersion != latest) throw new ProgramStoreException( $"based_on_version {basedOnVersion} не равна актуальной версии {latest}"); var effectiveBasedOn = basedOnVersion ?? (latest == 0 ? null : latest); var id = Guid.NewGuid(); versions.Insert(athleteId, new ProgramVersion { Id = id, ProgramId = program.Id, Version = 0, // номер считает БД (max+1) BasedOnVersion = effectiveBasedOn, Status = "draft", // выставит БД Author = author, Body = body, Rationale = rationale ?? string.Empty, }); return versions.Get(athleteId, id)!; } /// draft → active; прежняя активная → archived (гарантия БД). public void Activate(int athleteId, Guid versionId) { var version = GetOrThrow(athleteId, versionId); if (version.Status != "draft") throw new ProgramStoreException( $"активировать можно только draft-версию (текущий статус '{version.Status}')"); versions.UpdateStatus(athleteId, versionId, "active"); } /// active → archived. public void Archive(int athleteId, Guid versionId) { var version = GetOrThrow(athleteId, versionId); if (version.Status != "active") throw new ProgramStoreException( $"архивировать можно только active-версию (текущий статус '{version.Status}')"); versions.UpdateStatus(athleteId, versionId, "archived"); } /// Активная версия с body (или null). public ProgramVersion? GetActive(int athleteId) { var program = programs.Select(athleteId).FirstOrDefault(); if (program == null) return null; var meta = versions.Select(new ProgramVersionFilter { AthleteId = athleteId, ProgramId = program.Id, Status = "active", }).FirstOrDefault(); return meta == null ? null : versions.Get(athleteId, meta.Id); } /// Версии программы атлета (без body), по возрастанию номера. public IReadOnlyList ListVersions(int athleteId) { var program = programs.Select(athleteId).FirstOrDefault(); if (program == null) return []; var list = versions.Select(new ProgramVersionFilter { AthleteId = athleteId, ProgramId = program.Id, }).OrderBy(v => v.Version).ToList(); foreach (var v in list) v.Body = string.Empty; // body select не возвращает return list; } /// Версия с body; чужая/несуществующая → null. public ProgramVersion? GetVersion(int athleteId, Guid versionId) => versions.Get(athleteId, versionId); private int LatestVersion(int athleteId, int programId) => versions.Select(new ProgramVersionFilter { AthleteId = athleteId, ProgramId = programId, }).Select(v => v.Version).DefaultIfEmpty(0).Max(); private ProgramVersion GetOrThrow(int athleteId, Guid versionId) { var version = versions.Get(athleteId, versionId); if (version == null) throw new ProgramStoreException( $"версия {versionId} не найдена у атлета {athleteId} (изоляция 🛡)"); return version; } }