Added the edit payment request API

This commit is contained in:
ashutosh.nehete 2025-11-01 18:00:23 +05:30
parent b6295ebef4
commit a04ef7ca9b
3 changed files with 245 additions and 0 deletions

View File

@ -138,6 +138,18 @@ namespace Marco.Pms.Services.Controllers
}
return StatusCode(response.StatusCode, response);
}
[HttpPut("payment-request/edit/{id}")]
public async Task<IActionResult> 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
}
}

View File

@ -1226,6 +1226,197 @@ namespace Marco.Pms.Services.Service
}
}
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<object>.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<object>.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<object>.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<object>.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<PaymentRequestVM>(paymentRequest);
response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}";
response.Currency = currency;
response.ExpenseCategory = _mapper.Map<ExpensesTypeMasterVM>(expenseCategory);
response.Project = _mapper.Map<BasicProjectVM>(project);
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
_logger.LogInfo("PaymentRequest updated successfully with UID: {PaymentRequestUID}", response.PaymentRequestUID);
return ApiResponse<object>.SuccessResponse(response, "Payment Request updated successfully.", 200);
}
catch (ArgumentException ex)
{
_logger.LogError(ex, "Argument error in EditPaymentRequestAsync: {Message}", ex.Message);
return ApiResponse<object>.ErrorResponse(ex.Message, "Invalid data.", 400);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in EditPaymentRequestAsync: {Message}", ex.Message);
return ApiResponse<object>.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<Guid> 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<S3DeletionObject> deletionObject = new List<S3DeletionObject>();
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
}

View File

@ -17,5 +17,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
}
}