using PnvPanel.Domain.Common; using PnvPanel.Domain.Exceptions; namespace PnvPanel.Domain.Support; /// /// Обращение в поддержку: баг/предложение (свободная форма) либо заявка на роль (существующая роль /// или параметры новой). Текст обращения и переписка — в TicketComment, отдельной таблицей (не /// навигационная коллекция — см. конвенцию проекта на плоских сущностях, ср. TrafficSample/VpnConfig). /// Для RoleRequest заполнен либо RequestedRoleId, либо Proposed* — гарантируется отдельными фабриками, /// а не runtime-проверкой одного универсального конструктора. /// public sealed class SupportTicket : Entity { public Guid UserId { get; private set; } public TicketType Type { get; private set; } public TicketStatus Status { get; private set; } public Guid? RequestedRoleId { get; private set; } public string? ProposedRoleName { get; private set; } public int? ProposedMaxConfigs { get; private set; } public int? ProposedMaxIpLimit { get; private set; } public DateTimeOffset CreatedAt { get; private set; } private SupportTicket() { } public static SupportTicket CreateBugReport(Guid userId) { return new SupportTicket { Id = Guid.NewGuid(), UserId = userId, Type = TicketType.BugReport, Status = TicketStatus.Open, CreatedAt = DateTimeOffset.UtcNow, }; } public static SupportTicket CreateRoleRequestForExistingRole(Guid userId, Guid roleId) { return new SupportTicket { Id = Guid.NewGuid(), UserId = userId, Type = TicketType.RoleRequest, Status = TicketStatus.Open, RequestedRoleId = roleId, CreatedAt = DateTimeOffset.UtcNow, }; } public static SupportTicket CreateRoleRequestForNewRole(Guid userId, string name, int maxConfigs, int maxIpLimit) { return new SupportTicket { Id = Guid.NewGuid(), UserId = userId, Type = TicketType.RoleRequest, Status = TicketStatus.Open, ProposedRoleName = name, ProposedMaxConfigs = maxConfigs, ProposedMaxIpLimit = maxIpLimit, CreatedAt = DateTimeOffset.UtcNow, }; } /// Решено (в т.ч. заявка на роль одобрена — роль выдаётся оркестрацией на уровне Application). public void Resolve() { if (Status != TicketStatus.Open) throw new DomainException($"Нельзя перевести в Resolved тикет в статусе {Status}."); Status = TicketStatus.Resolved; } /// Финальное состояние — обратного пути нет (в т.ч. заявка на роль отклонена). public void Close() { if (Status == TicketStatus.Closed) throw new DomainException("Тикет уже закрыт."); Status = TicketStatus.Closed; } /// Только из Resolved — Closed финален и не переоткрывается. public void Reopen() { if (Status != TicketStatus.Resolved) throw new DomainException("Переоткрыть можно только решённый тикет."); Status = TicketStatus.Open; } }