using Collaboration.Domain.Enums; namespace Collaboration.Domain.Entities; /// /// Represents a collaboration session — ad-hoc or scheduled. /// Central aggregate for all co-browse and screen share activity. /// public class CollabSession { public Guid SessionId { get; private set; } /// Short human-friendly code (e.g. "XK-4291") for quick join. public string SessionCode { get; private set; } = default!; public string Title { get; private set; } = default!; public SessionType SessionType { get; private set; } public SessionStatus Status { get; private set; } public SessionMode Mode { get; private set; } // Context — loosely coupled to any module public string? ContextModuleCode { get; private set; } // e.g. "AP", "HRMS", "CRM" public string? ContextRecordId { get; private set; } public string? ContextRecordLabel { get; private set; } // e.g. "Invoice #4521" public string? ContextRouteUrl { get; private set; } // Deep link URL // Scheduling public DateTime? ScheduledAt { get; private set; } public int? ScheduledDurationMinutes { get; private set; } // Lifecycle timestamps public DateTime? StartedAt { get; private set; } public DateTime? EndedAt { get; private set; } public DateTime? LastActivityAt { get; private set; } public DateTime CreatedAt { get; private set; } // Ownership public Guid CreatedBy { get; private set; } public Guid OrgId { get; private set; } // Configurable per session (can override org defaults) public int MaxParticipants { get; private set; } public bool AllowAnonymousJoin { get; private set; } /// Flexible JSON blob for future extensibility without schema migrations. public string? SettingsJson { get; private set; } // Navigation public IReadOnlyCollection Participants => _participants.AsReadOnly(); public IReadOnlyCollection Invites => _invites.AsReadOnly(); public IReadOnlyCollection Events => _events.AsReadOnly(); private readonly List _participants = new(); private readonly List _invites = new(); private readonly List _events = new(); private CollabSession() { } // EF constructor public static CollabSession Create( string title, SessionType sessionType, SessionMode mode, Guid createdBy, Guid orgId, int maxParticipants, string? contextModuleCode = null, string? contextRecordId = null, string? contextRecordLabel = null, string? contextRouteUrl = null, DateTime? scheduledAt = null, int? scheduledDurationMinutes = null) { var session = new CollabSession { SessionId = Guid.NewGuid(), SessionCode = GenerateSessionCode(), Title = title, SessionType = sessionType, Mode = mode, Status = sessionType == SessionType.Scheduled ? SessionStatus.Pending : SessionStatus.Active, CreatedBy = createdBy, OrgId = orgId, MaxParticipants = maxParticipants, ContextModuleCode = contextModuleCode, ContextRecordId = contextRecordId, ContextRecordLabel = contextRecordLabel, ContextRouteUrl = contextRouteUrl, ScheduledAt = scheduledAt, ScheduledDurationMinutes = scheduledDurationMinutes, AllowAnonymousJoin = false, CreatedAt = DateTime.UtcNow, LastActivityAt = DateTime.UtcNow, StartedAt = sessionType == SessionType.AdHoc ? DateTime.UtcNow : null }; session.AddEvent(SessionEventType.SessionCreated, createdBy); if (sessionType == SessionType.AdHoc) session.AddEvent(SessionEventType.SessionStarted, createdBy); return session; } public void Start(Guid triggeredBy) { if (Status != SessionStatus.Pending) throw new InvalidOperationException($"Cannot start a session with status {Status}."); Status = SessionStatus.Active; StartedAt = DateTime.UtcNow; LastActivityAt = DateTime.UtcNow; AddEvent(SessionEventType.SessionStarted, triggeredBy); } public void Pause(Guid triggeredBy) { if (Status != SessionStatus.Active) throw new InvalidOperationException("Only active sessions can be paused."); Status = SessionStatus.Paused; AddEvent(SessionEventType.SessionPaused, triggeredBy); } public void Resume(Guid triggeredBy) { if (Status != SessionStatus.Paused) throw new InvalidOperationException("Only paused sessions can be resumed."); Status = SessionStatus.Active; LastActivityAt = DateTime.UtcNow; AddEvent(SessionEventType.SessionResumed, triggeredBy); } public void End(Guid triggeredBy, bool isAbandoned = false) { if (Status == SessionStatus.Ended || Status == SessionStatus.Abandoned) return; Status = isAbandoned ? SessionStatus.Abandoned : SessionStatus.Ended; EndedAt = DateTime.UtcNow; AddEvent(isAbandoned ? SessionEventType.SessionAbandoned : SessionEventType.SessionEnded, triggeredBy); } public void ChangeMode(SessionMode newMode, Guid triggeredBy) { Mode = newMode; AddEvent(SessionEventType.ModeChanged, triggeredBy, payload: $"{{\"mode\":\"{newMode}\"}}"); } public void UpdateContext(string? moduleCode, string? recordId, string? recordLabel, string? routeUrl) { ContextModuleCode = moduleCode; ContextRecordId = recordId; ContextRecordLabel = recordLabel; ContextRouteUrl = routeUrl; LastActivityAt = DateTime.UtcNow; } public void RecordActivity() => LastActivityAt = DateTime.UtcNow; internal void AddEvent(SessionEventType eventType, Guid triggeredBy, string? payload = null) { _events.Add(CollabSessionEvent.Create(SessionId, eventType, triggeredBy, payload)); } private static string GenerateSessionCode() { // Format: XX-9999 (2 letters + 4 digits) const string letters = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // Exclude I, O (ambiguous) var rng = Random.Shared; return $"{letters[rng.Next(letters.Length)]}{letters[rng.Next(letters.Length)]}-{rng.Next(1000, 9999)}"; } }