using AutoMapper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Filters; using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.OrganizationModel; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Roles; using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels.MongoDBModel; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Tenant; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Net; using System.Text.Json; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 namespace Marco.Pms.Services.Controllers { [Route("api/[controller]")] [ApiController] [Authorize] public class TenantController : ControllerBase { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILoggingService _logger; private readonly UserManager _userManager; private readonly IMapper _mapper; private readonly UserHelper _userHelper; private readonly FeatureDetailsHelper _featureDetailsHelper; private readonly Guid tenantId; private readonly static Guid projectActiveStatus = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); private readonly static Guid projectInProgressStatus = Guid.Parse("cdad86aa-8a56-4ff4-b633-9c629057dfef"); private readonly static Guid projectOnHoldStatus = Guid.Parse("603e994b-a27f-4e5d-a251-f3d69b0498ba"); private readonly static Guid projectInActiveStatus = Guid.Parse("ef1c356e-0fe0-42df-a5d3-8daee355492d"); private readonly static Guid projectCompletedStatus = Guid.Parse("33deaef9-9af1-4f2a-b443-681ea0d04f81"); private readonly static Guid tenantActiveStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191"); private readonly static Guid activePlanStatus = Guid.Parse("cd3a68ea-41fd-42f0-bd0c-c871c7337727"); private readonly static Guid EmployeeFeatureId = Guid.Parse("81ab8a87-8ccd-4015-a917-0627cee6a100"); private readonly static string AdminRoleName = "Admin"; public TenantController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, ILoggingService logger, UserManager userManager, IMapper mapper, 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)); tenantId = userHelper.GetTenantId(); } #region =================================================================== Tenant APIs =================================================================== /// /// Retrieves a paginated list of active tenants with optional filtering and searching. /// /// A string to search for across various tenant fields. /// A JSON serialized string containing advanced filter criteria. /// The number of records to return per page. /// The page number to retrieve. /// A paginated list of tenants matching the criteria. [HttpGet("list")] public async Task GetTenantList([FromQuery] string? searchString, string? filter, int pageSize = 20, int pageNumber = 1) { using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); _logger.LogInfo("Attempting to fetch tenant list with pageNumber: {PageNumber} and pageSize: {PageSize}", pageNumber, pageSize); try { // --- 1. PERMISSION CHECK --- var currentTenant = await _userHelper.GetCurrentTenant(); if (currentTenant == null) { _logger.LogWarning("Authentication failed: No logged-in tenant found."); return StatusCode(403, ApiResponse.ErrorResponse("Authentication required", "Tenant not found", 403)); } var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { _logger.LogWarning("Authentication failed: No logged-in employee found."); return StatusCode(403, ApiResponse.ErrorResponse("Authentication required", "User is not logged in.", 403)); } // A root user should have access regardless of the specific permission. var isSuperTenant = currentTenant.IsSuperTenant; var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission && !isSuperTenant) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to list tenants without 'ManageTenants' permission or root access.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } // --- 2. QUERY CONSTRUCTION --- // Using a DbContext from the factory ensures a fresh context for this request. await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Start with a base IQueryable. Filters will be appended to this. var tenantQuery = _context.Tenants.Where(t => t.IsActive); if (hasPermission && !isSuperTenant) { tenantQuery = tenantQuery.Where(t => t.Id == currentTenant.Id || t.CreatedById == loggedInEmployee.Id); } // Apply advanced filters from the JSON filter object. var tenantFilter = TryDeserializeFilter(filter); if (tenantFilter != null) { // Date range filtering if (tenantFilter.StartDate.HasValue && tenantFilter.EndDate.HasValue) { // OPTIMIZATION: Avoid using .Date on the database column, as it can prevent index usage. // This structure (`>= start` and `< end.AddDays(1)`) is index-friendly and correctly inclusive. var endDateExclusive = tenantFilter.EndDate.Value.AddDays(1); tenantQuery = tenantQuery.Where(t => t.OnBoardingDate >= tenantFilter.StartDate.Value && t.OnBoardingDate < endDateExclusive); } // List-based filtering if (tenantFilter.IndustryIds?.Any() == true) { tenantQuery = tenantQuery.Where(t => t.IndustryId.HasValue && tenantFilter.IndustryIds.Contains(t.IndustryId.Value)); } if (tenantFilter.References?.Any() == true) { tenantQuery = tenantQuery.Where(t => tenantFilter.References.Contains(t.Reference)); } if (tenantFilter.TenantStatusIds?.Any() == true) { tenantQuery = tenantQuery.Where(t => tenantFilter.TenantStatusIds.Contains(t.TenantStatusId)); } if (tenantFilter.CreatedByIds?.Any() == true) { tenantQuery = tenantQuery.Where(t => t.CreatedById.HasValue && tenantFilter.CreatedByIds.Contains(t.CreatedById.Value)); } } // Apply free-text search string. if (!string.IsNullOrWhiteSpace(searchString)) { // OPTIMIZATION: Do not use .ToLower() on the database columns (e.g., `t.Name.ToLower()`). // This makes the query non-SARGable and kills performance by preventing index usage. // This implementation relies on the database collation being case-insensitive (e.g., `SQL_Latin1_General_CP1_CI_AS` in SQL Server). tenantQuery = tenantQuery.Where(t => t.Name.Contains(searchString) || t.ContactName.Contains(searchString) || t.Email.Contains(searchString) || t.ContactNumber.Contains(searchString) || t.BillingAddress.Contains(searchString) || (t.TaxId != null && t.TaxId.Contains(searchString)) || (t.Description != null && t.Description.Contains(searchString)) || (t.DomainName != null && t.DomainName.Contains(searchString)) ); } // --- 3. PAGINATION AND EXECUTION --- // First, get the total count of records for pagination metadata. // This executes a separate, efficient `COUNT(*)` query. int totalRecords = await tenantQuery.CountAsync(); int totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); _logger.LogInfo("Found {TotalRecords} total tenants matching the query.", totalRecords); // Now, apply ordering and pagination to fetch only the data for the current page. // This is efficient server-side pagination. var tenantList = await tenantQuery .Include(t => t.Industry) // Eager load related data to prevent N+1 queries. .Include(t => t.TenantStatus) .OrderByDescending(t => t.OnBoardingDate) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); // Map the entities to a ViewModel/DTO for the response. var vm = _mapper.Map>(tenantList); // --- 4. CONSTRUCT RESPONSE --- var response = new { TotalCount = totalRecords, TotalPages = totalPages, CurrentPage = pageNumber, PageSize = pageSize, Filter = tenantFilter, // Return the applied filter for context on the client-side. Data = vm }; _logger.LogInfo("Successfully fetched {RecordCount} tenant records.", vm.Count); return Ok(ApiResponse.SuccessResponse(response, $"{totalRecords} records of tenants fetched successfully", 200)); } catch (Exception ex) { // CRITICAL SECURITY FIX: Do not expose the exception details to the client. // Log the full exception for debugging purposes. _logger.LogError(ex, "An unhandled exception occurred while fetching the tenant list."); // Return a generic 500 Internal Server Error response. return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", "An unexpected error prevented the request from completing.", 500)); } } // GET api//5 [HttpGet("details/{id}")] public async Task GetTenantDetailsAsync(Guid id) { _logger.LogInfo("GetTenantDetails started for TenantId: {TenantId}", id); // Get currently logged-in employee info var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { _logger.LogWarning("No logged-in employee found for the request."); return StatusCode(403, ApiResponse.ErrorResponse("Unauthorized", "User must be logged in.", 403)); } // Check permissions using a single service scope to avoid overhead bool hasManagePermission, hasModifyPermission, hasViewPermission; using (var scope = _serviceScopeFactory.CreateScope()) { var manageTask = Task.Run(async () => { var permissionService = scope.ServiceProvider.GetRequiredService(); return await permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); }); var modifyTask = Task.Run(async () => { var permissionService = scope.ServiceProvider.GetRequiredService(); return await permissionService.HasPermission(PermissionsMaster.ModifyTenant, loggedInEmployee.Id); }); var viewTask = Task.Run(async () => { var permissionService = scope.ServiceProvider.GetRequiredService(); return await permissionService.HasPermission(PermissionsMaster.ViewTenant, loggedInEmployee.Id); }); await Task.WhenAll(manageTask, modifyTask, viewTask); hasManagePermission = manageTask.Result; hasModifyPermission = modifyTask.Result; hasViewPermission = viewTask.Result; } if (!hasManagePermission && !hasModifyPermission && !hasViewPermission) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details without the required permissions.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } // Create a single DbContext for main tenant fetch and related data requests await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Fetch tenant details with related Industry and TenantStatus in a single query var tenant = await _context.Tenants .Include(t => t.Industry) .Include(t => t.TenantStatus) .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == id); if (tenant == null) { _logger.LogWarning("Tenant {TenantId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Tenant not found", "Tenant not found", 404)); } _logger.LogInfo("Tenant {TenantId} found.", tenant.Id); if (!hasManagePermission && (hasModifyPermission || hasViewPermission) && tenant.OrganizationId != loggedInEmployee.OrganizationId) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } // Fetch dependent data in parallel to improve performance var employeesTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees .Include(e => e.ApplicationUser) .Where(e => e.TenantId == tenant.Id) .AsNoTracking() .ToListAsync(); }); var createdByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees .AsNoTracking() .Where(e => e.Id == tenant.CreatedById) .Select(e => _mapper.Map(e)) .FirstOrDefaultAsync(); }); var plansTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.TenantSubscriptions .Include(sp => sp.CreatedBy) .ThenInclude(e => e!.JobRole) .Include(sp => sp.UpdatedBy) .ThenInclude(e => e!.JobRole) .Include(sp => sp.Currency) .Include(ts => ts.Plan).ThenInclude(sp => sp!.Plan) .Where(ts => ts.TenantId == tenant.Id && ts.Plan != null) .AsNoTracking() .OrderBy(ts => ts.CreatedBy) .ToListAsync(); }); var projectsTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Projects .Include(p => p.ProjectStatus) .Where(p => p.TenantId == tenant.Id) .AsNoTracking() .ToListAsync(); }); await Task.WhenAll(employeesTask, createdByTask, plansTask, projectsTask); var employees = employeesTask.Result; var createdBy = createdByTask.Result; var plans = plansTask.Result; var projects = projectsTask.Result; // Calculate active/inactive employees count var activeEmployeesCount = employees.Count(e => e.IsActive); var inActiveEmployeesCount = employees.Count - activeEmployeesCount; var activeUserCount = employees.Count(e => e.IsActive && e.ApplicationUserId != null && e.Email != null); // Filter current active (non-cancelled) subscription plan var currentPlan = plans.FirstOrDefault(ts => !ts.IsCancelled); var expiryDate = currentPlan?.EndDate; var nextBillingDate = currentPlan?.NextBillingDate; // Map Tenant entity to TenantDetailsVM response model var response = _mapper.Map(tenant); response.ActiveEmployees = activeEmployeesCount; response.InActiveEmployees = inActiveEmployeesCount; // Count projects by status response.ActiveProjects = projects.Count(p => p.ProjectStatusId == projectActiveStatus); response.InProgressProjects = projects.Count(p => p.ProjectStatusId == projectInProgressStatus); response.OnHoldProjects = projects.Count(p => p.ProjectStatusId == projectOnHoldStatus); response.InActiveProjects = projects.Count(p => p.ProjectStatusId == projectInActiveStatus); response.CompletedProjects = projects.Count(p => p.ProjectStatusId == projectCompletedStatus); response.SeatsAvailable = (int)(currentPlan?.MaxUsers ?? 1) - activeUserCount; response.ExpiryDate = expiryDate; response.NextBillingDate = nextBillingDate; response.CreatedBy = createdBy; response.CurrentPlan = _mapper.Map(currentPlan); response.CurrentPlanFeatures = await _featureDetailsHelper.GetFeatureDetails(currentPlan?.Plan?.FeaturesId ?? Guid.Empty); // Map subscription history plans to DTO response.SubscriptionHistery = _mapper.Map>(plans); _logger.LogInfo("Tenant details fetched successfully for TenantId: {TenantId}", tenant.Id); return Ok(ApiResponse.SuccessResponse(response, "Tenant profile fetched successfully", 200)); } // POST api/ [HttpPost("create")] public async Task CreateTenant([FromBody] CreateTenantDto model) { using var scope = _serviceScopeFactory.CreateScope(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); var _configuration = scope.ServiceProvider.GetRequiredService(); var _emailSender = scope.ServiceProvider.GetRequiredService(); var _permissionService = scope.ServiceProvider.GetRequiredService(); _logger.LogInfo("Attempting to create a new tenant with organization name: {OrganizationName}", model.OrganizationName); // 1. --- PERMISSION CHECK --- var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { // This case should ideally be handled by an [Authorize] attribute, but it's good practice to double-check. return StatusCode(403, ApiResponse.ErrorResponse("Authentication required", "User is not logged in.", 403)); } var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission || !(loggedInEmployee.ApplicationUser?.IsRootUser ?? false)) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to create a tenant without sufficient rights.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } // 2. --- VALIDATION --- // Check if a user with the same email already exists. var existingUser = await _userManager.FindByEmailAsync(model.Email); if (existingUser != null) { _logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", model.Email); return StatusCode(409, ApiResponse.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409)); } // Check if a tenant with the same Tax ID and Domain Name already exists. var taxTask = Task.Run(async () => { if (!string.IsNullOrWhiteSpace(model.TaxId)) { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Tenants.AnyAsync(t => t.TaxId == model.TaxId); } return false; }); var domainTask = Task.Run(async () => { if (!string.IsNullOrWhiteSpace(model.DomainName)) { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Tenants.AnyAsync(t => t.DomainName == model.DomainName); } return false; }); await Task.WhenAll(taxTask, domainTask); if (taxTask.Result || domainTask.Result) { if (!string.IsNullOrWhiteSpace(model.TaxId)) { _logger.LogWarning("Tenant creation failed for Tax ID {TaxId}: a tenant with this Tax ID already exists.", model.TaxId); } if (!string.IsNullOrWhiteSpace(model.DomainName)) { _logger.LogWarning("Tenant creation failed for Domain Name {DomainName}: a tenant with this Domain Name already exists.", model.DomainName); } return StatusCode(409, ApiResponse.ErrorResponse("Tenant cannot be created", "A tenant already exists.", 409)); } // Check if the provided logo is a valid Base64 string. if (!string.IsNullOrWhiteSpace(model.logoImage) && !IsBase64String(model.logoImage)) { _logger.LogWarning("Tenant creation failed for user {EmployeeId}: The provided logo image was not a valid Base64 string.", loggedInEmployee.Id); return StatusCode(400, ApiResponse.ErrorResponse("Tenant cannot be created", "The provided logo image is invalid.", 400)); } // 3. --- DATABASE TRANSACTION --- // Use a transaction to ensure all related entities are created successfully or none at all. await using var transaction = await _context.Database.BeginTransactionAsync(); try { // Get last SPRID and increment for new organization var lastOrganization = await _context.Organizations.OrderByDescending(sp => sp.SPRID).FirstOrDefaultAsync(); double lastSPRID = lastOrganization?.SPRID ?? 5400; // Map DTO to entity and set defaults Organization organization = new Organization { Name = model.OrganizationName, Email = model.Email, ContactPerson = $"{model.FirstName} {model.LastName}", Address = model.BillingAddress, ContactNumber = model.ContactNumber, SPRID = lastSPRID + 1, logoImage = model.logoImage, CreatedAt = DateTime.UtcNow, CreatedById = loggedInEmployee.Id, IsActive = true }; _context.Organizations.Add(organization); // Create the primary Tenant entity var tenant = _mapper.Map(model); tenant.TenantStatusId = tenantActiveStatus; tenant.CreatedById = loggedInEmployee.Id; tenant.IsSuperTenant = false; tenant.OrganizationId = organization.Id; _context.Tenants.Add(tenant); // Create the root ApplicationUser for the new tenant var applicationUser = new ApplicationUser { Email = model.Email, UserName = model.Email, // Best practice to use email as username for simplicity IsRootUser = true, EmailConfirmed = true // Auto-confirming email as it's part of a trusted setup process }; // SECURITY WARNING: Hardcoded passwords are a major vulnerability. // Replace "User@123" with a securely generated random password. var initialPassword = "User@123"; // TODO: Replace with password generation service. var result = await _userManager.CreateAsync(applicationUser, initialPassword); if (!result.Succeeded) { // If user creation fails, roll back the transaction immediately and return the errors. await transaction.RollbackAsync(); var errors = result.Errors.Select(e => e.Description).ToList(); _logger.LogWarning("Failed to create ApplicationUser for tenant {TenantName}. Errors: {Errors}", model.OrganizationName, string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Failed to create user", errors, 400)); } // Create the default "Admin" Job Role for the tenant var adminJobRole = new JobRole { Name = AdminRoleName, Description = "Default administrator role for the tenant.", TenantId = tenant.Id }; _context.JobRoles.Add(adminJobRole); // Create the primary Employee record and link it to the ApplicationUser and JobRole var employeeUser = new Employee { FirstName = model.FirstName, LastName = model.LastName, Email = model.Email, PhoneNumber = model.ContactNumber, JoiningDate = model.OnBoardingDate, ApplicationUserId = applicationUser.Id, JobRole = adminJobRole, // Link to the newly created role CurrentAddress = model.BillingAddress, IsActive = true, IsSystem = true, IsPrimary = true, OrganizationId = organization.Id, HasApplicationAccess = true }; _context.Employees.Add(employeeUser); var applicationRole = new ApplicationRole { Role = "Super User", Description = "Super User", IsSystem = true, TenantId = tenant.Id }; _context.ApplicationRoles.Add(applicationRole); var featureIds = new List { new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature }; var permissionIds = await _context.FeaturePermissions.Where(fp => featureIds.Contains(fp.FeatureId)).Select(fp => fp.Id).ToListAsync(); var rolePermissionMappigs = permissionIds.Select(p => new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = p }).ToList(); rolePermissionMappigs.Add(new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ModifyTenant }); rolePermissionMappigs.Add(new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ViewTenant }); _context.RolePermissionMappings.AddRange(rolePermissionMappigs); _context.EmployeeRoleMappings.Add(new EmployeeRoleMapping { EmployeeId = employeeUser.Id, RoleId = applicationRole.Id, IsEnabled = true, TenantId = tenant.Id }); // Create a default project for the new tenant var project = new Project { Name = "Default Project", ProjectStatusId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"), // Consider using a constant for this GUID ProjectAddress = model.BillingAddress, StartDate = model.OnBoardingDate, EndDate = DateTime.MaxValue, PromoterId = organization.Id, PMCId = organization.Id, ContactPerson = tenant.ContactName, TenantId = tenant.Id }; _context.Projects.Add(project); var projectAllocation = new ProjectAllocation { ProjectId = project.Id, EmployeeId = employeeUser.Id, AllocationDate = model.OnBoardingDate, IsActive = true, JobRoleId = adminJobRole.Id, TenantId = tenant.Id }; _context.ProjectAllocations.Add(projectAllocation); // Map organization services if (model.ServiceIds?.Any() ?? false) { var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping { ServiceId = s, OrganizationId = organization.Id }).ToList(); _context.OrgServiceMappings.AddRange(serviceOrgMappings); } var _masteData = scope.ServiceProvider.GetRequiredService(); var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id); var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id); var documentCategoryMaster = _masteData.GetDocumentCategoryData(tenant.Id); var employeeDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Employee Documents").Select(dc => dc.Id).FirstOrDefault(); var projectDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Project Documents").Select(dc => dc.Id).FirstOrDefault(); var documentTypeMaster = _masteData.GetDocumentTypeData(tenant.Id, employeeDocumentId, projectDocumentId); _context.ExpensesTypeMaster.AddRange(expensesTypeMaster); _context.PaymentModeMatser.AddRange(paymentModeMatser); _context.DocumentCategoryMasters.AddRange(documentCategoryMaster); _context.DocumentTypeMasters.AddRange(documentTypeMaster); // All entities are now added to the context. Save them all in a single database operation. await _context.SaveChangesAsync(); // 4. --- POST-CREATION ACTIONS --- // Generate a password reset token so the new user can set their own password. _logger.LogInfo("User {Email} created. Sending password setup email.", applicationUser.Email); var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser); var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}&email={WebUtility.UrlEncode(applicationUser.Email)}"; await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink); // Map the result to a ViewModel for the API response. var tenantVM = _mapper.Map(tenant); tenantVM.CreatedBy = _mapper.Map(loggedInEmployee); // Commit the transaction as all operations were successful. await transaction.CommitAsync(); _logger.LogInfo("Successfully created tenant {TenantId} for organization {OrganizationName}.", tenant.Id, tenant.Name); return StatusCode(201, ApiResponse.SuccessResponse(tenantVM, "Tenant created successfully.", 201)); } catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); // Log the detailed database exception, including the inner exception if available. _logger.LogError(dbEx, "A database update exception occurred while creating tenant for email {Email}. Inner Exception: {InnerException}", model.Email, dbEx.InnerException?.Message ?? string.Empty); return StatusCode(500, ApiResponse.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500)); } catch (Exception ex) { await transaction.RollbackAsync(); // Log the general exception. _logger.LogError(ex, "An unexpected exception occurred while creating tenant for email {Email}.", model.Email); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500)); } } /// /// Updates tenant and root employee details for a specified tenant ID. /// /// ID of the tenant to update /// Details to update /// Result of the operation [HttpPut("edit/{id}")] public async Task UpdateTenant(Guid id, [FromBody] UpdateTenantDto model) { _logger.LogInfo("UpdateTenant called for TenantId: {TenantId} by user.", id); // 1. Retrieve the logged-in employee information var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { _logger.LogWarning("Unauthorized access - User not logged in."); return StatusCode(403, ApiResponse.ErrorResponse("Unauthorized", "User must be logged in.", 403)); } // 2. Check permissions using a single service scope to avoid overhead bool hasManagePermission, hasModifyPermission; UtilityMongoDBHelper _updateLogHelper; using (var scope = _serviceScopeFactory.CreateScope()) { var permissionService = scope.ServiceProvider.GetRequiredService(); _updateLogHelper = scope.ServiceProvider.GetRequiredService(); var manageTask = permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); var modifyTask = permissionService.HasPermission(PermissionsMaster.ModifyTenant, loggedInEmployee.Id); await Task.WhenAll(manageTask, modifyTask); hasManagePermission = manageTask.Result; hasModifyPermission = modifyTask.Result; } if (!hasManagePermission && !hasModifyPermission) { _logger.LogWarning("Access denied: User {EmployeeId} lacks required permissions for UpdateTenant on TenantId: {TenantId}.", loggedInEmployee.Id, id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } if (!hasManagePermission && hasModifyPermission && id != loggedInEmployee.TenantId) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } // 3. Use a single DbContext instance for data access await using var context = await _dbContextFactory.CreateDbContextAsync(); // 4. Fetch tenant (with related Industry, TenantStatus), tracking enabled for updates var tenant = await context.Tenants .Include(t => t.Industry) .Include(t => t.TenantStatus) .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == id); if (tenant == null) { _logger.LogWarning("Tenant not found: ID {TenantId}", id); return NotFound(ApiResponse.ErrorResponse("Tenant not found", "Tenant not found", 404)); } _logger.LogInfo("Tenant {TenantId} fetched for update.", tenant.Id); var tenantObject = _updateLogHelper.EntityToBsonDocument(tenant); // 5. Map update DTO properties to the tenant entity _mapper.Map(model, tenant); // 6. Fetch root employee for the tenant (includes ApplicationUser) var rootEmployee = await context.Employees .Include(e => e.ApplicationUser) .FirstOrDefaultAsync(e => e.TenantId == tenant.Id && e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false)); if (rootEmployee == null) { _logger.LogWarning("Root employee not found for tenant {TenantId}", id); return NotFound(ApiResponse.ErrorResponse("Root employee not found", "Root employee not found", 404)); } var employeeobject = _updateLogHelper.EntityToBsonDocument(rootEmployee); // 7. Update root employee details rootEmployee.FirstName = model.FirstName; rootEmployee.LastName = model.LastName; rootEmployee.PhoneNumber = model.ContactNumber; rootEmployee.CurrentAddress = model.BillingAddress; // 8. Save changes to DB try { context.Tenants.Update(tenant); await context.SaveChangesAsync(); _logger.LogInfo("Tenant {TenantId} and root employee updated successfully.", tenant.Id); } catch (Exception ex) { _logger.LogError(ex, "Error updating Tenant {TenantId} or root employee.", tenant.Id); return StatusCode(500, ApiResponse.ErrorResponse("Error updating tenant", "Unexpected error occurred while updating tenant.", 500)); } var tenantTaks = Task.Run(async () => { await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = tenant.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = tenantObject, UpdatedAt = DateTime.UtcNow }, "TenantModificationLog"); }); var employeeTaks = Task.Run(async () => { await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = rootEmployee.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = employeeobject, UpdatedAt = DateTime.UtcNow }, "EmployeeModificationLog"); }); await Task.WhenAll(tenantTaks, employeeTaks); // 9. Map updated tenant to ViewModel for response var response = _mapper.Map(tenant); return Ok(ApiResponse.SuccessResponse(response, "Tenant updated successfully", 200)); } // DELETE api//5 [HttpDelete("delete/{id}")] public async Task DeleteTenant(Guid id, [FromQuery] bool isActive = false) { var action = isActive ? "activation" : "deactivation"; _logger.LogInfo("Attempting tenant {Action} for ID: {TenantId}", action, id); // --- 1. Authentication & Authorization --- var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { _logger.LogWarning("Unauthorized tenant status update attempt. User is not logged in."); return StatusCode(403, ApiResponse.ErrorResponse("Unauthorized", "User must be logged in to perform this action.", 403)); } using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); var _updateLogHelper = scope.ServiceProvider.GetRequiredService(); var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission && !(loggedInEmployee.ApplicationUser?.IsRootUser ?? false)) { _logger.LogWarning( "Permission Denied: User {EmployeeId} attempted tenant status update for Tenant {TenantId} without 'ManageTenants' permission.", loggedInEmployee.Id, id); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "User does not have the required permissions for this action.", 403)); } // --- 2. Data Retrieval --- await using var context = await _dbContextFactory.CreateDbContextAsync(); var tenant = await context.Tenants // Include related data only if it's required by the TenantVM mapping. // If not, removing these improves performance. .Include(t => t.Industry) .Include(t => t.TenantStatus) .FirstOrDefaultAsync(t => t.Id == id); if (tenant == null) { _logger.LogWarning("Tenant status update failed: Tenant with ID {TenantId} not found.", id); return NotFound(ApiResponse.ErrorResponse("Not Found", $"Tenant with ID '{id}' was not found.", 404)); } // --- 3. Logic & State Change --- // Efficiency: If the state is already what is being requested, do nothing. if (tenant.IsActive == isActive) { var currentStatus = isActive ? "already active" : "already inactive"; _logger.LogInfo("No action needed. Tenant {TenantId} is {Status}.", tenant.Id, currentStatus); var noChangeMessage = $"Tenant was {currentStatus}. No changes were made."; return Ok(ApiResponse.SuccessResponse(_mapper.Map(tenant), noChangeMessage, 200)); } // Capture the state *before* modification for the audit log. var tenantOldStateBson = _updateLogHelper.EntityToBsonDocument(tenant); tenant.IsActive = isActive; // --- 4. Database Persistence --- try { await context.SaveChangesAsync(); _logger.LogInfo("Successfully updated Tenant {TenantId} IsActive status to {IsActive}.", tenant.Id, isActive); } catch (DbUpdateException ex) // Be more specific with exceptions if possible. { _logger.LogError(ex, "Database error occurred while updating status for Tenant {TenantId}.", tenant.Id); return StatusCode(500, ApiResponse.ErrorResponse("Database Error", "An error occurred while saving changes to the database.", 500)); } catch (Exception ex) { _logger.LogError(ex, "An unexpected error occurred while updating status for Tenant {TenantId}.", tenant.Id); return StatusCode(500, ApiResponse.ErrorResponse("Server Error", "An unexpected error occurred.", 500)); } // --- 5. Audit Logging --- // This runs after the DB save is confirmed. // Note: If this call fails, the audit log will be missing for a successful DB change. // For critical systems, consider a more robust outbox pattern. await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = tenant.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = tenantOldStateBson, UpdatedAt = DateTime.UtcNow }, "TenantModificationLog"); _logger.LogInfo("Audit log created for status change of Tenant {TenantId} by User {EmployeeId}.", tenant.Id, loggedInEmployee.Id); // --- 6. Prepare and Return Response --- var responseData = _mapper.Map(tenant); var successMessage = $"Tenant successfully {(isActive ? "activated" : "deactivated")}."; return Ok(ApiResponse.SuccessResponse(responseData, successMessage, 200)); } #endregion #region =================================================================== Subscription APIs =================================================================== [HttpPost("add-subscription")] public async Task AddSubscriptionAsync(AddSubscriptionDto model) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogInfo("AddSubscription called by employee {EmployeeId} for Tenant {TenantId} and Plan {PlanId}", loggedInEmployee.Id, model.TenantId, model.PlanId); if (loggedInEmployee == null) { _logger.LogWarning("No logged-in employee found."); return StatusCode(403, ApiResponse.ErrorResponse("Unauthorized", "User must be logged in.", 403)); } await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission || !isRootUser) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to add subscription without permission or root access.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } var subscriptionPlan = await _context.SubscriptionPlanDetails.Include(sp => sp.Plan).FirstOrDefaultAsync(sp => sp.Id == model.PlanId); var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == model.TenantId); if (tenant == null) { _logger.LogWarning("Tenant {TenantId} not found in database", model.TenantId); return NotFound(ApiResponse.ErrorResponse("Tenant not found", "Tenant not found", 404)); } if (subscriptionPlan == null) { _logger.LogWarning("Subscription plan {PlanId} not found in database", model.PlanId); return NotFound(ApiResponse.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404)); } var activeUsers = await _context.Employees.CountAsync(e => e.Email != null && e.ApplicationUserId != null && e.TenantId == tenant.Id && e.IsActive); if (activeUsers > model.MaxUsers) { _logger.LogWarning("Employee {EmployeeId} add less max user than the active user in the tenant {TenantId}", loggedInEmployee.Id, tenant.Id); return BadRequest(ApiResponse.ErrorResponse("Invalid Max user count", "Max User count must be higher than active user count", 400)); } await using var transaction = await _context.Database.BeginTransactionAsync(); var utcNow = DateTime.UtcNow; // Prepare subscription dates based on frequency var endDate = subscriptionPlan.Frequency switch { PLAN_FREQUENCY.MONTHLY => utcNow.AddDays(30), PLAN_FREQUENCY.QUARTERLY => utcNow.AddDays(90), PLAN_FREQUENCY.HALF_YEARLY => utcNow.AddDays(120), PLAN_FREQUENCY.YEARLY => utcNow.AddDays(360), _ => utcNow // default if unknown }; var tenantSubscription = new TenantSubscriptions { TenantId = model.TenantId, PlanId = model.PlanId, StatusId = activePlanStatus, CreatedAt = utcNow, MaxUsers = model.MaxUsers, CreatedById = loggedInEmployee.Id, CurrencyId = model.CurrencyId, IsTrial = model.IsTrial, StartDate = utcNow, EndDate = endDate, NextBillingDate = endDate, AutoRenew = model.AutoRenew }; _context.TenantSubscriptions.Add(tenantSubscription); try { await _context.SaveChangesAsync(); _logger.LogInfo("Tenant subscription added successfully for Tenant {TenantId}, Plan {PlanId}", model.TenantId, model.PlanId); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database exception while adding subscription plan to tenant {TenantId}", model.TenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal error occured", ExceptionMapper(dbEx), 500)); } try { _ = Task.Run(async () => { await ClearPermissionForTenant(); }); var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId); if (features == null) { _logger.LogInfo("No features found for subscription plan {PlanId}", model.PlanId); await transaction.CommitAsync(); return Ok(ApiResponse.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200)); } // Helper to get permissions for a module asynchronously async Task> GetPermissionsForModuleAsync(List? featureIds) { if (featureIds == null || featureIds.Count == 0) return new List(); await using var ctx = await _dbContextFactory.CreateDbContextAsync(); return await ctx.FeaturePermissions.AsNoTracking() .Where(fp => featureIds.Contains(fp.FeatureId)) .Select(fp => fp.Id) .ToListAsync(); } // Fetch permission tasks for all modules in parallel var projectPermissionTask = GetPermissionsForModuleAsync(features.Modules?.ProjectManagement?.FeatureId); var attendancePermissionTask = GetPermissionsForModuleAsync(features.Modules?.Attendance?.FeatureId); var directoryPermissionTask = GetPermissionsForModuleAsync(features.Modules?.Directory?.FeatureId); var expensePermissionTask = GetPermissionsForModuleAsync(features.Modules?.Expense?.FeatureId); var employeePermissionTask = GetPermissionsForModuleAsync(new List { EmployeeFeatureId }); await Task.WhenAll(projectPermissionTask, attendancePermissionTask, directoryPermissionTask, expensePermissionTask, employeePermissionTask); var newPermissionIds = new List(); var deletePermissionIds = new List(); // Add or remove permissions based on modules enabled status void ProcessPermissions(bool? enabled, List permissions) { if (enabled == true) newPermissionIds.AddRange(permissions); else deletePermissionIds.AddRange(permissions); } ProcessPermissions(features.Modules?.ProjectManagement?.Enabled, projectPermissionTask.Result); ProcessPermissions(features.Modules?.Attendance?.Enabled, attendancePermissionTask.Result); ProcessPermissions(features.Modules?.Directory?.Enabled, directoryPermissionTask.Result); ProcessPermissions(features.Modules?.Expense?.Enabled, expensePermissionTask.Result); newPermissionIds = newPermissionIds.Distinct().ToList(); deletePermissionIds = deletePermissionIds.Distinct().ToList(); // Get root employee and role for this tenant var rootEmployee = await _context.Employees .Include(e => e.ApplicationUser) .FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId); if (rootEmployee == null) { _logger.LogWarning("Root employee not found for tenant {TenantId}", model.TenantId); await transaction.CommitAsync(); return Ok(ApiResponse.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200)); } var roleId = await _context.EmployeeRoleMappings .AsNoTracking() .Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId) .Select(er => er.RoleId) .FirstOrDefaultAsync(); if (roleId == Guid.Empty) { _logger.LogWarning("RoleId for root employee {EmployeeId} in tenant {TenantId} not found", rootEmployee.Id, model.TenantId); await transaction.CommitAsync(); return Ok(ApiResponse.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200)); } var oldRolePermissionMappings = await _context.RolePermissionMappings .Where(rp => rp.ApplicationRoleId == roleId) .ToListAsync(); var oldPermissionIds = oldRolePermissionMappings.Select(rp => rp.FeaturePermissionId).ToList(); // Prevent accidentally deleting essential employee permissions var permissionIdCount = oldPermissionIds.Count - deletePermissionIds.Count; if (permissionIdCount <= 4 && deletePermissionIds.Any()) { var employeePermissionIds = employeePermissionTask.Result; deletePermissionIds = deletePermissionIds.Where(p => !employeePermissionIds.Contains(p)).ToList(); } // Prepare mappings to delete and add var deleteMappings = oldRolePermissionMappings.Where(rp => deletePermissionIds.Contains(rp.FeaturePermissionId)).ToList(); var addRolePermissionMappings = newPermissionIds .Where(p => !oldPermissionIds.Contains(p)) .Select(p => new RolePermissionMappings { ApplicationRoleId = roleId, FeaturePermissionId = p }) .ToList(); if (addRolePermissionMappings.Any()) { _context.RolePermissionMappings.AddRange(addRolePermissionMappings); _logger.LogInfo("Added {Count} new role permission mappings for role {RoleId}", addRolePermissionMappings.Count, roleId); } if (deleteMappings.Any()) { _context.RolePermissionMappings.RemoveRange(deleteMappings); _logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId); } var _masteData = scope.ServiceProvider.GetRequiredService(); if (features.Modules?.ProjectManagement?.Enabled ?? false) { var workCategoryMaster = _masteData.GetWorkCategoriesData(tenant.Id); var workStatusMaster = _masteData.GetWorkStatusesData(tenant.Id); _context.WorkCategoryMasters.AddRange(workCategoryMaster); _context.WorkStatusMasters.AddRange(workStatusMaster); } if (features.Modules?.Expense?.Enabled ?? false) { var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id); var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id); _context.ExpensesTypeMaster.AddRange(expensesTypeMaster); _context.PaymentModeMatser.AddRange(paymentModeMatser); } await _context.SaveChangesAsync(); await transaction.CommitAsync(); _logger.LogInfo("Permissions updated successfully for tenant {TenantId} subscription", model.TenantId); return Ok(ApiResponse.SuccessResponse(tenantSubscription, "Tenant Subscription Successfully", 200)); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "Exception occurred while updating permissions for tenant {TenantId}", model.TenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal error occured", ExceptionMapper(ex), 500)); } } [HttpPut("update-subscription")] public async Task UpdateSubscriptionAsync(UpdateSubscriptionDto model) { // 1. Get the logged-in user's employee record. var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // 2. Create a new DbContext instance for this request. await using var context = await _dbContextFactory.CreateDbContextAsync(); // 3. Get PermissionServices from DI inside a fresh scope (rarely needed, but retained for your design). using var scope = _serviceScopeFactory.CreateScope(); var permissionService = scope.ServiceProvider.GetRequiredService(); var _updateLogHelper = scope.ServiceProvider.GetRequiredService(); // 4. Check user permissions: must be both Root user and have ManageTenants permission. var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!isRootUser || !hasPermission) { _logger.LogWarning("Permission denied for EmployeeId={EmployeeId}. Root: {IsRoot}, HasPermission: {HasPermission}", loggedInEmployee.Id, isRootUser, hasPermission); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions.", 403)); } // 5. Fetch Tenant, SubscriptionPlan, and TenantSubscription in parallel (efficiently). var tenantTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => ctx.Result.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == model.TenantId)).Unwrap(); var planDetailsTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => ctx.Result.SubscriptionPlanDetails.Include(sp => sp.Plan).AsNoTracking().FirstOrDefaultAsync(sp => sp.Id == model.PlanId)).Unwrap(); var currentSubscriptionTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => ctx.Result.TenantSubscriptions.Include(ts => ts.Currency).AsNoTracking().FirstOrDefaultAsync(ts => ts.TenantId == model.TenantId && !ts.IsCancelled)).Unwrap(); await Task.WhenAll(tenantTask, planDetailsTask, currentSubscriptionTask); var tenant = tenantTask.Result; if (tenant == null) { _logger.LogWarning("Tenant {TenantId} not found.", model.TenantId); return NotFound(ApiResponse.ErrorResponse("Tenant not found", "Tenant not found", 404)); } var subscriptionPlan = planDetailsTask.Result; if (subscriptionPlan == null) { _logger.LogWarning("Subscription plan {PlanId} not found.", model.PlanId); return NotFound(ApiResponse.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404)); } var currentSubscription = currentSubscriptionTask.Result; var utcNow = DateTime.UtcNow; var activeUsers = await context.Employees.CountAsync(e => e.Email != null && e.ApplicationUserId != null && e.TenantId == tenant.Id && e.IsActive); if (activeUsers > model.MaxUsers) { _logger.LogWarning("Employee {EmployeeId} add less max user than the active user in the tenant {TenantId}", loggedInEmployee.Id, tenant.Id); return BadRequest(ApiResponse.ErrorResponse("Invalid Max user count", "Max User count must be higher than active user count", 400)); } // 6. If the tenant already has this plan, extend/renew it. if (currentSubscription != null && currentSubscription.PlanId == subscriptionPlan.Id) { var existingEntityBson = _updateLogHelper.EntityToBsonDocument(currentSubscription); DateTime newEndDate; // 6a. If the subscription is still active, extend from current EndDate; else start from now. if (currentSubscription.EndDate.Date >= utcNow.Date) { newEndDate = subscriptionPlan.Frequency switch { PLAN_FREQUENCY.MONTHLY => currentSubscription.EndDate.AddDays(30), PLAN_FREQUENCY.QUARTERLY => currentSubscription.EndDate.AddDays(90), PLAN_FREQUENCY.HALF_YEARLY => currentSubscription.EndDate.AddDays(120), PLAN_FREQUENCY.YEARLY => currentSubscription.EndDate.AddDays(360), _ => currentSubscription.EndDate }; } else { newEndDate = subscriptionPlan.Frequency switch { PLAN_FREQUENCY.MONTHLY => utcNow.AddDays(30), PLAN_FREQUENCY.QUARTERLY => utcNow.AddDays(90), PLAN_FREQUENCY.HALF_YEARLY => utcNow.AddDays(120), PLAN_FREQUENCY.YEARLY => utcNow.AddDays(360), _ => utcNow // default if unknown }; } // 6b. Update subscription details if (model.MaxUsers != null && model.MaxUsers > 0) { currentSubscription.MaxUsers = model.MaxUsers.Value; } currentSubscription.StartDate = utcNow; currentSubscription.EndDate = newEndDate; currentSubscription.NextBillingDate = newEndDate; currentSubscription.UpdateAt = utcNow; currentSubscription.UpdatedById = loggedInEmployee.Id; context.TenantSubscriptions.Update(currentSubscription); await context.SaveChangesAsync(); _logger.LogInfo("Subscription renewed: Tenant={TenantId}, Plan={PlanId}, NewEnd={EndDate}", model.TenantId, model.PlanId, newEndDate); await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject { EntityId = currentSubscription.Id.ToString(), UpdatedById = loggedInEmployee.Id.ToString(), OldObject = existingEntityBson, UpdatedAt = DateTime.UtcNow }, "SubscriptionPlanModificationLog"); return Ok(ApiResponse.SuccessResponse(currentSubscription, "Subscription renewed/extended", 200)); } // 7. Else, change plan: new subscription record, close the old if exists. await using var transaction = await context.Database.BeginTransactionAsync(); try { // 7a. Compute new plan dates var endDate = subscriptionPlan.Frequency switch { PLAN_FREQUENCY.MONTHLY => utcNow.AddDays(30), PLAN_FREQUENCY.QUARTERLY => utcNow.AddDays(90), PLAN_FREQUENCY.HALF_YEARLY => utcNow.AddDays(120), PLAN_FREQUENCY.YEARLY => utcNow.AddDays(360), _ => utcNow // default if unknown }; var newSubscription = new TenantSubscriptions { TenantId = model.TenantId, PlanId = model.PlanId, StatusId = activePlanStatus, CreatedAt = utcNow, MaxUsers = model.MaxUsers ?? (currentSubscription?.MaxUsers ?? subscriptionPlan.MaxUser), CreatedById = loggedInEmployee.Id, CurrencyId = model.CurrencyId, StartDate = utcNow, EndDate = endDate, NextBillingDate = endDate, IsTrial = currentSubscription?.IsTrial ?? false, AutoRenew = currentSubscription?.AutoRenew ?? false }; context.TenantSubscriptions.Add(newSubscription); // 7b. If an old subscription exists, cancel it. if (currentSubscription != null) { currentSubscription.IsCancelled = true; currentSubscription.CancellationDate = utcNow; currentSubscription.UpdateAt = utcNow; currentSubscription.UpdatedById = loggedInEmployee.Id; context.TenantSubscriptions.Update(currentSubscription); } await context.SaveChangesAsync(); _logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}", model.TenantId, model.PlanId); _ = Task.Run(async () => { await ClearPermissionForTenant(); }); // 8. Update tenant permissions based on subscription features. var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId); if (features == null) { _logger.LogInfo("No features for Plan={PlanId}.", model.PlanId); await transaction.CommitAsync(); return Ok(ApiResponse.SuccessResponse(newSubscription, "Tenant subscription updated (no features)", 200)); } // 8a. Async helper to get all permission IDs for a given module. async Task> GetPermissionsForFeaturesAsync(List? featureIds) { if (featureIds == null || featureIds.Count == 0) return new List(); await using var ctx = await _dbContextFactory.CreateDbContextAsync(); return await ctx.FeaturePermissions.AsNoTracking() .Where(fp => featureIds.Contains(fp.FeatureId)) .Select(fp => fp.Id) .ToListAsync(); } // 8b. Fetch all module permissions concurrently. var projectPermTask = GetPermissionsForFeaturesAsync(features.Modules?.ProjectManagement?.FeatureId); var attendancePermTask = GetPermissionsForFeaturesAsync(features.Modules?.Attendance?.FeatureId); var directoryPermTask = GetPermissionsForFeaturesAsync(features.Modules?.Directory?.FeatureId); var expensePermTask = GetPermissionsForFeaturesAsync(features.Modules?.Expense?.FeatureId); var employeePermTask = GetPermissionsForFeaturesAsync(new List { EmployeeFeatureId }); // assumed defined await Task.WhenAll(projectPermTask, attendancePermTask, directoryPermTask, expensePermTask, employeePermTask); // 8c. Find root employee & role for this tenant. var rootEmployee = await context.Employees .Include(e => e.ApplicationUser) .FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId); if (rootEmployee == null) { _logger.LogWarning("No root employee for Tenant={TenantId}.", model.TenantId); await transaction.CommitAsync(); return Ok(ApiResponse.SuccessResponse(newSubscription, "Tenant subscription updated (no root employee)", 200)); } var rootRoleId = await context.EmployeeRoleMappings .AsNoTracking() .Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId) .Select(er => er.RoleId) .FirstOrDefaultAsync(); if (rootRoleId == Guid.Empty) { _logger.LogWarning("No root role for Employee={EmployeeId}, Tenant={TenantId}.", rootEmployee.Id, model.TenantId); await transaction.CommitAsync(); return Ok(ApiResponse.SuccessResponse(newSubscription, "Tenant subscription updated (no root role)", 200)); } var dbOldRolePerms = await context.RolePermissionMappings.Where(x => x.ApplicationRoleId == rootRoleId).ToListAsync(); var oldPermIds = dbOldRolePerms.Select(rp => rp.FeaturePermissionId).ToList(); // 8d. Prepare add and remove permission lists. var newPermissionIds = new List(); var revokePermissionIds = new List(); var employeePerms = employeePermTask.Result; var isOldEmployeePermissionIdExist = oldPermIds.Any(fp => employeePerms.Contains(fp)); void ProcessPerms(bool? enabled, List ids) { var isOldPermissionIdExist = oldPermIds.Any(fp => ids.Contains(fp) && !employeePerms.Contains(fp)); if (enabled == true && !isOldPermissionIdExist) newPermissionIds.AddRange(ids); if (enabled == true && !isOldEmployeePermissionIdExist) newPermissionIds.AddRange(ids); if (enabled == false && isOldPermissionIdExist) revokePermissionIds.AddRange(ids); } ProcessPerms(features.Modules?.ProjectManagement?.Enabled, projectPermTask.Result); ProcessPerms(features.Modules?.Attendance?.Enabled, attendancePermTask.Result); ProcessPerms(features.Modules?.Directory?.Enabled, directoryPermTask.Result); ProcessPerms(features.Modules?.Expense?.Enabled, expensePermTask.Result); newPermissionIds = newPermissionIds.Distinct().ToList(); revokePermissionIds = revokePermissionIds.Distinct().ToList(); // 8e. Prevent accidental loss of basic employee permissions. if ((features.Modules?.ProjectManagement?.Enabled == true || features.Modules?.Attendance?.Enabled == true || features.Modules?.Directory?.Enabled == true || features.Modules?.Expense?.Enabled == true) && isOldEmployeePermissionIdExist) { revokePermissionIds = revokePermissionIds.Where(pid => !employeePerms.Contains(pid)).ToList(); } // 8f. Prepare permission-mapping records to add/remove. var mappingsToRemove = dbOldRolePerms.Where(rp => revokePermissionIds.Contains(rp.FeaturePermissionId)).ToList(); var mappingsToAdd = newPermissionIds .Where(pid => !oldPermIds.Contains(pid)) .Select(pid => new RolePermissionMappings { ApplicationRoleId = rootRoleId, FeaturePermissionId = pid }) .ToList(); if (mappingsToAdd.Any()) { context.RolePermissionMappings.AddRange(mappingsToAdd); _logger.LogInfo("Permissions granted: {Count} for Role={RoleId}", mappingsToAdd.Count, rootRoleId); } if (mappingsToRemove.Any()) { context.RolePermissionMappings.RemoveRange(mappingsToRemove); _logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId); } var _masteData = scope.ServiceProvider.GetRequiredService(); if (features.Modules?.ProjectManagement?.Enabled ?? false) { var workCategoryMaster = _masteData.GetWorkCategoriesData(tenant.Id); var workStatusMaster = _masteData.GetWorkStatusesData(tenant.Id); var workCategoryTask = Task.Run(async () => { await using var _context = await _dbContextFactory.CreateDbContextAsync(); return await _context.WorkCategoryMasters.AnyAsync(wc => wc.IsSystem && wc.TenantId == tenant.Id); }); var workStatusTask = Task.Run(async () => { await using var _context = await _dbContextFactory.CreateDbContextAsync(); return await _context.WorkStatusMasters.AnyAsync(ws => ws.IsSystem && ws.TenantId == tenant.Id); }); await Task.WhenAll(workCategoryTask, workStatusTask); var workCategoryExist = workCategoryTask.Result; var workStatusExist = workStatusTask.Result; if (!workCategoryExist) { context.WorkCategoryMasters.AddRange(workCategoryMaster); } if (!workStatusExist) { context.WorkStatusMasters.AddRange(workStatusMaster); } } if (features.Modules?.Expense?.Enabled ?? false) { var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id); var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id); var expensesTypeTask = Task.Run(async () => { await using var _context = await _dbContextFactory.CreateDbContextAsync(); var expensesTypeNames = expensesTypeMaster.Select(et => et.Name).ToList(); return await _context.ExpensesTypeMaster.AnyAsync(et => expensesTypeNames.Contains(et.Name) && et.TenantId == tenant.Id); }); var paymentModeTask = Task.Run(async () => { await using var _context = await _dbContextFactory.CreateDbContextAsync(); var paymentModeNames = paymentModeMatser.Select(py => py.Name).ToList(); return await _context.PaymentModeMatser.AnyAsync(py => paymentModeNames.Contains(py.Name) && py.TenantId == tenant.Id); }); await Task.WhenAll(expensesTypeTask, paymentModeTask); var expensesTypeExist = expensesTypeTask.Result; var paymentModeExist = paymentModeTask.Result; if (!expensesTypeExist) { context.ExpensesTypeMaster.AddRange(expensesTypeMaster); } if (!paymentModeExist) { context.PaymentModeMatser.AddRange(paymentModeMatser); } } await context.SaveChangesAsync(); await transaction.CommitAsync(); _logger.LogInfo("Tenant subscription and permissions updated: Tenant={TenantId}", model.TenantId); return Ok(ApiResponse.SuccessResponse(newSubscription, "Tenant subscription successfully updated", 200)); } catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); _logger.LogError(dbEx, "Database exception updating subscription for TenantId={TenantId}", model.TenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal database error", ExceptionMapper(dbEx), 500)); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "General exception for TenantId={TenantId}", model.TenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal error occurred", ExceptionMapper(ex), 500)); } } #endregion #region =================================================================== Subscription Plan APIs =================================================================== [HttpGet("list/subscription-plan")] public async Task GetSubscriptionPlanList([FromQuery] PLAN_FREQUENCY? frequency) { _logger.LogInfo("GetSubscriptionPlanList called with frequency: {Frequency}", frequency ?? PLAN_FREQUENCY.MONTHLY); // Initialize the list to store subscription plan view models List detailsVM = new List(); try { // Create DbContext await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Load subscription plans with optional frequency filtering IQueryable query = _context.SubscriptionPlanDetails.Include(sp => sp.Plan).Include(sp => sp.Currency); if (frequency.HasValue) { query = query.Where(sp => sp.Frequency == frequency.Value); _logger.LogInfo("Filtering subscription plans by frequency: {Frequency}", frequency); } else { _logger.LogInfo("Fetching all subscription plans without frequency filter"); } var subscriptionPlans = await query.ToListAsync(); // Map and fetch feature details for each subscription plan foreach (var subscriptionPlan in subscriptionPlans) { var response = _mapper.Map(subscriptionPlan); try { response.Features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId); } catch (Exception exFeature) { _logger.LogError(exFeature, "Failed to fetch features for FeaturesId: {FeaturesId}", subscriptionPlan.FeaturesId); response.Features = null; // or set to a default/fallback value } detailsVM.Add(response); } _logger.LogInfo("Successfully fetched {Count} subscription plans", detailsVM.Count); return Ok(ApiResponse.SuccessResponse(detailsVM, "List of plans fetched successfully", 200)); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while fetching subscription plans"); return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while fetching subscription plans.")); } } /// /// Creates a new subscription plan with details for each frequency (monthly, quarterly, half-yearly, yearly). /// Only employees with root status and 'ManageTenants' permission can create plans. /// [HttpPost("create/subscription-plan")] public async Task CreateSubscriptionPlan([FromBody] SubscriptionPlanDto model) { // Step 1: Authenticate and check permissions var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); // Permission check: root user or explicit ManageTenants permission var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!(hasPermission && isRootUser)) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to create a subscription plan.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } _logger.LogInfo("User {EmployeeId} authorized to create subscription plan.", loggedInEmployee.Id); // Step 2: Map DTO to entity and persist the base SubscriptionPlan var plan = _mapper.Map(model); _context.SubscriptionPlans.Add(plan); try { await _context.SaveChangesAsync(); _logger.LogInfo("Base subscription plan {PlanId} saved by user {EmployeeId}.", plan.Id, loggedInEmployee.Id); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database exception occurred while saving the base subscription plan."); return StatusCode(500, ApiResponse.ErrorResponse("Internal error occurred", ExceptionMapper(dbEx), 500)); } // Step 3: Prepare tasks for each plan frequency var frequencies = new[] { (model.MonthlyPlan, PLAN_FREQUENCY.MONTHLY), (model.QuarterlyPlan, PLAN_FREQUENCY.QUARTERLY), (model.HalfYearlyPlan, PLAN_FREQUENCY.HALF_YEARLY), (model.YearlyPlan, PLAN_FREQUENCY.YEARLY) }; var tasks = frequencies .Select(f => CreateSubscriptionPlanDetails(f.Item1, plan, loggedInEmployee, f.Item2)) .ToArray(); // Await all frequency tasks await Task.WhenAll(tasks); // Step 4: Collect successful plan details or return errors if any var responseList = new List(); for (int i = 0; i < tasks.Length; i++) { var result = tasks[i].Result; if (result.StatusCode == 200 && result.Data != null) { responseList.Add(result.Data); } // status code 400 - skip, e.g. missing data for this frequency else if (result.StatusCode != 400) { _logger.LogWarning("Failed to create plan details for {Frequency}: {Error}", frequencies[i].Item2, result.Message); return StatusCode(result.StatusCode, result); } } _logger.LogInfo("Subscription plan {PlanId} created successfully with {DetailCount} details.", plan.Id, responseList.Count); return StatusCode(201, ApiResponse.SuccessResponse(responseList, "Plan Created Successfully", 201)); } #endregion #region =================================================================== Helper Functions =================================================================== private static object ExceptionMapper(Exception ex) { return new { Message = ex.Message, StackTrace = ex.StackTrace, Source = ex.Source, InnerException = new { Message = ex.InnerException?.Message, StackTrace = ex.InnerException?.StackTrace, Source = ex.InnerException?.Source, } }; } private bool IsBase64String(string? input) { if (string.IsNullOrWhiteSpace(input)) { return false; } string base64Data = input; const string dataUriMarker = "base64,"; int markerIndex = input.IndexOf(dataUriMarker, StringComparison.Ordinal); // If the marker is found, extract the actual Base64 data if (markerIndex >= 0) { base64Data = input.Substring(markerIndex + dataUriMarker.Length); } // Now, validate the extracted payload base64Data = base64Data.Trim(); // Check for valid length (must be a multiple of 4) and non-empty if (base64Data.Length == 0 || base64Data.Length % 4 != 0) { return false; } // The most reliable test is to simply try to convert it. // The .NET converter is strict and will throw a FormatException // for invalid characters or incorrect padding. try { Convert.FromBase64String(base64Data); return true; } catch (FormatException) { // The string is not a valid Base64 payload. return false; } } private TenantFilter? TryDeserializeFilter(string? filter) { if (string.IsNullOrWhiteSpace(filter)) { return null; } var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; TenantFilter? expenseFilter = null; try { // First, try to deserialize directly. This is the expected case (e.g., from a web client). expenseFilter = JsonSerializer.Deserialize(filter, options); } catch (JsonException ex) { _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter); // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients). try { // Unescape the string first, then deserialize the result. string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; if (!string.IsNullOrWhiteSpace(unescapedJsonString)) { expenseFilter = JsonSerializer.Deserialize(unescapedJsonString, options); } } catch (JsonException ex1) { // If both attempts fail, log the final error and return null. _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter); return null; } } return expenseFilter; } /// /// Handles the creation and persistence of SubscriptionPlanDetails for a particular frequency. /// private async Task> CreateSubscriptionPlanDetails(SubscriptionPlanDetailsDto? model, SubscriptionPlan plan, Employee loggedInEmployee, PLAN_FREQUENCY frequency) { if (model == null) { _logger.LogInfo("No plan detail provided for {Frequency} - skipping.", frequency); return ApiResponse.ErrorResponse("Invalid", "No data provided for this frequency", 400); } await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); // Fetch currency master record var currencyMaster = await _dbContext.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.CurrencyId); if (currencyMaster == null) { _logger.LogWarning("Currency with Id {CurrencyId} not found for plan {PlanId}/{Frequency}.", model.CurrencyId, plan.Id, frequency); return ApiResponse.ErrorResponse("Currency not found", "Specified currency not found", 404); } // Map to entity and create related feature details var planDetails = _mapper.Map(model); var features = _mapper.Map(model.Features); try { await _featureDetailsHelper.AddFeatureDetails(features); _logger.LogInfo("FeatureDetails for plan {PlanId}/{Frequency} saved in MongoDB.", plan.Id, frequency); } catch (Exception ex) { _logger.LogError(ex, "Exception occurred while saving features in MongoDB for {PlanId}/{Frequency}.", plan.Id, frequency); return ApiResponse.ErrorResponse("Internal error occurred", ExceptionMapper(ex), 500); } planDetails.PlanId = plan.Id; planDetails.Frequency = frequency; planDetails.FeaturesId = features.Id; planDetails.CreatedById = loggedInEmployee.Id; planDetails.CreateAt = DateTime.UtcNow; _dbContext.SubscriptionPlanDetails.Add(planDetails); // Prepare view model var VM = _mapper.Map(planDetails); VM.PlanName = plan.PlanName; VM.Description = plan.Description; VM.Features = features; VM.Currency = currencyMaster; try { await _dbContext.SaveChangesAsync(); _logger.LogInfo("Subscription plan details for {PlanId}/{Frequency} saved to SQL.", plan.Id, frequency); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database exception occurred while saving plan details for {PlanId}/{Frequency}.", plan.Id, frequency); return ApiResponse.ErrorResponse("Internal error occurred", ExceptionMapper(dbEx), 500); } return ApiResponse.SuccessResponse(VM, "Success", 200); } private async Task ClearPermissionForTenant() { await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _cache = scope.ServiceProvider.GetRequiredService(); var _cacheLogger = scope.ServiceProvider.GetRequiredService(); var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId).Select(e => e.Id).ToListAsync(); await _cache.ClearAllEmployeesFromCacheByEmployeeIds(employeeIds, tenantId); _cacheLogger.LogInfo("{EmployeeCount} number of employee deleted", employeeIds.Count); } #endregion } }