using EntitlementBLL.Exceptions; using EntitlementBLL.Interfaces; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; namespace EntitlementBLL.Implementations; public class LicenseBLL : ILicenseBLL { private readonly ILicenseDAL _LicenseDAL; private readonly ISubscriptionDAL _SubscriptionDAL; private readonly IQueryExecutor _QueryExecutor; public LicenseBLL(ILicenseDAL licenseDAL, ISubscriptionDAL subscriptionDAL, IQueryExecutor queryExecutor) { _LicenseDAL = licenseDAL; _SubscriptionDAL = subscriptionDAL; _QueryExecutor = queryExecutor; } public async Task> GetSeatsAsync(int subscriptionId, LoginDTO login, CancellationToken ct) => await _LicenseDAL.GetBySubscriptionAsync(subscriptionId, login, ct).ConfigureAwait(false); public async Task> GetSeatsByClientAsync(int clientId, LoginDTO login, CancellationToken ct) { var sub = await _SubscriptionDAL.GetByClientAsync(clientId, login, ct).ConfigureAwait(false) ?? throw new EntitlementNotFoundException($"No subscription found for ClientId {clientId}."); return await _LicenseDAL.GetBySubscriptionAsync(sub.SubscriptionId, login, ct).ConfigureAwait(false); } public async Task UpdateSeatsAsync(int subscriptionId, byte licenseType, int seats, LoginDTO login, CancellationToken ct) { if (seats < 0) throw new ArgumentException("Seats cannot be negative.", nameof(seats)); var tx = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { GB5Trace.Step("validate-license-seats", new { subscriptionId, licenseType, seats }); var existing = await _LicenseDAL.GetBySubscriptionAndTypeAsync(subscriptionId, licenseType, login, ct).ConfigureAwait(false); GB5Trace.Step("save-license-seats", new { subscriptionId, licenseType, seats, isNew = existing is null }); if (existing is null) { await _LicenseDAL.SaveAsync(new LicenseDTO { // LicenseId is app-generated elsewhere (Provisioning seeds the initial rows); // ad-hoc creation here uses a deterministic composite fallback only when no // seed row exists yet — SubscriptionId*100 + LicenseType keeps it unique and // avoids a second AutoNumber round-trip on the hot admin-seat-update path. LicenseId = subscriptionId * 100 + licenseType, SubscriptionId = subscriptionId, LicenseType = licenseType, Seats = seats, ExpiryDate = new DateTime(9999, 12, 31) }, login, tx, ct).ConfigureAwait(false); } else { await _LicenseDAL.UpdateSeatsAsync(existing.LicenseId, seats, login, tx, ct).ConfigureAwait(false); } await _QueryExecutor.CommitAsync(tx).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { await _QueryExecutor.RollbackAsync(tx).ConfigureAwait(false); GB5Trace.MarkFailed("update-license-seats-failed", ex); throw; } } public async Task> GetUtilisationAsync(int clientId, LoginDTO login, CancellationToken ct) => await _LicenseDAL.GetUtilisationByClientAsync(clientId, login, ct).ConfigureAwait(false); }