Added the API to update payment request

This commit is contained in:
ashutosh.nehete 2025-11-07 17:42:04 +05:30
parent 73b85bee84
commit 6f2903c0c7
3 changed files with 226 additions and 0 deletions

View File

@ -197,6 +197,19 @@ 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
#region =================================================================== Payment Request Functions ===================================================================

View File

@ -2191,6 +2191,218 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("End ChangeToExpanseFromPaymentRequestAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id);
}
}
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;
});
var hasManagePermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
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<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);
}
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<object>.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<object>.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<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 = paymentRequestUID;
response.Currency = currency;
response.ExpenseCategory = _mapper.Map<ExpenseCategoryMasterVM>(expenseCategory);
response.Project = _mapper.Map<BasicProjectVM>(project);
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 ===================================================================

View File

@ -25,6 +25,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> ChangePaymentRequestStatusAsync(PaymentRequestRecordDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> ChangeToExpanseFromPaymentRequestAsync(ExpenseConversionDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Payment Request Functions ===================================================================