diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 8b2a80c..7cebd65 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -891,11 +891,18 @@ namespace Marco.Pms.Services.Service public async Task> UpdateExpanseAsync(Guid id, UpdateExpensesDto model, Employee loggedInEmployee, Guid tenantId) { + // Validate if the employee Id from the URL path matches the Id in the request body (model) if (id != model.Id) { - _logger.LogWarning("Id provided by path parameter and Id from body not matches for employee {EmployeeId}", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Invalid Parameters", "Invalid Parameters", 400); + // Log a warning with details for traceability when Ids do not match + _logger.LogWarning("Mismatch detected: Path parameter Id ({PathId}) does not match body Id ({BodyId}) for employee {EmployeeId}", + id, model.Id, loggedInEmployee.Id); + + // Return standardized error response with HTTP 400 Bad Request status and clear message + return ApiResponse.ErrorResponse("The employee Id in the path does not match the Id in the request body.", + "The employee Id in the path does not match the Id in the request body.", 400); } + var existingExpense = await _context.Expenses .Include(e => e.ExpenseCategory) .Include(e => e.Project) @@ -2337,7 +2344,6 @@ namespace Marco.Pms.Services.Service }; return ApiResponse.SuccessResponse(response, $"{results.Count} Recurring payments fetched successfully", 200); } - public async Task> CreateRecurringPaymentAsync(RecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("Start CreateRecurringPaymentAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", loggedInEmployee.Id, tenantId); @@ -2450,89 +2456,128 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("End CreateRecurringPaymentAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); } } - - public async Task> EditRecurringPaymentAsync(Guid id, RecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId) { - using var scope = _serviceScopeFactory.CreateScope(); - var permissionService = scope.ServiceProvider.GetRequiredService(); - var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageRecurring, loggedInEmployee.Id); + _logger.LogInfo("Start EditRecurringPaymentAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}, RecurringPaymentId: {RecurringPaymentId}", + loggedInEmployee.Id, tenantId, id); - if (!hasPermission) + try { - _logger.LogWarning("Employee {EmployeeId} attempted to update recurring template", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("You do not have access to update recurring template", "You do not have access to update recurring template", 400); + // Validate if the employee Id from the URL path matches the Id in the request body (model) + if (id != model.Id || model.Id.HasValue) + { + // Log a warning with details for traceability when Ids do not match + _logger.LogWarning("Mismatch detected: Path parameter Id ({PathId}) does not match body Id ({BodyId}) for employee {EmployeeId}", + id, model.Id ?? Guid.Empty, loggedInEmployee.Id); + + // Return standardized error response with HTTP 400 Bad Request status and clear message + return ApiResponse.ErrorResponse("The employee Id in the path does not match the Id in the request body.", + "The employee Id in the path does not match the Id in the request body.", 400); + } + + // Permission check for managing recurring payments + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageRecurring, loggedInEmployee.Id); + + if (!hasPermission) + { + _logger.LogWarning("Access denied: Employee {EmployeeId} attempted to update recurring template without permission.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("You do not have access to update recurring template.", "Permission denied.", 403); + } + + // Concurrently fetch required related entities for validation + var expenseCategoryTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.ExpenseCategoryMasters.FirstOrDefaultAsync(et => et.Id == model.ExpenseCategoryId && et.IsActive); + }); + + var recurringStatusTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.RecurringPaymentStatus.FirstOrDefaultAsync(rs => rs.Id == model.StatusId); + }); + + var currencyTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.CurrencyMaster.FirstOrDefaultAsync(c => c.Id == model.CurrencyId); + }); + + var projectTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return model.ProjectId.HasValue ? await context.Projects.FirstOrDefaultAsync(p => p.Id == model.ProjectId.Value) : null; + }); + + await Task.WhenAll(expenseCategoryTask, recurringStatusTask, currencyTask, projectTask); + + var expenseCategory = await expenseCategoryTask; + if (expenseCategory == null) + { + _logger.LogWarning("Expense Category not found with Id: {ExpenseCategoryId}", model.ExpenseCategoryId); + return ApiResponse.ErrorResponse("Expense Category not found.", "Expense Category not found.", 404); + } + + var recurringStatus = await recurringStatusTask; + if (recurringStatus == null) + { + _logger.LogWarning("Recurring Payment Status not found with Id: {StatusId}", model.StatusId); + return ApiResponse.ErrorResponse("Recurring Payment Status not found.", "Recurring Payment Status not found.", 404); + } + + var currency = await currencyTask; + if (currency == null) + { + _logger.LogWarning("Currency not found with Id: {CurrencyId}", model.CurrencyId); + return ApiResponse.ErrorResponse("Currency not found.", "Currency not found.", 404); + } + + var project = await projectTask; // Optional + + // Retrieve the existing recurring payment record for update + var recurringPayment = await _context.RecurringPayments + .FirstOrDefaultAsync(rp => rp.Id == id && rp.IsActive && rp.TenantId == tenantId); + + if (recurringPayment == null) + { + _logger.LogWarning("Recurring Payment Template not found with Id: {RecurringPaymentId}", id); + return ApiResponse.ErrorResponse("Recurring Payment Template not found.", "Recurring Payment Template not found.", 404); + } + + // Map updated values from DTO to entity + _mapper.Map(model, recurringPayment); + recurringPayment.UpdatedAt = DateTime.UtcNow; + recurringPayment.UpdatedById = loggedInEmployee.Id; + + // Save changes to database + await _context.SaveChangesAsync(); + + // Map entity to view model for response + var response = _mapper.Map(recurringPayment); + response.RecurringPaymentUId = $"{recurringPayment.UIDPrefix}/{recurringPayment.UIDPostfix:D5}"; + response.CreatedBy = _mapper.Map(loggedInEmployee); + response.ExpenseCategory = _mapper.Map(expenseCategory); + response.Status = recurringStatus; + response.Currency = currency; + response.Project = _mapper.Map(project); + + _logger.LogInfo("Recurring Payment Template updated successfully with UID: {RecurringPaymentUId} by EmployeeId: {EmployeeId}", response.RecurringPaymentUId, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(response, "Recurring Payment Template updated successfully.", 200); } - - var expenseCategoryTask = Task.Run(async () => + catch (Exception ex) { - await using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.ExpenseCategoryMasters.FirstOrDefaultAsync(et => et.Id == model.ExpenseCategoryId && et.IsActive); - }); - var recurringStatusTask = Task.Run(async () => - { - await using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.RecurringPaymentStatus.FirstOrDefaultAsync(et => et.Id == model.StatusId); - }); - var currencyTask = Task.Run(async () => - { - await using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.CurrencyMaster.FirstOrDefaultAsync(c => c.Id == model.CurrencyId); - }); - var projectTask = Task.Run(async () => - { - await using var context = await _dbContextFactory.CreateDbContextAsync(); - return await context.Projects.FirstOrDefaultAsync(P => model.ProjectId.HasValue && P.Id == model.ProjectId.Value); - }); - - await Task.WhenAll(expenseCategoryTask, currencyTask, projectTask, recurringStatusTask); - - var expenseCategory = await expenseCategoryTask; - if (expenseCategory == null) - { - _logger.LogWarning("Expense Category not found with Id: {ExpenseCategoryId}", model.ExpenseCategoryId); - return ApiResponse.ErrorResponse("Expense Category not found.", "Expense Category not found.", 404); + _logger.LogError(ex, "Error in EditRecurringPaymentAsync called by EmployeeId: {EmployeeId}: {Message}", loggedInEmployee.Id, ex.Message); + return ApiResponse.ErrorResponse("An error occurred while updating the recurring payment template.", ex.Message, 500); } - - var recurringStatus = await recurringStatusTask; - if (expenseCategory == null) + finally { - _logger.LogWarning("Recurring Payment Status not found with Id: {StatusId}", model.StatusId); - return ApiResponse.ErrorResponse("Recurring Payment Status not found.", "Recurring Payment Status not found.", 404); + _logger.LogInfo("End EditRecurringPaymentAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); } - - var currency = await currencyTask; - if (currency == null) - { - _logger.LogWarning("Currency not found with Id: {CurrencyId}", model.CurrencyId); - return ApiResponse.ErrorResponse("Currency not found.", "Currency not found.", 404); - } - - var project = await projectTask; - - var recurringPayment = await _context.RecurringPayments.FirstOrDefaultAsync(rp => rp.Id == id && rp.IsActive && rp.TenantId == tenantId); - - if (recurringPayment == null) - { - return ApiResponse.ErrorResponse("Recurring Payment Template not found", "Recurring Payment Template not found", 404); - } - - _mapper.Map(model, recurringPayment); - recurringPayment.UpdatedAt = DateTime.UtcNow; - recurringPayment.UpdatedById = loggedInEmployee.Id; - - await _context.SaveChangesAsync(); - - var response = _mapper.Map(recurringPayment); - response.RecurringPaymentUId = $"{recurringPayment.UIDPrefix}/{recurringPayment.UIDPostfix:D5}"; - response.CreatedBy = _mapper.Map(loggedInEmployee); - response.ExpenseCategory = _mapper.Map(expenseCategory); - response.Status = recurringStatus; - response.Currency = currency; - response.Project = _mapper.Map(project); - - return ApiResponse.SuccessResponse(response, "Recurring Payment Template updated successfully", 200); } + #endregion #region =================================================================== Helper Functions ===================================================================