using AutoMapper; using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Roles; using Marco.Pms.Model.TenantModel; 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.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 static Guid activeStatus = 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; _serviceScopeFactory = serviceScopeFactory; _logger = logger; _userManager = userManager; _mapper = mapper; _userHelper = userHelper; _featureDetailsHelper = featureDetailsHelper; } #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 loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { // This case should be handled by the [Authorize] attribute. // This check is a safeguard. _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 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 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); // 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 GetDetails(Guid id) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); var manageTenantsTask = Task.Run(async () => { using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); return await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); }); var modifyTenantTask = Task.Run(async () => { using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); return await _permissionService.HasPermission(PermissionsMaster.ModifyTenant, loggedInEmployee.Id); }); var viewTenantTask = Task.Run(async () => { using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); return await _permissionService.HasPermission(PermissionsMaster.ViewTenant, loggedInEmployee.Id); }); await Task.WhenAll(manageTenantsTask, modifyTenantTask, viewTenantTask); var hasManageTenantsPermission = manageTenantsTask.Result; var hasModifyTenantPermission = modifyTenantTask.Result; var hasViewTenantPermission = viewTenantTask.Result; if (!hasManageTenantsPermission && !hasModifyTenantPermission && !hasViewTenantPermission) { _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 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)); } var employeeTask = Task.Run(async () => { await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); return await _dbContext.Employees.Include(e => e.ApplicationUser).AsNoTracking().Where(e => e.TenantId == tenant.Id).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 planTask = Task.Run(async () => { await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); return await _dbContext.TenantSubscriptions.AsNoTracking().Where(ts => ts.TenantId == tenant.Id && !ts.IsCancelled && ts.Plan != null).FirstOrDefaultAsync(); }); var projectTask = Task.Run(async () => { await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); return await _dbContext.Projects .Include(p => p.ProjectStatus) .AsNoTracking() .Where(p => p.TenantId == tenant.Id) .GroupBy(p => p.ProjectStatusId) .Select(g => new { Status = g.Where(p => p.ProjectStatus != null && p.ProjectStatus.Id == g.Key).Select(p => p.ProjectStatus).FirstOrDefault(), ProjectsCount = g.Where(p => p.ProjectStatusId == g.Key).Count() }) .ToListAsync(); }); await Task.WhenAll(employeeTask, projectTask, planTask, createdByTask); var employees = employeeTask.Result; var projects = projectTask.Result; var currentPlan = planTask.Result; var createdBy = createdByTask.Result; var activeEmployeesCount = employees.Where(e => e.IsActive).Count(); var inActiveEmployeesCount = employees.Where(e => !e.IsActive).Count(); var expiryDate = currentPlan?.EndDate; var nextBillingDate = currentPlan?.NextBillingDate; var response = _mapper.Map(tenant); response.ActiveEmployees = activeEmployeesCount; response.InActiveEmployees = inActiveEmployeesCount; response.Projects = projects; response.ExpiryDate = expiryDate; response.NextBillingDate = nextBillingDate; response.CreatedBy = createdBy; return Ok(); } // 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 Unauthorized(ApiResponse.ErrorResponse("Authentication required", "User is not logged in.", 401)); } 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 { // Create the primary Tenant entity var tenant = _mapper.Map(model); tenant.TenantStatusId = activeStatus; tenant.CreatedById = loggedInEmployee.Id; tenant.IsSuperTenant = false; _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 TenantId = tenant.Id }; // 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, TenantId = tenant.Id }; _context.Employees.Add(employeeUser); var applicationRole = new ApplicationRole { Role = "Super User", Description = "Super User", IsSystem = true, TenantId = tenant.Id }; _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 } }; _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, ContactPerson = tenant.ContactName, TenantId = tenant.Id }; _context.Projects.Add(project); // 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)); } } // PUT api//5 [HttpPut("{id}")] public void Put(int id, [FromBody] string value) { } // DELETE api//5 [HttpDelete("{id}")] public void Delete(int id) { } #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 Unauthorized(ApiResponse.ErrorResponse("Unauthorized", "User must be logged in.", 401)); } 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)); } 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.AddMonths(1), PLAN_FREQUENCY.QUARTERLY => utcNow.AddMonths(3), PLAN_FREQUENCY.HALF_MONTHLY => utcNow.AddMonths(6), PLAN_FREQUENCY.YEARLY => utcNow.AddMonths(12), _ => 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 { 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); } 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(); // // 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 subscriptionPlanTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => // ctx.Result.SubscriptionPlans.AsNoTracking().FirstOrDefaultAsync(sp => sp.Id == model.PlanId)).Unwrap(); // var currentSubscriptionTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(ctx => // ctx.Result.TenantSubscriptions.AsNoTracking().FirstOrDefaultAsync(ts => ts.TenantId == model.TenantId)).Unwrap(); // await Task.WhenAll(tenantTask, subscriptionPlanTask, 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 = subscriptionPlanTask.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; // // 6. If the tenant already has this plan, extend/renew it. // if (currentSubscription != null && currentSubscription.PlanId == model.PlanId) // { // DateTime newEndDate; // // 6a. If the subscription is still active, extend from current EndDate; else start from now. // if (currentSubscription.EndDate.Date >= utcNow.Date) // { // newEndDate = model.Frequency switch // { // PLAN_FREQUENCY.MONTHLY => currentSubscription.EndDate.AddMonths(1), // PLAN_FREQUENCY.QUARTERLY => currentSubscription.EndDate.AddMonths(3), // PLAN_FREQUENCY.HALF_MONTHLY => currentSubscription.EndDate.AddMonths(6), // PLAN_FREQUENCY.YEARLY => currentSubscription.EndDate.AddMonths(12), // _ => currentSubscription.EndDate // }; // } // else // { // newEndDate = model.Frequency switch // { // PLAN_FREQUENCY.MONTHLY => utcNow.AddMonths(1), // PLAN_FREQUENCY.QUARTERLY => utcNow.AddMonths(3), // PLAN_FREQUENCY.HALF_MONTHLY => utcNow.AddMonths(6), // PLAN_FREQUENCY.YEARLY => utcNow.AddMonths(12), // _ => utcNow // }; // } // // 6b. Update subscription details // if (model.MaxUsers != null && model.MaxUsers > 0) // { // currentSubscription.MaxUsers = model.MaxUsers.Value; // } // 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); // 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 = model.Frequency switch // { // PLAN_FREQUENCY.MONTHLY => utcNow.AddMonths(1), // PLAN_FREQUENCY.QUARTERLY => utcNow.AddMonths(3), // PLAN_FREQUENCY.HALF_MONTHLY => utcNow.AddMonths(6), // PLAN_FREQUENCY.YEARLY => utcNow.AddMonths(12), // _ => utcNow // }; // 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); // // 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. Prepare add and remove permission lists. // var newPermissionIds = new List(); // var revokePermissionIds = new List(); // void ProcessPerms(bool? enabled, List ids) // { // if (enabled == true) newPermissionIds.AddRange(ids); // else 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(); // // 8d. 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(); // // 8e. Prevent accidental loss of basic employee permissions. // if (oldPermIds.Count - revokePermissionIds.Count <= 4 && revokePermissionIds.Any()) // { // var employeePerms = employeePermTask.Result; // 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); // } // 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); 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.")); } } [HttpPost("create/subscription-plan")] public async Task CreateSubscriptionPlan1([FromBody] SubscriptionPlanDto model) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); // A root user should have access regardless of the specific 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 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)); } var plan = _mapper.Map(model); _context.SubscriptionPlans.Add(plan); List response = new List(); if (model.MonthlyPlan != null) { var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.MonthlyPlan.CurrencyId); if (currencyMaster == null) { return NotFound(ApiResponse.ErrorResponse("Currency not found", "Currency not found", 404)); } var monthlyPlan = _mapper.Map(model.MonthlyPlan); var features = _mapper.Map(model.MonthlyPlan.Features); try { await _featureDetailsHelper.AddFeatureDetails(features); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while saving feature in mongoDB"); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500)); } monthlyPlan.PlanId = plan.Id; monthlyPlan.Frequency = PLAN_FREQUENCY.MONTHLY; monthlyPlan.FeaturesId = features.Id; monthlyPlan.CreatedById = loggedInEmployee.Id; monthlyPlan.CreateAt = DateTime.UtcNow; _context.SubscriptionPlanDetails.Add(monthlyPlan); var VM = _mapper.Map(monthlyPlan); VM.PlanName = plan.PlanName; VM.Description = plan.Description; VM.Currency = currencyMaster; response.Add(VM); } if (model.QuarterlyPlan != null) { var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.QuarterlyPlan.CurrencyId); if (currencyMaster == null) { return NotFound(ApiResponse.ErrorResponse("Currency not found", "Currency not found", 404)); } var quarterlyPlan = _mapper.Map(model.QuarterlyPlan); var features = _mapper.Map(model.QuarterlyPlan.Features); try { await _featureDetailsHelper.AddFeatureDetails(features); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while saving feature in mongoDB"); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500)); } quarterlyPlan.PlanId = plan.Id; quarterlyPlan.Frequency = PLAN_FREQUENCY.QUARTERLY; quarterlyPlan.FeaturesId = features.Id; quarterlyPlan.CreatedById = loggedInEmployee.Id; quarterlyPlan.CreateAt = DateTime.UtcNow; _context.SubscriptionPlanDetails.Add(quarterlyPlan); var VM = _mapper.Map(quarterlyPlan); VM.PlanName = plan.PlanName; VM.Description = plan.Description; VM.Currency = currencyMaster; response.Add(VM); } if (model.HalfYearlyPlan != null) { var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.HalfYearlyPlan.CurrencyId); if (currencyMaster == null) { return NotFound(ApiResponse.ErrorResponse("Currency not found", "Currency not found", 404)); } var halfYearlyPlan = _mapper.Map(model.HalfYearlyPlan); var features = _mapper.Map(model.HalfYearlyPlan.Features); try { await _featureDetailsHelper.AddFeatureDetails(features); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while saving feature in mongoDB"); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500)); } halfYearlyPlan.PlanId = plan.Id; halfYearlyPlan.Frequency = PLAN_FREQUENCY.HALF_MONTHLY; halfYearlyPlan.FeaturesId = features.Id; halfYearlyPlan.CreatedById = loggedInEmployee.Id; halfYearlyPlan.CreateAt = DateTime.UtcNow; _context.SubscriptionPlanDetails.Add(halfYearlyPlan); var VM = _mapper.Map(halfYearlyPlan); VM.PlanName = plan.PlanName; VM.Description = plan.Description; VM.Currency = currencyMaster; response.Add(VM); } if (model.YearlyPlan != null) { var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.YearlyPlan.CurrencyId); if (currencyMaster == null) { return NotFound(ApiResponse.ErrorResponse("Currency not found", "Currency not found", 404)); } var yearlyPlan = _mapper.Map(model.YearlyPlan); var features = _mapper.Map(model.YearlyPlan.Features); try { await _featureDetailsHelper.AddFeatureDetails(features); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while saving feature in mongoDB"); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500)); } yearlyPlan.PlanId = plan.Id; yearlyPlan.Frequency = PLAN_FREQUENCY.YEARLY; yearlyPlan.FeaturesId = features.Id; yearlyPlan.CreatedById = loggedInEmployee.Id; yearlyPlan.CreateAt = DateTime.UtcNow; _context.SubscriptionPlanDetails.Add(yearlyPlan); var VM = _mapper.Map(yearlyPlan); VM.PlanName = plan.PlanName; VM.Description = plan.Description; VM.Currency = currencyMaster; response.Add(VM); } try { await _context.SaveChangesAsync(); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while saving subscription plan"); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500)); } return StatusCode(201, ApiResponse.SuccessResponse(response, "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; } #endregion } }