using EntitlementBLL.Common; using EntitlementBLL.Interfaces; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; namespace EntitlementBLL.Implementations; public class ClientEntitlementSnapshotBLL : IClientEntitlementSnapshotBLL { private readonly ISubscriptionService _SubscriptionService; private readonly IPlanBLL _PlanBLL; private readonly IClientProvisioningDAL _ClientProvisioningDAL; private readonly IEntitlementLoginFactory _LoginFactory; public ClientEntitlementSnapshotBLL( ISubscriptionService subscriptionService, IPlanBLL planBLL, IClientProvisioningDAL clientProvisioningDAL, IEntitlementLoginFactory loginFactory) { _SubscriptionService = subscriptionService; _PlanBLL = planBLL; _ClientProvisioningDAL = clientProvisioningDAL; _LoginFactory = loginFactory; } public async Task GetSnapshotAsync(int clientId, CancellationToken ct) { // Real GB5 ClientIds are large negative AutoNumber-allocated values (e.g. -999999001), // never small positive ones — a "clientId <= 0" guard would reject every genuine client // and only let through malformed/uninitialized-int-default callers, which is exactly // backwards. Live-verified 2026-09-08: this rejected every real ClientId with a 400, // which SwBLL.Integration.EntitlementSubscriptionClient then turned into a thrown // InvalidOperationException, aborting Gate A/B rollout-campaign target resolution // entirely for any real client. Only reject the genuinely-missing/default case. if (clientId == 0) throw new ArgumentException("ClientId is required.", nameof(clientId)); var login = _LoginFactory.Create(clientId); // ISubscriptionService.GetAsync throws EntitlementNotFoundException if no subscription // exists — a real, valid Gate-A answer ("this client has no subscription at all"), not // an error condition for this caller. Surfacing it as SubscriptionStatus=0 (Pending, the // same sentinel a never-activated subscription would carry) rather than propagating the // exception keeps the snapshot endpoint a plain data read, never a 4xx/5xx for a state // Gate A needs to reason about directly. SubscriptionDto? sub; try { sub = await _SubscriptionService.GetAsync(clientId, ct).ConfigureAwait(false); } catch (EntitlementBLL.Exceptions.EntitlementNotFoundException) { sub = null; } var enabledFeatureCodes = new List(); if (sub is not null) { var planFeatures = await _PlanBLL.GetPlanFeaturesAsync(sub.PlanId, login, ct).ConfigureAwait(false); enabledFeatureCodes = planFeatures .Where(f => f.IsEnabled != 0) .Select(f => f.FeatureCode) .ToList(); } var deploymentType = await _ClientProvisioningDAL.GetClientDeploymentTypeAsync(clientId, login, ct).ConfigureAwait(false); return new ClientEntitlementSnapshotDTO { ClientId = clientId, SubscriptionStatus = sub?.SubscriptionStatus ?? 0, SupportValidTill = sub?.SupportValidTill ?? DateTime.MinValue, GracePeriodDays = sub?.GracePeriodDays ?? 0, DeploymentType = deploymentType ?? 0, // 0 = SaaS (GB5Shared.Enums.Client.ClientDeploymentType default) EnabledFeatureCodes = enabledFeatureCodes }; } }