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.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.ServiceInterfaces; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using System.Net; using Tenant = Marco.Pms.Model.TenantModels.Tenant; namespace Marco.Pms.Services.Service { public class TenantService : ITenantService { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILoggingService _logger; private readonly UserManager _userManager; private readonly IMapper _mapper; private readonly FeatureDetailsHelper _featureDetailsHelper; 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 TenantService(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, ILoggingService logger, UserManager userManager, IMapper mapper, 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)); _featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper)); } public async Task> CreateTenantAsync(Guid enquireId, Guid paymentDetailId, Guid planId) { using var scope = _serviceScopeFactory.CreateScope(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); var _configuration = scope.ServiceProvider.GetRequiredService(); var _emailSender = scope.ServiceProvider.GetRequiredService(); var tenantEnquireTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.TenantEnquires.FirstOrDefaultAsync(te => te.Id == enquireId); }); var subscriptionPlanTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.SubscriptionPlanDetails.Include(sp => sp.Plan).FirstOrDefaultAsync(sp => sp.Id == planId); }); await Task.WhenAll(tenantEnquireTask, subscriptionPlanTask); var tenantEnquire = tenantEnquireTask.Result; var subscriptionPlan = subscriptionPlanTask.Result; if (tenantEnquire == null) { _logger.LogWarning("Tenant Enquire {TenantEnquireId} not found in database", enquireId); return ApiResponse.ErrorResponse("Tenant Enquire not found", "Tenant Enquire not found", 404); } if (subscriptionPlan == null) { _logger.LogWarning("Subscription plan {PlanId} not found in database", planId); return ApiResponse.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404); } var existingUser = await _userManager.FindByEmailAsync(tenantEnquire.Email); if (existingUser != null) { _logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", tenantEnquire.Email); return ApiResponse.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409); } await using var transaction = await _context.Database.BeginTransactionAsync(); try { Guid employeeId = Guid.NewGuid(); DateTime onBoardingDate = DateTime.UtcNow; Guid tenantId = Guid.NewGuid(); // 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 = tenantEnquire.OrganizationName, Email = tenantEnquire.Email, ContactPerson = $"{tenantEnquire.FirstName} {tenantEnquire.LastName}", Address = tenantEnquire.BillingAddress, ContactNumber = tenantEnquire.ContactNumber, SPRID = lastSPRID + 1, CreatedAt = DateTime.UtcNow, CreatedById = employeeId, IsActive = true }; _context.Organizations.Add(organization); // Create the primary Tenant entity var tenant = new Tenant { Id = tenantId, Name = tenantEnquire.OrganizationName, Email = tenantEnquire.Email, ContactName = $"{tenantEnquire.FirstName} {tenantEnquire.LastName}", ContactNumber = tenantEnquire.ContactNumber, OrganizationSize = tenantEnquire.OrganizationSize, BillingAddress = tenantEnquire.BillingAddress, IndustryId = tenantEnquire.IndustryId, Reference = tenantEnquire.Reference, OnBoardingDate = onBoardingDate, TenantStatusId = tenantActiveStatus, OrganizationId = organization.Id, CreatedById = employeeId, IsActive = true, IsSuperTenant = false }; _context.Tenants.Add(tenant); // Create the root ApplicationUser for the new tenant var applicationUser = new ApplicationUser { Email = tenantEnquire.Email, UserName = tenantEnquire.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}", tenantEnquire.OrganizationName, string.Join(", ", errors)); return 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 = tenantId }; _context.JobRoles.Add(adminJobRole); // Create the primary Employee record and link it to the ApplicationUser and JobRole var employeeUser = new Employee { Id = employeeId, FirstName = tenantEnquire.FirstName, LastName = tenantEnquire.LastName, Email = tenantEnquire.Email, PhoneNumber = tenantEnquire.ContactNumber, JoiningDate = onBoardingDate, ApplicationUserId = applicationUser.Id, JobRole = adminJobRole, // Link to the newly created role CurrentAddress = tenantEnquire.BillingAddress, IsActive = true, IsSystem = false, IsPrimary = true, OrganizationId = organization.Id, HasApplicationAccess = true }; _context.Employees.Add(employeeUser); var applicationRole = new ApplicationRole { Role = "Super User", Description = "Super User", IsSystem = true, TenantId = tenantId }; _context.ApplicationRoles.Add(applicationRole); var rolePermissionMappigs = new List { new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ModifyTenant }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ViewTenant }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ManageMasters }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ViewMasters }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ViewOrganization }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.AddOrganization }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.EditOrganization } }; _context.RolePermissionMappings.AddRange(rolePermissionMappigs); _context.EmployeeRoleMappings.Add(new EmployeeRoleMapping { EmployeeId = employeeUser.Id, RoleId = applicationRole.Id, IsEnabled = true, TenantId = tenantId }); // 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 = tenantEnquire.BillingAddress, StartDate = onBoardingDate, EndDate = DateTime.MaxValue, PromoterId = organization.Id, PMCId = organization.Id, ContactPerson = tenant.ContactName, TenantId = tenantId }; _context.Projects.Add(project); var projectAllocation = new ProjectAllocation { ProjectId = project.Id, EmployeeId = employeeUser.Id, AllocationDate = onBoardingDate, IsActive = true, JobRoleId = adminJobRole.Id, TenantId = tenantId }; _context.ProjectAllocations.Add(projectAllocation); // 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(employeeUser); // Commit the transaction as all operations were successful. await transaction.CommitAsync(); await AddSubscriptionAsync(tenantId, employeeId, paymentDetailId, planId); _logger.LogInfo("Successfully created tenant {TenantId} for organization {OrganizationName}.", tenant.Id, tenant.Name); return 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}", tenantEnquire.Email, dbEx.InnerException?.Message ?? string.Empty); return ApiResponse.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500); } catch (Exception ex) { // Log the general exception. _logger.LogError(ex, "An unexpected exception occurred while creating tenant for email {Email}.", tenantEnquire.Email); return ApiResponse.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500); } } public async Task> AddSubscriptionAsync(Guid tenantId, Guid employeeId, Guid paymentDetailId, Guid planId) { _logger.LogInfo("AddSubscription called for Tenant {TenantId} and Plan {PlanId}", tenantId, planId); await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var paymentDetail = await _context.PaymentDetails.FirstOrDefaultAsync(pd => pd.Id == paymentDetailId); if (paymentDetail == null) { _logger.LogWarning("Payment Details {PaymentDetailsId} not found in database", paymentDetailId); return ApiResponse.ErrorResponse("Payment Details not found", "Payment Details not found", 404); } var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId); if (tenant == null) { _logger.LogWarning("Tenant {TenantId} not found in database", tenantId); return ApiResponse.ErrorResponse("Tenant not found", "Tenant not found", 404); } var subscriptionPlan = await _context.SubscriptionPlanDetails.Include(sp => sp.Plan).FirstOrDefaultAsync(sp => sp.Id == planId); if (subscriptionPlan == null) { _logger.LogWarning("Subscription plan {PlanId} not found in database", planId); return 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 > subscriptionPlan.MaxUser) { _logger.LogWarning("Add less max user than the active user in the tenant {TenantId}", tenant.Id); return 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 = tenantId, PlanId = planId, StatusId = activePlanStatus, CreatedAt = utcNow, MaxUsers = subscriptionPlan.MaxUser, CreatedById = employeeId, CurrencyId = subscriptionPlan.CurrencyId, PaymentDetailId = paymentDetailId, IsTrial = false, StartDate = utcNow, EndDate = endDate, NextBillingDate = endDate, AutoRenew = false }; _context.TenantSubscriptions.Add(tenantSubscription); try { await _context.SaveChangesAsync(); _logger.LogInfo("Tenant subscription added successfully for Tenant {TenantId}, Plan {PlanId}", tenantId, planId); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database exception while adding subscription plan to tenant {TenantId}", tenantId); return ApiResponse.ErrorResponse("Internal error occured", ExceptionMapper(dbEx), 500); } try { var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId); if (features == null) { _logger.LogInfo("No features found for subscription plan {PlanId}", planId); await transaction.CommitAsync(); return 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.OrganizationId == tenant.OrganizationId); if (rootEmployee == null) { _logger.LogWarning("Root employee not found for tenant {TenantId}", tenantId); await transaction.CommitAsync(); return ApiResponse.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200); } var roleId = await _context.EmployeeRoleMappings .AsNoTracking() .Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == tenantId) .Select(er => er.RoleId) .FirstOrDefaultAsync(); if (roleId == Guid.Empty) { _logger.LogWarning("RoleId for root employee {EmployeeId} in tenant {TenantId} not found", rootEmployee.Id, tenantId); await transaction.CommitAsync(); return 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 _cache = scope.ServiceProvider.GetRequiredService(); await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id); 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); _context.ExpensesTypeMaster.AddRange(expensesTypeMaster); } await _context.SaveChangesAsync(); await transaction.CommitAsync(); _logger.LogInfo("Permissions updated successfully for tenant {TenantId} subscription", tenantId); return ApiResponse.SuccessResponse(tenantSubscription, "Tenant Subscription Successfully", 200); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "Exception occurred while updating permissions for tenant {TenantId}", tenantId); return ApiResponse.ErrorResponse("Internal error occured", ExceptionMapper(ex), 500); } } public async Task> RenewSubscriptionAsync(Guid tenantId, Guid employeeId, Guid paymentDetailId, Guid planId) { // 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 hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageTenants, employeeId); if (!hasPermission) { _logger.LogWarning("Permission denied for EmployeeId={EmployeeId}. HasPermission: {HasPermission}", employeeId, hasPermission); return 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 == tenantId)).Unwrap(); var planDetailsTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => ctx.Result.SubscriptionPlanDetails.Include(sp => sp.Plan).AsNoTracking().FirstOrDefaultAsync(sp => sp.Id == planId)).Unwrap(); var currentSubscriptionTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => ctx.Result.TenantSubscriptions.Include(ts => ts.Currency).AsNoTracking().FirstOrDefaultAsync(ts => ts.TenantId == tenantId && !ts.IsCancelled)).Unwrap(); var paymentDetailsTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => ctx.Result.PaymentDetails.AsNoTracking().FirstOrDefaultAsync(pd => pd.Id == paymentDetailId)).Unwrap(); await Task.WhenAll(tenantTask, planDetailsTask, currentSubscriptionTask, paymentDetailsTask); var tenant = tenantTask.Result; if (tenant == null) { _logger.LogWarning("Tenant {TenantId} not found.", tenantId); return ApiResponse.ErrorResponse("Tenant not found", "Tenant not found", 404); } var subscriptionPlan = planDetailsTask.Result; if (subscriptionPlan == null) { _logger.LogWarning("Subscription plan {PlanId} not found.", planId); return ApiResponse.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404); } var paymentDetail = paymentDetailsTask.Result; if (paymentDetail == null) { _logger.LogWarning("Payment details {PaymentDetailId} not found", paymentDetailId); return ApiResponse.ErrorResponse("Payment details not found", "Payment details 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 > subscriptionPlan.MaxUser) { _logger.LogWarning("Employee {EmployeeId} add less max user than the active user in the tenant {TenantId}", employeeId, tenant.Id); return ApiResponse.ErrorResponse("Invalid Max user count", "Max User count must be higher than active user count", 400); } // 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 = tenantId, PlanId = planId, StatusId = activePlanStatus, CreatedAt = utcNow, MaxUsers = subscriptionPlan.MaxUser, CreatedById = employeeId, CurrencyId = subscriptionPlan.CurrencyId, PaymentDetailId = paymentDetailId, 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 = employeeId; context.TenantSubscriptions.Update(currentSubscription); } await context.SaveChangesAsync(); _logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}", tenantId, planId); _ = Task.Run(async () => { await ClearPermissionForTenant(tenantId); }); // 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}.", planId); await transaction.CommitAsync(); return 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 == tenantId); if (rootEmployee == null) { _logger.LogWarning("No root employee for Tenant={TenantId}.", tenantId); await transaction.CommitAsync(); return ApiResponse.SuccessResponse(newSubscription, "Tenant subscription updated (no root employee)", 200); } var rootRoleId = await context.EmployeeRoleMappings .AsNoTracking() .Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == tenantId) .Select(er => er.RoleId) .FirstOrDefaultAsync(); if (rootRoleId == Guid.Empty) { _logger.LogWarning("No root role for Employee={EmployeeId}, Tenant={TenantId}.", rootEmployee.Id, tenantId); await transaction.CommitAsync(); return 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 _cache = scope.ServiceProvider.GetRequiredService(); await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id); 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 expensesTypeNames = expensesTypeMaster.Select(et => et.Name).ToList(); var expensesTypeExist = await context.ExpensesTypeMaster.AnyAsync(et => expensesTypeNames.Contains(et.Name) && et.TenantId == tenant.Id); if (!expensesTypeExist) { context.ExpensesTypeMaster.AddRange(expensesTypeMaster); } } await context.SaveChangesAsync(); await transaction.CommitAsync(); _logger.LogInfo("Tenant subscription and permissions updated: Tenant={TenantId}", tenantId); return ApiResponse.SuccessResponse(newSubscription, "Tenant subscription successfully updated", 200); } catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); _logger.LogError(dbEx, "Database exception updating subscription for TenantId={TenantId}", tenantId); return ApiResponse.ErrorResponse("Internal database error", ExceptionMapper(dbEx), 500); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "General exception for TenantId={TenantId}", tenantId); return ApiResponse.ErrorResponse("Internal error occurred", ExceptionMapper(ex), 500); } } #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; } } /// /// 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(Guid tenantId) { 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 } }