From 6f2903c0c7068bfc7104c7f39cb6bb13498cbe67 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 7 Nov 2025 17:42:04 +0530 Subject: [PATCH] Added the API to update payment request --- .../Controllers/ExpenseController.cs | 13 ++ Marco.Pms.Services/Service/ExpensesService.cs | 212 ++++++++++++++++++ .../ServiceInterfaces/IExpensesService.cs | 1 + 3 files changed, 226 insertions(+) diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index af2e9eb..27a626f 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -197,6 +197,19 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpPut("payment-request/edit/{id}")] + public async Task EditPaymentRequest(Guid id, [FromBody] PaymentRequestDto model) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _expensesService.EditPaymentRequestAsync(id, model, loggedInEmployee, tenantId); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Payment_Request", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } + #endregion #region =================================================================== Payment Request Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 43f8aa6..24adb7c 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -2191,6 +2191,218 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("End ChangeToExpanseFromPaymentRequestAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); } } + public async Task> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId) + { + _logger.LogInfo("Start EditPaymentRequestAsync for PaymentRequestId: {PaymentRequestId}, EmployeeId: {EmployeeId}", id, loggedInEmployee.Id); + + if (model.Id == null || id != model.Id) + { + _logger.LogWarning("Mismatch between URL id and payload id: {Id} vs {ModelId}", id, model?.Id ?? Guid.Empty); + return ApiResponse.ErrorResponse("Invalid argument: ID mismatch.", "Invalid argument: ID mismatch.", 400); + } + + try + { + // Concurrently fetch related entities to validate input references + 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 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; + }); + var hasManagePermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id); + }); + + await Task.WhenAll(expenseCategoryTask, currencyTask, projectTask, hasManagePermissionTask); + + 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 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; // Project can be null (optional) + + // Retrieve the existing payment request with relevant navigation properties for validation and mapping + var paymentRequest = await _context.PaymentRequests + .Include(pr => pr.ExpenseCategory) + .Include(pr => pr.Project) + .Include(pr => pr.ExpenseStatus) + .Include(pr => pr.RecurringPayment) + .Include(pr => pr.Currency) + .Include(pr => pr.CreatedBy).ThenInclude(e => e!.JobRole) + .Include(pr => pr.UpdatedBy).ThenInclude(e => e!.JobRole) + .FirstOrDefaultAsync(pr => pr.Id == id); + + if (paymentRequest == null) + { + _logger.LogWarning("Payment Request not found with Id: {PaymentRequestId}", id); + return ApiResponse.ErrorResponse("Payment Request not found.", "Payment Request not found.", 404); + } + var hasManagePermission = hasManagePermissionTask.Result; + if (!hasManagePermission && paymentRequest.CreatedById != loggedInEmployee.Id) + { + _logger.LogWarning("Access DENIED: Employee {EmployeeId} has no permission to edit payment requests.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "You do not have permission to edit any payment request.", 409); + } + // Check if status prevents editing (only allow edit if status Draft, RejectedByReviewer, or RejectedByApprover) + bool statusCheck = paymentRequest.ExpenseStatusId != Draft && + paymentRequest.ExpenseStatusId != RejectedByReviewer && + paymentRequest.ExpenseStatusId != RejectedByApprover; + + bool isVariableRecurring = paymentRequest.RecurringPayment?.IsVariable ?? false; + + // Capture existing state for auditing + var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentRequest); + + // Only map updates if allowed by status + if (!statusCheck && paymentRequest.CreatedById == loggedInEmployee.Id) + { + _mapper.Map(model, paymentRequest); + paymentRequest.UpdatedAt = DateTime.UtcNow; + paymentRequest.UpdatedById = loggedInEmployee.Id; + } + + if (isVariableRecurring) + { + paymentRequest.Amount = model.Amount; + } + + paymentRequest.IsAdvancePayment = model.IsAdvancePayment; + + var paymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}"; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("PaymentRequest updated successfully with UID: {PaymentRequestUID}", paymentRequestUID); + } + catch (DbUpdateException dbEx) + { + _logger.LogError(dbEx, "Database Exception during Payment Request update"); + return ApiResponse.ErrorResponse("Database exception during payment request updation", ExceptionMapper(dbEx), 500); + } + + // Handle bill attachment updates: add new attachments and delete deactivated ones + if (model.BillAttachments?.Any() == true && !statusCheck) + { + var newBillAttachments = model.BillAttachments.Where(ba => ba.DocumentId == null && ba.IsActive).ToList(); + if (newBillAttachments.Any()) + { + _logger.LogInfo("Processing {AttachmentCount} new attachments for PaymentRequest Id: {PaymentRequestId}", newBillAttachments.Count, paymentRequest.Id); + + // Pre-validate base64 data before upload + foreach (var attachment in newBillAttachments) + { + if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data)) + { + _logger.LogWarning("Invalid or missing Base64 data for attachment: {FileName}", attachment.FileName ?? "N/A"); + throw new ArgumentException($"Invalid or missing Base64 data for attachment: {attachment.FileName ?? "N/A"}"); + } + } + + var batchId = Guid.NewGuid(); + + var processingTasks = newBillAttachments.Select(attachment => + ProcessSinglePaymentRequestAttachmentAsync(attachment, paymentRequest, loggedInEmployee.Id, tenantId, batchId)).ToList(); + + var results = await Task.WhenAll(processingTasks); + + foreach (var (document, paymentRequestAttachment) in results) + { + _context.Documents.Add(document); + _context.PaymentRequestAttachments.Add(paymentRequestAttachment); + } + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Added {Count} new attachments for PaymentRequest {PaymentRequestId} by Employee {EmployeeId}", newBillAttachments.Count, paymentRequest.Id, loggedInEmployee.Id); + } + catch (DbUpdateException dbEx) + { + _logger.LogError(dbEx, "Database Exception while adding attachments during PaymentRequest update"); + return ApiResponse.ErrorResponse("Database exception during attachment addition.", ExceptionMapper(dbEx), 500); + } + } + + var deleteBillAttachments = model.BillAttachments.Where(ba => ba.DocumentId != null && !ba.IsActive).ToList(); + if (deleteBillAttachments.Any()) + { + var documentIds = deleteBillAttachments.Select(d => d.DocumentId!.Value).ToList(); + + try + { + await DeletePaymentRequestAttachemnts(documentIds); + _logger.LogInfo("Deleted {Count} attachments for PaymentRequest {PaymentRequestId} by Employee {EmployeeId}", deleteBillAttachments.Count, paymentRequest.Id, loggedInEmployee.Id); + } + catch (DbUpdateException dbEx) + { + _logger.LogError(dbEx, "Database Exception while deleting attachments during PaymentRequest update"); + return ApiResponse.ErrorResponse("Database exception during attachment deletion.", ExceptionMapper(dbEx), 500); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected exception while deleting attachments during PaymentRequest update"); + return ApiResponse.ErrorResponse("Error occurred while deleting attachments.", ExceptionMapper(ex), 500); + } + } + } + + // Log the update audit trail + await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject + { + EntityId = paymentRequest.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = existingEntityBson, + UpdatedAt = DateTime.UtcNow + }, "PaymentRequestModificationLog"); + + // Prepare response view model with updated details + var response = _mapper.Map(paymentRequest); + response.PaymentRequestUID = paymentRequestUID; + response.Currency = currency; + response.ExpenseCategory = _mapper.Map(expenseCategory); + response.Project = _mapper.Map(project); + + return ApiResponse.SuccessResponse(response, "Payment Request updated successfully.", 200); + } + catch (ArgumentException ex) + { + _logger.LogError(ex, "Argument error in EditPaymentRequestAsync: {Message}", ex.Message); + return ApiResponse.ErrorResponse(ex.Message, "Invalid data.", 400); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error in EditPaymentRequestAsync: {Message}", ex.Message); + return ApiResponse.ErrorResponse("An error occurred while updating the payment request.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End EditPaymentRequestAsync for PaymentRequestId: {PaymentRequestId}", id); + } + } #endregion #region =================================================================== Payment Request Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index 7ed0b28..33a0a21 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -25,6 +25,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId); Task> ChangePaymentRequestStatusAsync(PaymentRequestRecordDto model, Employee loggedInEmployee, Guid tenantId); Task> ChangeToExpanseFromPaymentRequestAsync(ExpenseConversionDto model, Employee loggedInEmployee, Guid tenantId); + Task> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId); #endregion #region =================================================================== Payment Request Functions ===================================================================