721 lines
36 KiB
C#

using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Expenses;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Expenses;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Expanses;
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ExpenseController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper;
private readonly PermissionServices _permission;
private readonly ILoggingService _logger;
private readonly S3UploadService _s3Service;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IMapper _mapper;
private readonly Guid tenantId;
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
public ExpenseController(
IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context,
UserHelper userHelper,
PermissionServices permission,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
S3UploadService s3Service,
IMapper mapper)
{
_dbContextFactory = dbContextFactory;
_context = context;
_userHelper = userHelper;
_permission = permission;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_s3Service = s3Service;
_mapper = mapper;
tenantId = userHelper.GetTenantId();
}
/// <summary>
/// Retrieves a paginated list of expenses based on user permissions and optional filters.
/// </summary>
/// <param name="filter">A URL-encoded JSON string containing filter criteria. See <see cref="ExpensesFilter"/>.</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 expenses.</returns>
[HttpGet("list")] // Assuming this is a GET endpoint
public async Task<IActionResult> GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1)
{
try
{
_logger.LogInfo(
"Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
pageNumber, pageSize, filter ?? "");
// 1. --- Get User and Permissions ---
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (loggedInEmployee == null)
{
// This is an authentication/authorization issue. The user should be logged in.
_logger.LogWarning("Could not find an employee for the current logged-in user.");
return Unauthorized(ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 401));
}
Guid loggedInEmployeeId = loggedInEmployee.Id;
var hasViewSelfPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
});
var hasViewAllPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
});
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
// 2. --- Build Base Query and Apply Permissions ---
// Start with a base IQueryable. Filters will be chained onto this.
var expensesQuery = _context.Expenses
.Include(e => e.ExpensesType)
.Include(e => e.Project)
.Include(e => e.PaidBy)
.ThenInclude(e => e!.JobRole)
.Include(e => e.PaymentMode)
.Include(e => e.Status)
.Include(e => e.CreatedBy)
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
// Apply permission-based filtering BEFORE any other filters or pagination.
if (hasViewAllPermissionTask.Result)
{
// User has 'View All' permission, no initial restriction on who created the expense.
_logger.LogInfo("User {EmployeeId} has 'View All' permission.", loggedInEmployeeId);
}
else if (hasViewSelfPermissionTask.Result)
{
// User only has 'View Self' permission, so restrict the query to their own expenses.
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
}
else
{
// User has neither required permission. Deny access.
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200));
}
// 3. --- Deserialize Filter and Apply ---
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
if (expenseFilter != null)
{
// CRITICAL FIX: Apply filters cumulatively using multiple `if` statements, not `if-else if`.
if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
{
expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date);
}
if (expenseFilter.ProjectIds?.Any() == true)
{
expensesQuery = expensesQuery.Where(e => expenseFilter.ProjectIds.Contains(e.ProjectId));
}
if (expenseFilter.StatusIds?.Any() == true)
{
expensesQuery = expensesQuery.Where(e => expenseFilter.StatusIds.Contains(e.StatusId));
}
if (expenseFilter.PaidById?.Any() == true)
{
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
}
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
{
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
}
}
// 4. --- Apply Ordering and Pagination ---
// This should be the last step before executing the query.
var paginatedQuery = expensesQuery
.OrderByDescending(e => e.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
// 5. --- Execute Query and Map Results ---
var expensesList = await paginatedQuery.ToListAsync();
if (!expensesList.Any())
{
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200));
}
var response = _mapper.Map<List<ExpenseList>>(expensesList);
// 6. --- Efficiently Fetch and Append 'Next Status' Information ---
var statusIds = expensesList.Select(e => e.StatusId).Distinct().ToList();
var statusMappings = await _context.ExpensesStatusMapping
.Include(sm => sm.NextStatus)
.Where(sm => statusIds.Contains(sm.StatusId))
.ToListAsync();
// Use a Lookup for efficient O(1) mapping. This is much better than repeated `.Where()` in a loop.
var statusMapLookup = statusMappings.ToLookup(sm => sm.StatusId);
foreach (var expense in response)
{
if (expense.Status?.Id != null && statusMapLookup.Contains(expense.Status.Id))
{
expense.NextStatus = statusMapLookup[expense.Status.Id]
.Select(sm => _mapper.Map<ExpensesStatusMasterVM>(sm.NextStatus))
.ToList();
}
else
{
expense.NextStatus = new List<ExpensesStatusMasterVM>(); // Ensure it's never null
}
}
// 7. --- Return Final Success Response ---
var message = $"{response.Count} expense records fetched successfully.";
_logger.LogInfo(message);
return StatusCode(200, ApiResponse<object>.SuccessResponse(response, message, 200));
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Databsae Exception occured while fetching list expenses");
return BadRequest(ApiResponse<object>.ErrorResponse("Databsae Exception", new
{
Message = dbEx.Message,
StackTrace = dbEx.StackTrace,
Source = dbEx.Source,
innerexcption = new
{
Message = dbEx.InnerException?.Message,
StackTrace = dbEx.InnerException?.StackTrace,
Source = dbEx.InnerException?.Source,
}
}, 400));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while fetching list expenses");
return BadRequest(ApiResponse<object>.ErrorResponse("Error Occured", new
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
innerexcption = new
{
Message = ex.InnerException?.Message,
StackTrace = ex.InnerException?.StackTrace,
Source = ex.InnerException?.Source,
}
}, 400));
}
}
[HttpGet("details/{id}")]
public string Get(int id)
{
return "value";
}
[HttpPost("create")]
public async Task<IActionResult> CreateExpense1([FromBody] CreateExpensesDto dto)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasUploadPermission = await _permission.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, dto.ProjectId);
if (!hasUploadPermission || !hasProjectPermission)
{
_logger.LogWarning("Access DENIED for employee {EmployeeId} for uploading expense on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to Upload expenses for this project", 403));
}
var isExpensesTypeExist = await _context.ExpensesTypeMaster.AnyAsync(et => et.Id == dto.ExpensesTypeId);
if (!isExpensesTypeExist)
{
_logger.LogWarning("Expenses type not for ID: {ExpensesTypeId} when creating new expense", dto.ExpensesTypeId);
return NotFound(ApiResponse<object>.ErrorResponse("Expanses Type not found", "Expanses Type not found", 404));
}
var isPaymentModeExist = await _context.PaymentModeMatser.AnyAsync(et => et.Id == dto.PaymentModeId);
if (!isPaymentModeExist)
{
_logger.LogWarning("Payment Mode not for ID: {PaymentModeId} when creating new expense", dto.PaymentModeId);
return NotFound(ApiResponse<object>.ErrorResponse("Payment Mode not found", "Payment Mode not found", 404));
}
var isStatusExist = await _context.ExpensesStatusMaster.AnyAsync(et => et.Id == Draft);
if (!isStatusExist)
{
_logger.LogWarning("Status not for ID: {PaymentModeId} when creating new expense", dto.PaymentModeId);
return NotFound(ApiResponse<object>.ErrorResponse("Status not found", "Status not found", 404));
}
var expense = _mapper.Map<Expenses>(dto);
expense.CreatedById = loggedInEmployee.Id;
expense.CreatedAt = DateTime.UtcNow;
expense.TenantId = tenantId;
expense.IsActive = true;
expense.StatusId = Draft;
_context.Expenses.Add(expense);
Guid batchId = Guid.NewGuid();
foreach (var attachment in dto.BillAttachments)
{
if (!_s3Service.IsBase64String(attachment.Base64Data))
{
_logger.LogWarning("Image upload failed: Base64 data is missing While creating new expense entity for project {ProjectId} by employee {EmployeeId}", expense.ProjectId, expense.PaidById);
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
var base64 = attachment.Base64Data!.Contains(',')
? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..]
: attachment.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
try
{
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
_logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while saving image to S3");
return BadRequest(ApiResponse<object>.ErrorResponse("Cannot upload attachment to S3", new
{
message = ex.Message,
innerexcption = ex.InnerException?.Message,
stackTrace = ex.StackTrace,
source = ex.Source
}, 400));
}
var document = new Document
{
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = attachment.FileName ?? "",
ContentType = attachment.ContentType ?? "",
S3Key = objectKey,
FileSize = attachment.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
var billAttachement = new BillAttachments
{
DocumentId = document.Id,
ExpensesId = expense.Id,
TenantId = tenantId
};
_context.BillAttachments.Add(billAttachement);
}
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Error occured while saving Expense, Document and bill attachment entity");
return BadRequest(ApiResponse<object>.ErrorResponse("Databsae Exception", new
{
Message = dbEx.Message,
StackTrace = dbEx.StackTrace,
Source = dbEx.Source,
innerexcption = new
{
Message = dbEx.InnerException?.Message,
StackTrace = dbEx.InnerException?.StackTrace,
Source = dbEx.InnerException?.Source,
}
}, 400));
}
_logger.LogInfo("Documents and attachments saved for Expense: {ExpenseId}", expense.Id);
return StatusCode(201, ApiResponse<object>.SuccessResponse(expense, "Expense created Successfully", 201));
}
/// <summary>
/// Creates a new expense entry along with its bill attachments.
/// This operation is transactional and performs validations and file uploads in parallel for optimal performance.
/// Permission checks are also run in parallel using IServiceScopeFactory to ensure thread safety.
/// </summary>
/// <param name="dto">The data transfer object containing expense details and attachments.</param>
/// <returns>An IActionResult indicating the result of the creation operation.</returns>
[HttpPost]
public async Task<IActionResult> CreateExpense([FromBody] CreateExpensesDto dto)
{
_logger.LogDebug("CreateExpense for Project {ProjectId}", dto.ProjectId);
// The entire operation is wrapped in a transaction to ensure data consistency.
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// 1. Authorization: Run permission checks in parallel using a service scope for each task.
// This is crucial for thread-safety as IPermissionService is a scoped service.
var hasUploadPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
});
var hasProjectPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
});
await Task.WhenAll(hasUploadPermissionTask, hasProjectPermissionTask);
if (!hasUploadPermissionTask.Result || !hasProjectPermissionTask.Result)
{
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403));
}
// 2. Validation: Check if prerequisite entities exist.
// The method now returns a tuple indicating success or failure.
// Each task creates its own DbContext instance from the factory, making the parallel calls thread-safe.
var projectGetTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.FirstOrDefaultAsync(p => p.Id == dto.ProjectId);
});
var expenseTypeGetTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesTypeMaster.FirstOrDefaultAsync(et => et.Id == dto.ExpensesTypeId);
});
var paymentModeGetTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
});
var statusGetTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesStatusMaster.FirstOrDefaultAsync(es => es.Id == Draft);
});
await Task.WhenAll(projectGetTask, expenseTypeGetTask, paymentModeGetTask, statusGetTask);
var project = await projectGetTask;
var expenseType = await expenseTypeGetTask;
var paymentMode = await paymentModeGetTask;
var status = await statusGetTask;
if (project == null)
{
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
_logger.LogWarning("Expense creation failed due to validation: Project with ID {ProjectId} not found.", dto.ProjectId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404));
}
else if (expenseType == null)
{
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
_logger.LogWarning("Expense creation failed due to validation: Expense Type with ID {ExpensesTypeId} not found.", dto.ExpensesTypeId);
return NotFound(ApiResponse<object>.ErrorResponse("Expense Type not found.", "Expense Type not found.", 404));
}
else if (paymentMode == null)
{
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
_logger.LogWarning("Expense creation failed due to validation: Payment Mode with ID {PaymentModeId} not found.", dto.PaymentModeId);
return NotFound(ApiResponse<object>.ErrorResponse("Payment Mode not found.", "Payment Mode not found.", 404));
}
else if (status == null)
{
await transaction.RollbackAsync(); // Ensure transaction is terminated before returning.
_logger.LogWarning("Expense creation failed due to validation: Status with ID {StatusId} not found.", Draft);
return NotFound(ApiResponse<object>.ErrorResponse("Status not found.", "Status not found.", 404));
}
// 3. Entity Creation
var expense = _mapper.Map<Expenses>(dto);
expense.CreatedById = loggedInEmployee.Id;
expense.CreatedAt = DateTime.UtcNow;
expense.TenantId = tenantId;
expense.IsActive = true;
expense.StatusId = Draft;
_context.Expenses.Add(expense);
// 4. Process Attachments
if (dto.BillAttachments?.Any() ?? false)
{
await ProcessAndUploadAttachmentsAsync(dto.BillAttachments, expense, loggedInEmployee.Id, tenantId);
}
// 5. Database Commit
await _context.SaveChangesAsync();
// 6. Transaction Commit
await transaction.CommitAsync();
var response = _mapper.Map<ExpenseList>(expense);
response.Project = _mapper.Map<ProjectInfoVM>(project);
response.Status = _mapper.Map<ExpensesStatusMasterVM>(status);
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);
_logger.LogInfo("Successfully created Expense {ExpenseId} for Project {ProjectId}.", expense.Id, expense.ProjectId);
return StatusCode(201, ApiResponse<object>.SuccessResponse(response, "Expense created successfully.", 201));
}
catch (ArgumentException ex) // For invalid Base64 or other bad arguments.
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Invalid argument provided during expense creation for project {ProjectId}.", dto.ProjectId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Request Data", new
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
innerexcption = new
{
Message = ex.InnerException?.Message,
StackTrace = ex.InnerException?.StackTrace,
Source = ex.InnerException?.Source,
}
}, 400));
}
catch (Exception ex) // General-purpose catch for unexpected errors (e.g., S3 failure).
{
await transaction.RollbackAsync();
_logger.LogError(ex, "An unhandled exception occurred while creating an expense for project {ProjectId}.", dto.ProjectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", new
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
innerexcption = new
{
Message = ex.InnerException?.Message,
StackTrace = ex.InnerException?.StackTrace,
Source = ex.InnerException?.Source,
}
}, 500));
}
}
/// <summary>
/// Processes and uploads attachments in parallel, then adds the resulting entities to the main DbContext.
/// </summary>
private async Task ProcessAndUploadAttachmentsAsync(IEnumerable<FileUploadModel> attachments, Expenses expense, Guid employeeId, Guid tenantId)
{
var batchId = Guid.NewGuid();
var processingTasks = attachments.Select(attachment => Task.Run(async () =>
{
if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data))
throw new ArgumentException("Invalid or missing Base64 data for an attachment.");
var base64Data = attachment.Base64Data!.Contains(',') ? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..] : attachment.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
// Upload and create entities
await _s3Service.UploadFileAsync(base64Data, fileType, objectKey);
_logger.LogInfo("Uploaded file to S3 with key: {ObjectKey}", objectKey);
return CreateAttachmentEntities(batchId, expense.Id, employeeId, tenantId, objectKey, attachment);
})).ToList();
var results = await Task.WhenAll(processingTasks);
// This part is thread-safe as it runs after all parallel tasks are complete.
foreach (var (document, billAttachment) in results)
{
_context.Documents.Add(document);
_context.BillAttachments.Add(billAttachment);
}
_logger.LogInfo("{AttachmentCount} attachments processed and staged for saving.", results.Length);
}
/// <summary>
/// A private static helper method to create Document and BillAttachment entities.
/// </summary>
private static (Document document, BillAttachments billAttachment) CreateAttachmentEntities(
Guid batchId, Guid expenseId, Guid uploadedById, Guid tenantId, string s3Key, FileUploadModel attachmentDto)
{
var document = new Document
{
BatchId = batchId,
UploadedById = uploadedById,
FileName = attachmentDto.FileName ?? "",
ContentType = attachmentDto.ContentType ?? "",
S3Key = s3Key,
FileSize = attachmentDto.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
var billAttachment = new BillAttachments { Document = document, ExpensesId = expenseId, TenantId = tenantId };
return (document, billAttachment);
}
[HttpPost("action")]
public async Task<IActionResult> ChangeStatus([FromBody] ExpenseRecordDto model)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var exsitingExpenses = await _context.Expenses
.FirstOrDefaultAsync(e => e.Id == model.ExpenseId && e.TenantId == tenantId);
if (exsitingExpenses == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404));
}
exsitingExpenses.StatusId = model.StatusId;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException dbEx)
{
// --- Step 3: Handle Concurrency Conflicts ---
// This happens if another user modified the project after we fetched it.
_logger.LogError(dbEx, "Error occured while update status of expanse.");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error occured while update status of expanse.", new
{
Message = dbEx.Message,
StackTrace = dbEx.StackTrace,
Source = dbEx.Source,
innerexcption = new
{
Message = dbEx.InnerException?.Message,
StackTrace = dbEx.InnerException?.StackTrace,
Source = dbEx.InnerException?.Source,
}
}, 500));
}
var response = _mapper.Map<ExpenseList>(exsitingExpenses);
return Ok(ApiResponse<object>.SuccessResponse(response));
}
[HttpPut("edit/{id}")]
public async Task<IActionResult> UpdateExpanse(Guid id, [FromBody] UpdateExpensesDto model)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var exsitingExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == model.Id && e.TenantId == tenantId);
if (exsitingExpense == null)
{
return NotFound(ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404));
}
_mapper.Map(model, exsitingExpense);
_context.Entry(exsitingExpense).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
_logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id);
}
catch (DbUpdateConcurrencyException ex)
{
// --- Step 3: Handle Concurrency Conflicts ---
// This happens if another user modified the project after we fetched it.
_logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409));
}
var response = _mapper.Map<ExpenseList>(exsitingExpense);
return Ok(ApiResponse<object>.SuccessResponse(response));
}
[HttpDelete("delete/{id}")]
public void Delete(int id)
{
}
#region =================================================================== Helper Functions ===================================================================
/// <summary>
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
/// </summary>
/// <param name="filter">The JSON filter string from the request.</param>
/// <returns>An <see cref="ExpensesFilter"/> object or null if deserialization fails.</returns>
private ExpensesFilter? TryDeserializeFilter(string? filter)
{
if (string.IsNullOrWhiteSpace(filter))
{
return null;
}
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
ExpensesFilter? expenseFilter = null;
try
{
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(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<ExpensesFilter>(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
}
}