using EnablementBLL.ShowRule; using EnablementDAL.CustomCode.ShowChannelSubscription; 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.ShowChannel; public class ShowChannelSubscriptionBLL : IShowChannelSubscriptionBLL { 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 IShowChannelSubscriptionDAL _dal; private readonly IShowRuleBLL _showRuleBLL; private readonly AutoNumber _autoNumber; private readonly EventLogPublish _eventLog; private readonly ILogger? _logger; public ShowChannelSubscriptionBLL( IShowChannelSubscriptionDAL dal, IShowRuleBLL showRuleBLL, AutoNumber autoNumber, EventLogPublish eventLog, ILogger? logger = null) { _dal = dal; _showRuleBLL = showRuleBLL; _autoNumber = autoNumber; _eventLog = eventLog; _logger = logger; } public async Task> GetMyChannelsAsync(LoginDTO login, CancellationToken ct) { var channels = await _dal.GetActiveChannelsAsync(login, ct).ConfigureAwait(false); var overrides = (await _dal.GetMyOverridesAsync(login, ct).ConfigureAwait(false)) .ToDictionary(o => o.ChannelId); // Same userRoles convention as ShowResolverBLL's resolver call (default to // "Admin" if the login has no positive RoleId) — kept identical so a channel's // Audience rule authored the same way a Show's Audience rule would be behaves // identically for both callers. 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 channel in channels) { bool baseEligible = await ComputeBaseEligibilityAsync(channel, context, userRoles, login, ct).ConfigureAwait(false); overrides.TryGetValue(channel.ChannelId, out var over); bool effective = over?.IsSubscribed ?? baseEligible; bool canToggle = channel.SubscriptionType != MANDATORY && !(over?.IsLocked ?? false); result.Add(new MyChannelDTO { ChannelId = channel.ChannelId, ChannelCode = channel.ChannelCode, ChannelName = channel.ChannelName, Description = channel.Description, SubscriptionType = channel.SubscriptionType, IsSubscribed = effective, CanToggle = canToggle }); } return OkResponse(result); } private async Task ComputeBaseEligibilityAsync( ShowChannelDTO channel, ResolverContextDTO context, List userRoles, LoginDTO login, CancellationToken ct) { if (channel.SubscriptionType == MANDATORY) return true; return channel.AutoSubscribeRule switch { AUTOSUBSCRIBE_ALL => true, AUTOSUBSCRIBE_BY_ROLE or AUTOSUBSCRIBE_BY_OU => await EvaluateChannelRulesAsync(channel.ChannelId, context, userRoles, login, ct).ConfigureAwait(false), AUTOSUBSCRIBE_MANUAL_ONLY => false, _ => false }; } private async Task EvaluateChannelRulesAsync( int channelId, ResolverContextDTO context, List userRoles, LoginDTO login, CancellationToken ct) { var rules = (await _showRuleBLL.GetShowRulesForChannelAsync(channelId, 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 channel 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 channelId, bool isSubscribed, LoginDTO login, CancellationToken ct) { // Real GB5 ChannelIds are commonly large negative numbers — "<= 0" wrongly rejects // them; -1/0 are the actual "not selected" sentinels. if (channelId == 0 || channelId == -1) throw new ArgumentException("ChannelId must be selected."); var channels = await _dal.GetActiveChannelsAsync(login, ct).ConfigureAwait(false); var channel = channels.FirstOrDefault(c => c.ChannelId == channelId) ?? throw new ArgumentException($"Channel {channelId} not found or inactive."); if (channel.SubscriptionType == MANDATORY) throw new InvalidOperationException("This channel is mandatory and cannot be unsubscribed from."); var existing = await _dal.GetOverrideAsync(channelId, 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 ShowChannelSubscriptionDTO { SubscriptionId = an.StartNumber, ChannelId = channelId, 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("ShowChannelSubscription 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("ShowChannelSubscription Updated", existing, 1, existing.SubscriptionId, login, ct: ct); } _logger?.LogInformation("Channel subscription set ChannelId={ChannelId} UserId={UserId} IsSubscribed={IsSubscribed}", channelId, login.UserId, isSubscribed); return OkResponse(isSubscribed ? "Subscribed." : "Unsubscribed."); } // ---- Admin tooling (Slice 2) ---- private const byte SOURCE_SELF = 0; private const byte SOURCE_ADMIN_FORCED = 1; public async Task> GetChannelSubscriptionOverridesAsync(int channelId, LoginDTO login, CancellationToken ct) { var overrides = await _dal.GetOverridesForChannelAsync(channelId, login, ct).ConfigureAwait(false); return OkResponse(overrides); } public async Task> ForceSubscriptionAsync(ForceChannelSubscriptionDTO dto, LoginDTO login, CancellationToken ct) { // Real GB5 ChannelIds/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.ChannelId == 0 || dto.ChannelId == -1) throw new ArgumentException("ChannelId must be selected."); if (dto.UserId == 0 || dto.UserId == -1) throw new ArgumentException("UserId must be selected."); var channels = await _dal.GetActiveChannelsAsync(login, ct).ConfigureAwait(false); _ = channels.FirstOrDefault(c => c.ChannelId == dto.ChannelId) ?? throw new ArgumentException($"Channel {dto.ChannelId} not found or inactive."); var existing = await _dal.GetOverrideAsync(dto.ChannelId, 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 ShowChannelSubscriptionDTO { SubscriptionId = an.StartNumber, ChannelId = dto.ChannelId, 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("ShowChannelSubscription 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("ShowChannelSubscription AdminForced", existing, 1, existing.SubscriptionId, login, ct: ct); } _logger?.LogInformation("Admin-forced channel subscription ChannelId={ChannelId} UserId={UserId} IsSubscribed={IsSubscribed} IsLocked={IsLocked} By={AdminUserId}", dto.ChannelId, 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 }; }