VEU.WathBody/3-app/2-cli/Server.WatchBody.Core/Programs/ProgramStore.cs

130 lines
6.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Server.WatchBody.Core.Dal.Src.Domain;
using Server.WatchBody.Core.Dal.Src.Repository;
namespace Server.WatchBody.Core.Programs;
/// <summary>Нарушение правил жизненного цикла программы (спецификации m03).</summary>
public class ProgramStoreException(string message) : Exception(message);
/// <summary>
/// Единая точка работы с программами атлета (m03): версионный JSON,
/// жизненный цикл draft → active → archived, «active — ровно одна».
/// 🛡 Версия после создания неизменяема (меняется только статус);
/// переходы только вперёд; автоматической смены активной версии нет.
/// </summary>
public class ProgramStore(IProgramRepository programs, IProgramVersionRepository versions)
{
public static readonly IReadOnlyList<string> AllowedAuthors = ["user", "admin", "rules"];
/// <summary>Программа атлета (создаёт при отсутствии; одна на атлета).</summary>
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 };
}
/// <summary>
/// Новая draft-версия: based_on_version = актуальная максимальная
/// (или явная, равная ей); version считает БД (max+1).
/// </summary>
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)!;
}
/// <summary>draft → active; прежняя активная → archived (гарантия БД).</summary>
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");
}
/// <summary>active → archived.</summary>
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");
}
/// <summary>Активная версия с body (или null).</summary>
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);
}
/// <summary>Версии программы атлета (без body), по возрастанию номера.</summary>
public IReadOnlyList<ProgramVersion> 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;
}
/// <summary>Версия с body; чужая/несуществующая → null.</summary>
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;
}
}