546 lines
26 KiB
C#
546 lines
26 KiB
C#
using AutoMapper;
|
|
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.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;
|
|
using System.Text.RegularExpressions;
|
|
|
|
// 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<ApplicationDbContext> _dbContextFactory;
|
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
private readonly ILoggingService _logger;
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
private readonly IMapper _mapper;
|
|
private readonly UserHelper _userHelper;
|
|
private readonly static Guid activeStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191");
|
|
private readonly static string AdminRoleName = "Admin";
|
|
public TenantController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
|
IServiceScopeFactory serviceScopeFactory,
|
|
ILoggingService logger,
|
|
UserManager<ApplicationUser> userManager,
|
|
IMapper mapper,
|
|
UserHelper userHelper)
|
|
{
|
|
_dbContextFactory = dbContextFactory;
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
_logger = logger;
|
|
_userManager = userManager;
|
|
_mapper = mapper;
|
|
_userHelper = userHelper;
|
|
}
|
|
#region =================================================================== Tenant APIs ===================================================================
|
|
|
|
/// <summary>
|
|
/// Retrieves a paginated list of active tenants with optional filtering and searching.
|
|
/// </summary>
|
|
/// <param name="searchString">A string to search for across various tenant fields.</param>
|
|
/// <param name="filter">A JSON serialized string containing advanced filter criteria.</param>
|
|
/// <param name="pageSize">The number of records to return per page.</param>
|
|
/// <param name="pageNumber">The page number to retrieve.</param>
|
|
/// <returns>A paginated list of tenants matching the criteria.</returns>
|
|
[HttpGet("list")]
|
|
public async Task<IActionResult> GetTenantList([FromQuery] string? searchString, string? filter, int pageSize = 20, int pageNumber = 1)
|
|
{
|
|
|
|
using var scope = _serviceScopeFactory.CreateScope();
|
|
|
|
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
_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<object>.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<object>.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<List<TenantListVM>>(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<object>.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<object>.ErrorResponse("An internal server error occurred.", "An unexpected error prevented the request from completing.", 500));
|
|
}
|
|
}
|
|
|
|
// GET api/<TenantController>/5
|
|
[HttpGet("details/{id}")]
|
|
public async Task<IActionResult> GetDetails(Guid id)
|
|
{
|
|
return Ok();
|
|
}
|
|
|
|
// POST api/<TenantController>
|
|
[HttpPost("create")]
|
|
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto model)
|
|
{
|
|
using var scope = _serviceScopeFactory.CreateScope();
|
|
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
|
|
|
var _configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
|
var _emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
|
|
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
|
|
_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<object>.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<object>.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<object>.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))
|
|
{
|
|
return await _context.Tenants.AnyAsync(t => t.TaxId == model.TaxId);
|
|
|
|
}
|
|
return false;
|
|
});
|
|
var domainTask = Task.Run(async () =>
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(model.DomainName))
|
|
{
|
|
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<object>.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<object>.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<Tenant>(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<object>.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,
|
|
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<RolePermissionMappings> {
|
|
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 = $"{model.OrganizationName} - Default Project",
|
|
ProjectStatusId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"), // Consider using a constant for this GUID
|
|
ProjectAddress = model.BillingAddress,
|
|
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<TenantVM>(tenant);
|
|
tenantVM.CreatedBy = _mapper.Map<BasicEmployeeVM>(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<TenantVM>.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<object>.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<object>.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500));
|
|
}
|
|
}
|
|
|
|
// PUT api/<TenantController>/5
|
|
[HttpPut("{id}")]
|
|
public void Put(int id, [FromBody] string value)
|
|
{
|
|
}
|
|
|
|
// DELETE api/<TenantController>/5
|
|
[HttpDelete("{id}")]
|
|
public void Delete(int id)
|
|
{
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region =================================================================== Subscription APIs ===================================================================
|
|
|
|
#endregion
|
|
|
|
#region =================================================================== Subscription Plan APIs ===================================================================
|
|
|
|
#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;
|
|
|
|
// Normalize string
|
|
input = input.Trim();
|
|
|
|
// Length must be multiple of 4
|
|
if (input.Length % 4 != 0)
|
|
return false;
|
|
|
|
// Valid Base64 characters with correct padding
|
|
var base64Regex = new Regex(@"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$");
|
|
if (!base64Regex.IsMatch(input))
|
|
return false;
|
|
|
|
try
|
|
{
|
|
// Decode and re-encode to confirm validity
|
|
var bytes = Convert.FromBase64String(input);
|
|
var reEncoded = Convert.ToBase64String(bytes);
|
|
return input == reEncoded;
|
|
}
|
|
catch
|
|
{
|
|
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<TenantFilter>(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<string>(filter, options) ?? "";
|
|
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
|
{
|
|
expenseFilter = JsonSerializer.Deserialize<TenantFilter>(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
|
|
}
|
|
}
|