diff --git a/Marco.Pms.Services/Controllers/FeatureController.cs b/Marco.Pms.Services/Controllers/FeatureController.cs index 779a4b0..f1706be 100644 --- a/Marco.Pms.Services/Controllers/FeatureController.cs +++ b/Marco.Pms.Services/Controllers/FeatureController.cs @@ -1,9 +1,11 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Entitlements; -using Marco.Pms.Model.Mapper; +using AutoMapper; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Master; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Master; +using Marco.Pms.Services.Helpers; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -15,43 +17,133 @@ namespace MarcoBMS.Services.Controllers public class FeatureController : ControllerBase { private readonly ApplicationDbContext _context; + private readonly GeneralHelper _generalHelper; + //private readonly UserHelper _userHelper; + private readonly IMapper _mapper; + private readonly ILoggingService _logger; + private readonly Guid tenantId; - - public FeatureController(ApplicationDbContext context) + public FeatureController(ApplicationDbContext context, GeneralHelper generalHelper, UserHelper userHelper, IMapper mapper, ILoggingService logger) { _context = context; + _generalHelper = generalHelper; + //_userHelper = userHelper; + _mapper = mapper; + _logger = logger; + tenantId = userHelper.GetTenantId(); } - private ICollection GetFeaturePermissionVMs(Feature model) + private ICollection GetFeaturePermissionVM(Feature model) { - ICollection features = []; - if (model.FeaturePermissions != null) + if (model.FeaturePermissions == null) { - foreach (FeaturePermission permission in model.FeaturePermissions) - { - FeaturePermissionVM item = permission.ToFeaturePermissionVMFromFeaturePermission(); - features.Add(item); - } + return []; } - return features.OrderBy(f => f.Name).ToList(); + + ICollection features = model.FeaturePermissions.Select(p => _mapper.Map(p)).OrderBy(f => f.Name).ToList(); + return features; } - [HttpGet] + [HttpGet("features")] public async Task GetAllFeatures() { - var roles = await _context.Features.Include("FeaturePermissions").Include("Module").ToListAsync(); + List featureIds = await _generalHelper.GetFeatureIdsByTenentId(tenantId); + var roles = await _context.Features + .Include(f => f.FeaturePermissions) + .Include(f => f.Module) + .Where(f => featureIds.Contains(f.Id)) + .ToListAsync(); var rolesVM = roles.Select(c => new FeatureVM() { Id = c.Id, Name = c.Name, Description = c.Description, - FeaturePermissions = GetFeaturePermissionVMs(c), + FeaturePermissions = GetFeaturePermissionVM(c), ModuleId = c.ModuleId, ModuleName = c.Module != null ? c.Module.Name : string.Empty, IsActive = c.IsActive }).OrderBy(f => f.Name).ToList(); return Ok(ApiResponse.SuccessResponse(rolesVM, "Success.", 200)); } + + /// + /// Converts FeaturePermissions from Feature entity into FeaturePermissionVM collection. + /// + /// Feature entity from DB + /// Collection of FeaturePermissionVM, ordered by Name + private ICollection GetFeaturePermissionVMs(Feature model) + { + if (model.FeaturePermissions == null || !model.FeaturePermissions.Any()) + { + _logger.LogInfo("No feature permissions found for Feature: {FeatureId}", model.Id); + return new List(); + } + + // Project and order feature permissions + var features = model.FeaturePermissions + .Select(p => _mapper.Map(p)) + .OrderBy(f => f.Name) + .ToList(); + + _logger.LogDebug("Mapped {Count} feature permissions for Feature: {FeatureId}", features.Count, model.Id); + return features; + } + + /// + /// API endpoint to fetch all features and their permissions for the given tenant. + /// + [HttpGet] + public async Task GetAllFeaturesAsync() + { + try + { + _logger.LogInfo("Fetching all features for tenant: {TenantId}", tenantId); + + // Step 1: Get tenant-specific FeatureIds + List featureIds = await _generalHelper.GetFeatureIdsByTenentIdAsync(tenantId); + if (featureIds == null || !featureIds.Any()) + { + _logger.LogWarning("No features found for tenant: {TenantId}", tenantId); + return Ok(ApiResponse.SuccessResponse(new List(), "No features found.", 200)); + } + + _logger.LogDebug("Retrieved {Count} feature IDs for tenant: {TenantId}", featureIds.Count, tenantId); + + // Step 2: Query Features with related FeaturePermissions & Module + var features = await _context.Features + .AsNoTracking() // Optimization: Read-only query + .Include(f => f.FeaturePermissions) + .Include(f => f.Module) + .Where(f => featureIds.Contains(f.Id)) + .ToListAsync(); + + _logger.LogDebug("Fetched {Count} features from DB for tenant: {TenantId}", features.Count, tenantId); + + // Step 3: Map features to ViewModels + var featureVMs = features + .Select(c => new FeatureVM + { + Id = c.Id, + Name = c.Name, + Description = c.Description, + FeaturePermissions = GetFeaturePermissionVMs(c), + ModuleId = c.ModuleId, + ModuleName = c.Module?.Name ?? string.Empty, + IsActive = c.IsActive + }) + .OrderBy(f => f.Name) + .ToList(); + + _logger.LogInfo("Returning {Count} features for tenant: {TenantId}", featureVMs.Count, tenantId); + + return Ok(ApiResponse.SuccessResponse(featureVMs, "Success.", 200)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while fetching features for tenant: {TenantId}", tenantId); + return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred.", 500)); + } + } } } diff --git a/Marco.Pms.Services/Controllers/TenantController.cs b/Marco.Pms.Services/Controllers/TenantController.cs index cb5ca10..ec6362f 100644 --- a/Marco.Pms.Services/Controllers/TenantController.cs +++ b/Marco.Pms.Services/Controllers/TenantController.cs @@ -57,13 +57,13 @@ namespace Marco.Pms.Services.Controllers UserHelper userHelper, FeatureDetailsHelper featureDetailsHelper) { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); ; - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); ; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); ; - _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); ; - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); ; - _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); ; - _featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper)); ; + _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); + _featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper)); } #region =================================================================== Tenant APIs =================================================================== diff --git a/Marco.Pms.Services/Helpers/GeneralHelper.cs b/Marco.Pms.Services/Helpers/GeneralHelper.cs index 8669811..7284490 100644 --- a/Marco.Pms.Services/Helpers/GeneralHelper.cs +++ b/Marco.Pms.Services/Helpers/GeneralHelper.cs @@ -1,4 +1,5 @@ using Marco.Pms.DataAccess.Data; +using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.MongoDBModels.Masters; using Marco.Pms.Model.MongoDBModels.Project; using MarcoBMS.Services.Service; @@ -11,13 +12,16 @@ namespace Marco.Pms.Services.Helpers private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate private readonly ILoggingService _logger; + private readonly FeatureDetailsHelper _featureDetailsHelper; public GeneralHelper(IDbContextFactory dbContextFactory, ApplicationDbContext context, - ILoggingService logger) + ILoggingService logger, + FeatureDetailsHelper featureDetailsHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper)); } public async Task> GetProjectInfraFromDB(Guid projectId) { @@ -211,5 +215,122 @@ namespace Marco.Pms.Services.Helpers return new List(); } } + + public async Task> GetFeatureIdsByTenentId(Guid tenantId) + { + var tenantSubscription = await _context.TenantSubscriptions.Include(ts => ts.Plan) + .FirstOrDefaultAsync(ts => ts.TenantId == tenantId && ts.Plan != null && !ts.IsCancelled && ts.EndDate.Date < DateTime.UtcNow.Date); + + if (tenantSubscription == null) + { + _logger.LogWarning("Cannot found the tenant subscription for tenant {TenantId}", tenantId); + return new List(); + } + var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId); + if (featureDetails == null) + { + _logger.LogWarning("Cannot found the feature details for tenant {TenantId}", tenantId); + return new List(); + } + var featureIds = new List(); + if (featureDetails.Modules!.Attendance!.Enabled) + { + featureIds.AddRange(featureDetails.Modules.Attendance.FeatureId); + } + if (featureDetails.Modules.ProjectManagement!.Enabled) + { + featureIds.AddRange(featureDetails.Modules.ProjectManagement.FeatureId); + } + if (featureDetails.Modules.Directory!.Enabled) + { + featureIds.AddRange(featureDetails.Modules.Directory.FeatureId); + } + if (featureDetails.Modules.Expense!.Enabled) + { + featureIds.AddRange(featureDetails.Modules.Expense.FeatureId); + } + + return featureIds; + } + + /// + /// Retrieves all enabled feature IDs for a given tenant based on their active subscription. + /// + /// The unique identifier of the tenant. + /// A list of feature IDs available for the tenant. + public async Task> GetFeatureIdsByTenentIdAsync(Guid tenantId) + { + try + { + _logger.LogInfo("Fetching feature IDs for tenant: {TenantId}", tenantId); + + // Step 1: Get active tenant subscription with plan + var tenantSubscription = await _context.TenantSubscriptions + .Include(ts => ts.Plan) + .AsNoTracking() // Optimization: Read-only query, no need to track + .FirstOrDefaultAsync(ts => + ts.TenantId == tenantId && + ts.Plan != null && + !ts.IsCancelled && + ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired + + if (tenantSubscription == null) + { + _logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId); + return new List(); + } + + _logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}", + tenantId, tenantSubscription.Plan!.Id); + + // Step 2: Get feature details from Plan + var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId); + + if (featureDetails == null) + { + _logger.LogWarning("No feature details found for tenant: {TenantId}, PlanId: {PlanId}", + tenantId, tenantSubscription.Plan!.Id); + return new List(); + } + + // Step 3: Collect all enabled feature IDs from modules + var featureIds = new List { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") }; + + if (featureDetails.Modules?.Attendance?.Enabled == true) + { + featureIds.AddRange(featureDetails.Modules.Attendance.FeatureId); + _logger.LogDebug("Added Attendance module features for tenant: {TenantId}", tenantId); + } + + if (featureDetails.Modules?.ProjectManagement?.Enabled == true) + { + featureIds.AddRange(featureDetails.Modules.ProjectManagement.FeatureId); + _logger.LogDebug("Added Project Management module features for tenant: {TenantId}", tenantId); + } + + if (featureDetails.Modules?.Directory?.Enabled == true) + { + featureIds.AddRange(featureDetails.Modules.Directory.FeatureId); + _logger.LogDebug("Added Directory module features for tenant: {TenantId}", tenantId); + } + + if (featureDetails.Modules?.Expense?.Enabled == true) + { + featureIds.AddRange(featureDetails.Modules.Expense.FeatureId); + _logger.LogDebug("Added Expense module features for tenant: {TenantId}", tenantId); + } + + _logger.LogInfo("Returning {Count} feature IDs for tenant: {TenantId}", featureIds.Count, tenantId); + + return featureIds.Distinct().ToList(); + } + catch (Exception ex) + { + // Step 4: Handle unexpected errors + _logger.LogError(ex, "Error retrieving feature IDs for tenant: {TenantId}", tenantId); + return new List(); + } + } + } } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index aad81de..00e008a 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -4,6 +4,7 @@ using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; @@ -221,6 +222,8 @@ namespace Marco.Pms.Services.MappingProfiles #region ======================================================= Master ======================================================= + CreateMap(); + #region ======================================================= Expenses Type Master ======================================================= CreateMap()