From a04ef7ca9b7a00faa9f806a93fd62d57ca454b50 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 1 Nov 2025 18:00:23 +0530 Subject: [PATCH] Added the edit payment request API --- .../Controllers/ExpenseController.cs | 12 + Marco.Pms.Services/Service/ExpensesService.cs | 232 ++++++++++++++++++ .../ServiceInterfaces/IExpensesService.cs | 1 + 3 files changed, 245 insertions(+) diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index 21d2eeb..7d7d9a1 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -138,6 +138,18 @@ 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 } } diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 1488f56..d4b7baf 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -1226,6 +1226,197 @@ namespace Marco.Pms.Services.Service } } + 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; + }); + + await Task.WhenAll(expenseCategoryTask, 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 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); + } + + // 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; + + // Handle bill attachment updates: add new attachments and delete deactivated ones + if (model.BillAttachments?.Any() == true) + { + 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 = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}"; + response.Currency = currency; + response.ExpenseCategory = _mapper.Map(expenseCategory); + response.Project = _mapper.Map(project); + response.UpdatedBy = _mapper.Map(loggedInEmployee); + + _logger.LogInfo("PaymentRequest updated successfully with UID: {PaymentRequestUID}", response.PaymentRequestUID); + 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 =================================================================== @@ -1660,6 +1851,47 @@ namespace Marco.Pms.Services.Service await Task.WhenAll(attachmentTask, documentsTask); } + private async Task DeletePaymentRequestAttachemnts(List documentIds) + { + var attachmentTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var attachments = await dbContext.PaymentRequestAttachments.AsNoTracking().Where(ba => documentIds.Contains(ba.DocumentId)).ToListAsync(); + + dbContext.PaymentRequestAttachments.RemoveRange(attachments); + await dbContext.SaveChangesAsync(); + }); + var documentsTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var documents = await dbContext.Documents.AsNoTracking().Where(ba => documentIds.Contains(ba.Id)).ToListAsync(); + + if (documents.Any()) + { + dbContext.Documents.RemoveRange(documents); + await dbContext.SaveChangesAsync(); + + List deletionObject = new List(); + foreach (var document in documents) + { + deletionObject.Add(new S3DeletionObject + { + Key = document.S3Key + }); + if (!string.IsNullOrWhiteSpace(document.ThumbS3Key) && document.ThumbS3Key != document.S3Key) + { + deletionObject.Add(new S3DeletionObject + { + Key = document.ThumbS3Key + }); + } + } + await _updateLogHelper.PushToS3DeletionAsync(deletionObject); + } + }); + + await Task.WhenAll(attachmentTask, documentsTask); + } #endregion } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index 67c9048..0f5a349 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -17,5 +17,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId); + Task> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId); } }