130 lines
6.0 KiB
C#
130 lines
6.0 KiB
C#
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;
|
||
}
|
||
}
|