using EnablementBLL.ShowRule; using EnablementDAL.CustomCode.ShowSpaceSubscription; using EnablementDAL.DTO; using GB5Shared.DTO.Framework.Enum; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using Microsoft.Extensions.Logging; namespace EnablementBLL.ShowSpace; public class ShowSpaceSubscriptionBLL : IShowSpaceSubscriptionBLL { private const byte MANDATORY = 0; private const byte AUTO_SUBSCRIBED = 1; private const byte OPTIONAL = 2; private const byte AUTOSUBSCRIBE_ALL = 0; private const byte AUTOSUBSCRIBE_BY_ROLE = 1; private const byte AUTOSUBSCRIBE_BY_OU = 2; private const byte AUTOSUBSCRIBE_MANUAL_ONLY = 3; private readonly IShowSpaceSubscriptionDAL _dal; private readonly IShowRuleBLL _showRuleBLL; private readonly AutoNumber _autoNumber; private readonly EventLogPublish _eventLog; private readonly ILogger? _logger; public ShowSpaceSubscriptionBLL( IShowSpaceSubscriptionDAL dal, IShowRuleBLL showRuleBLL, AutoNumber autoNumber, EventLogPublish eventLog, ILogger? logger = null) { _dal = dal; _showRuleBLL = showRuleBLL; _autoNumber = autoNumber; _eventLog = eventLog; _logger = logger; } public async Task> GetMySpacesAsync(LoginDTO login, CancellationToken ct) { var spaces = await _dal.GetActiveSpacesAsync(login, ct).ConfigureAwait(false); var overrides = (await _dal.GetMyOverridesAsync(login, ct).ConfigureAwait(false)) .ToDictionary(o => o.SpaceId); // Same userRoles convention as ShowResolverBLL/ShowChannelSubscriptionBLL (default // to "Admin" if the login has no positive RoleId) — kept identical so a Space's // Audience rule authored the same way a Show's/Channel's Audience rule would be // behaves identically for every caller. var userRoles = new List(); if (login.RoleId > 0) userRoles.Add(login.RoleId.ToString()); if (!userRoles.Any()) userRoles.Add("Admin"); var context = ShowRuleBLL.BuildAudienceOnlyContext(login); var result = new List(); foreach (var space in spaces) { bool baseEligible = await ComputeBaseEligibilityAsync(space, context, userRoles, login, ct).ConfigureAwait(false); overrides.TryGetValue(space.SpaceId, out var over); bool effective = over?.IsSubscribed ?? baseEligible; bool canToggle = space.SubscriptionType != MANDATORY && !(over?.IsLocked ?? false); result.Add(new MySpaceDTO { SpaceId = space.SpaceId, SpaceCode = space.SpaceCode, SpaceName = space.SpaceName, Description = space.Description, Icon = space.Icon, ColorHex = space.ColorHex, SubscriptionType = space.SubscriptionType, IsSubscribed = effective, CanToggle = canToggle }); } return OkResponse(result); } private async Task ComputeBaseEligibilityAsync( ShowSpaceDTO space, ResolverContextDTO context, List userRoles, LoginDTO login, CancellationToken ct) { if (space.SubscriptionType == MANDATORY) return true; return space.AutoSubscribeRule switch { AUTOSUBSCRIBE_ALL => true, AUTOSUBSCRIBE_BY_ROLE or AUTOSUBSCRIBE_BY_OU => await EvaluateSpaceRulesAsync(space.SpaceId, context, userRoles, login, ct).ConfigureAwait(false), AUTOSUBSCRIBE_MANUAL_ONLY => false, _ => false }; } private async Task EvaluateSpaceRulesAsync( int spaceId, ResolverContextDTO context, List userRoles, LoginDTO login, CancellationToken ct) { var rules = (await _showRuleBLL.GetShowRulesForSpaceAsync(spaceId, login, ct).ConfigureAwait(false)).ToList(); // ShowRuleBLL.Evaluate()'s own "no rules ⇒ pass" convention is correct for Show // display (an unrestricted show), but wrong here: a Space configured as // ByRole/ByOU with zero rules actually authored is a misconfiguration, not "open // to everyone" — treat it as not-yet-eligible rather than silently eligible for // every user. if (!rules.Any()) return false; return _showRuleBLL.Evaluate(rules, ShowRuleBLL.AUDIENCE, context, userRoles); } public async Task> SetSubscriptionAsync(int spaceId, bool isSubscribed, LoginDTO login, CancellationToken ct) { // Real GB5 SpaceIds are commonly large negative numbers — "<= 0" wrongly rejects // them; -1/0 are the actual "not selected" sentinels. if (spaceId == 0 || spaceId == -1) throw new ArgumentException("SpaceId must be selected."); var spaces = await _dal.GetActiveSpacesAsync(login, ct).ConfigureAwait(false); var space = spaces.FirstOrDefault(s => s.SpaceId == spaceId) ?? throw new ArgumentException($"Space {spaceId} not found or inactive."); if (space.SubscriptionType == MANDATORY) throw new InvalidOperationException("This space is mandatory and cannot be unsubscribed from."); var existing = await _dal.GetOverrideAsync(spaceId, login.UserId, login, ct).ConfigureAwait(false); if (existing is not null && existing.IsLocked) throw new InvalidOperationException("This subscription has been locked by an administrator."); var now = DateTime.UtcNow; if (existing is null) { var an = await _autoNumber.GetNumberAsync(1, "SHOWSUBSCRIPTION", login).ConfigureAwait(false); var dto = new ShowSpaceSubscriptionDTO { SubscriptionId = an.StartNumber, SpaceId = spaceId, UserId = login.UserId, IsSubscribed = isSubscribed, IsLocked = false, SubscriptionSource = SOURCE_SELF, SubscribedOn = now, CreatedById = login.UserId, CreatedOn = now, ModifiedById = login.UserId, ModifiedOn = now, SourceType = login.SourceType, TenantId = login.ClientId }; await _dal.InsertOverrideAsync(dto, login, ct).ConfigureAwait(false); _ = _eventLog.PublishEventLogAsync("ShowSpaceSubscription Created", dto, 1, dto.SubscriptionId, login, ct: ct); } else { existing.IsSubscribed = isSubscribed; existing.SubscribedOn = now; existing.ModifiedById = login.UserId; existing.ModifiedOn = now; await _dal.UpdateOverrideAsync(existing, login, ct).ConfigureAwait(false); _ = _eventLog.PublishEventLogAsync("ShowSpaceSubscription Updated", existing, 1, existing.SubscriptionId, login, ct: ct); } _logger?.LogInformation("Space subscription set SpaceId={SpaceId} UserId={UserId} IsSubscribed={IsSubscribed}", spaceId, login.UserId, isSubscribed); return OkResponse(isSubscribed ? "Subscribed." : "Unsubscribed."); } // ---- Admin tooling ---- private const byte SOURCE_SELF = 0; private const byte SOURCE_ADMIN_FORCED = 1; public async Task> GetSpaceSubscriptionOverridesAsync(int spaceId, LoginDTO login, CancellationToken ct) { var overrides = await _dal.GetOverridesForSpaceAsync(spaceId, login, ct).ConfigureAwait(false); return OkResponse(overrides); } public async Task> ForceSubscriptionAsync(ForceSpaceSubscriptionDTO dto, LoginDTO login, CancellationToken ct) { // Real GB5 SpaceIds/UserIds are commonly large negative numbers (see // feedback_gb5_negative_autonumber_ids memory) — "<= 0" wrongly rejects them. // -1/0 are the actual "not selected" sentinels from the FE picklist fields. if (dto.SpaceId == 0 || dto.SpaceId == -1) throw new ArgumentException("SpaceId must be selected."); if (dto.UserId == 0 || dto.UserId == -1) throw new ArgumentException("UserId must be selected."); var spaces = await _dal.GetActiveSpacesAsync(login, ct).ConfigureAwait(false); _ = spaces.FirstOrDefault(s => s.SpaceId == dto.SpaceId) ?? throw new ArgumentException($"Space {dto.SpaceId} not found or inactive."); var existing = await _dal.GetOverrideAsync(dto.SpaceId, dto.UserId, login, ct).ConfigureAwait(false); var now = DateTime.UtcNow; if (existing is null) { var an = await _autoNumber.GetNumberAsync(1, "SHOWSUBSCRIPTION", login).ConfigureAwait(false); var newOverride = new ShowSpaceSubscriptionDTO { SubscriptionId = an.StartNumber, SpaceId = dto.SpaceId, UserId = dto.UserId, IsSubscribed = dto.IsSubscribed, IsLocked = dto.IsLocked, SubscriptionSource = SOURCE_ADMIN_FORCED, SubscribedOn = now, CreatedById = login.UserId, CreatedOn = now, ModifiedById = login.UserId, ModifiedOn = now, SourceType = login.SourceType, TenantId = login.ClientId }; await _dal.InsertOverrideAsync(newOverride, login, ct).ConfigureAwait(false); _ = _eventLog.PublishEventLogAsync("ShowSpaceSubscription AdminForced", newOverride, 1, newOverride.SubscriptionId, login, ct: ct); } else { existing.IsSubscribed = dto.IsSubscribed; existing.IsLocked = dto.IsLocked; existing.SubscriptionSource = SOURCE_ADMIN_FORCED; existing.SubscribedOn = now; existing.ModifiedById = login.UserId; existing.ModifiedOn = now; await _dal.UpdateOverrideAsync(existing, login, ct).ConfigureAwait(false); _ = _eventLog.PublishEventLogAsync("ShowSpaceSubscription AdminForced", existing, 1, existing.SubscriptionId, login, ct: ct); } _logger?.LogInformation("Admin-forced space subscription SpaceId={SpaceId} UserId={UserId} IsSubscribed={IsSubscribed} IsLocked={IsLocked} By={AdminUserId}", dto.SpaceId, dto.UserId, dto.IsSubscribed, dto.IsLocked, login.UserId); return OkResponse("Subscription updated."); } private static ResponseStandardDTO OkResponse(object body) => new() { Body = body, Status = FrameworkEnumDTO.ResponseStatus.Ok }; }