diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 9411443..9ea150f 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -250,6 +250,8 @@ namespace Marco.Pms.Services.Service public async Task> CreateExpenseAsync(CreateExpensesDto dto, Employee loggedInEmployee, Guid tenantId) { _logger.LogDebug("Starting 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 { @@ -299,7 +301,14 @@ namespace Marco.Pms.Services.Service .Include(s => s.Status) .Include(s => s.NextStatus) .AsNoTracking() - .Where(es => es.StatusId == Draft && es.Status != null).ToListAsync(); + .Where(es => es.StatusId == Draft && es.Status != null) + .GroupBy(s => s.StatusId) + .Select(g => new + { + Status = g.Select(s => s.Status).FirstOrDefault(), + NextStatus = g.Select(s => s.NextStatus).ToList() + }) + .FirstOrDefaultAsync(); }); @@ -327,10 +336,11 @@ namespace Marco.Pms.Services.Service if (paidBy == null) validationErrors.Add("Paid by employee not found"); if (expenseType == null) validationErrors.Add("Expense Type not found."); if (paymentMode == null) validationErrors.Add("Payment Mode not found."); - if (!statusMapping.Any()) validationErrors.Add("Default status 'Draft' not found."); + if (statusMapping == null) validationErrors.Add("Default status 'Draft' not found."); if (validationErrors.Any()) { + await transaction.RollbackAsync(); var errorMessage = string.Join(" ", validationErrors); _logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage); return ApiResponse.ErrorResponse("Invalid input data.", errorMessage, 400); @@ -355,13 +365,14 @@ namespace Marco.Pms.Services.Service // 5. Database Commit await _context.SaveChangesAsync(); - var status = statusMapping.Select(sm => sm.Status).FirstOrDefault(); - var nextStatus = statusMapping.Select(sm => sm.NextStatus).ToList(); + // 6. Transaction Commit + await transaction.CommitAsync(); + var response = _mapper.Map(expense); response.PaidBy = _mapper.Map(paidBy); response.Project = _mapper.Map(project); - response.Status = _mapper.Map(status); - response.NextStatus = _mapper.Map>(nextStatus); + response.Status = _mapper.Map(statusMapping!.Status); + response.NextStatus = _mapper.Map>(statusMapping.NextStatus); response.PaymentMode = _mapper.Map(paymentMode); response.ExpensesType = _mapper.Map(expenseType); @@ -370,6 +381,7 @@ namespace Marco.Pms.Services.Service } catch (ArgumentException ex) // Catches bad Base64 from attachment pre-validation { + await transaction.RollbackAsync(); _logger.LogError(ex, "Invalid argument during expense creation for project {ProjectId}.", dto.ProjectId); return ApiResponse.ErrorResponse("Invalid Request Data.", new { @@ -386,6 +398,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) // General-purpose catch for unexpected errors (e.g., S3 or DB connection failure) { + await transaction.RollbackAsync(); _logger.LogError(ex, "An unhandled exception occurred while creating an expense for project {ProjectId}.", dto.ProjectId); return ApiResponse.ErrorResponse("An internal server error occurred.", new {