diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index 2731bc2..117eba0 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -133,6 +133,14 @@ namespace Marco.Pms.Services.Controllers var response = await _expensesService.GetPaymentRequestListAsync(searchString, filter, isActive, pageSize, pageNumber, loggedInEmployee, tenantId); return StatusCode(response.StatusCode, response); } + + [HttpGet("get/payment-request/details/{id?}")] + public async Task GetPaymentRequestDetails(Guid? id, [FromQuery] string? paymentRequestUId) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _expensesService.GetPaymentRequestDetailsAsync(id, paymentRequestUId, loggedInEmployee, tenantId); + 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 aa74004..90c1c60 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -1364,7 +1364,251 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("End GetPaymentRequestListAsync for TenantId={TenantId}, EmployeeId={EmployeeId}", tenantId, loggedInEmployee.Id); } } + public async Task> GetPaymentRequestDetailsAsync(Guid? id, string? paymentRequestUId, Employee loggedInEmployee, Guid tenantId) + { + _logger.LogInfo("Start GetPaymentRequestDetailsAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId} with Id: {Id}, UID: {UID}", + loggedInEmployee.Id, tenantId, id ?? Guid.Empty, paymentRequestUId ?? "PY/1125/00000"); + try + { + // Validate input: at least one identifier must be provided + if (!id.HasValue && string.IsNullOrWhiteSpace(paymentRequestUId)) + { + _logger.LogWarning("Invalid parameters: Both Id and PaymentRequestUID are null or empty."); + return ApiResponse.ErrorResponse("At least one parameter (Id or PaymentRequestUID) must be provided.", "Invalid argument.", 400); + } + + // Check user permissions concurrently + var hasViewSelfPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id); + }); + + var hasViewAllPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id); + }); + + var hasReviewPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ExpenseReview, loggedInEmployee.Id); + }); + + var hasApprovePermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id); + }); + + var hasProcessPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id); + }); + + 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(hasViewSelfPermissionTask, hasViewAllPermissionTask, hasReviewPermissionTask, hasApprovePermissionTask, hasProcessPermissionTask, hasManagePermissionTask); + + bool hasViewSelfPermission = hasViewSelfPermissionTask.Result; + bool hasViewAllPermission = hasViewAllPermissionTask.Result; + bool hasReviewPermission = hasReviewPermissionTask.Result; + bool hasApprovePermission = hasApprovePermissionTask.Result; + bool hasProcessPermission = hasProcessPermissionTask.Result; + bool hasManagePermission = hasProcessPermissionTask.Result; + + // Deny access if user has no relevant permissions + if (!hasViewSelfPermission && !hasViewAllPermission && !hasReviewPermission && !hasApprovePermission && !hasProcessPermission) + { + _logger.LogWarning("Access DENIED: Employee {EmployeeId} has no permission to view payment requests.", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view any payment request.", 200); + } + + // Query payment request with all necessary navigation properties and validation constraints + var paymentRequest = await _context.PaymentRequests + .Include(pr => pr.Currency) + .Include(pr => pr.Project) + .Include(pr => pr.RecurringPayment) + .Include(pr => pr.ExpenseCategory) + .Include(pr => pr.ExpenseStatus) + .Include(pr => pr.CreatedBy).ThenInclude(e => e!.JobRole) + .Include(pr => pr.UpdatedBy).ThenInclude(e => e!.JobRole) + .Where(pr => + (pr.Id == id || (pr.UIDPrefix + "/" + pr.UIDPostfix.ToString().PadLeft(5, '0')) == paymentRequestUId) && + pr.TenantId == tenantId && + pr.Currency != null && + pr.ExpenseCategory != null && + pr.ExpenseStatus != null && + pr.CreatedBy != null && + pr.CreatedBy.JobRole != null) + .FirstOrDefaultAsync(); + + if (paymentRequest == null) + { + _logger.LogWarning("Payment Request not found: Id={Id}, UID={UID}, TenantId={TenantId}", id ?? Guid.Empty, paymentRequestUId ?? "PY/1125/00000", tenantId); + return ApiResponse.ErrorResponse("Payment Request not found.", "Payment Request not found.", 404); + } + + // Check if employee has only "view self" permission but the payment request is created by another employee => deny + bool selfCheck = hasViewSelfPermission && !hasViewAllPermission && !hasReviewPermission && !hasApprovePermission && !hasProcessPermission + && paymentRequest.CreatedById != loggedInEmployee.Id; + + if (selfCheck) + { + _logger.LogWarning("Access DENIED: Employee {EmployeeId} lacks permission to view PaymentRequest {PaymentRequestId} created by another employee.", + loggedInEmployee.Id, paymentRequest.Id); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view this payment request.", 200); + } + + // Concurrently fetch next possible statuses and related permissions + var nextStatusTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + + var nextStatuses = await context.ExpensesStatusMapping + .Include(esm => esm.NextStatus) + .Where(esm => esm.StatusId == paymentRequest.ExpenseStatusId && esm.NextStatus != null) + .Select(esm => esm.NextStatus!) + .ToListAsync(); + + var nextStatusIds = nextStatuses.Select(ns => ns.Id).ToList(); + var permissionMappings = await context.StatusPermissionMapping.Where(spm => nextStatusIds.Contains(spm.StatusId)).ToListAsync(); + + var results = new List(); + + foreach (var status in nextStatuses) + { + var permissionIds = permissionMappings.Where(spm => spm.StatusId == status.Id).Select(spm => spm.PermissionId).ToList(); + bool hasPermission = await permissionService.HasPermissionAny(permissionIds, loggedInEmployee.Id); + + // Special case: allow review status if creator is the logged-in user + bool hasStatusPermission = Review == status.Id && loggedInEmployee.Id == paymentRequest.CreatedById; + + if (!hasPermission && !hasStatusPermission) + { + continue; + } + + var mappedStatus = _mapper.Map(status); + mappedStatus.PermissionIds = permissionIds; + results.Add(mappedStatus); + } + + int index = results.FindIndex(ns => ns.DisplayName == "Reject"); + if (index > -1) + { + var item = results[index]; + results.RemoveAt(index); + results.Insert(0, item); + } + + return results; + }); + + // Concurrently fetch attachments with pre-signed URLs + var documentTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + var documents = await context.PaymentRequestAttachments + .Include(pra => pra.Document) + .Where(pra => pra.PaymentRequestId == paymentRequest.Id && pra.Document != null) + .Select(pra => pra.Document!) + .ToListAsync(); + + return documents.Select(d => + { + var attachmentVM = _mapper.Map(d); + attachmentVM.Url = _s3Service.GeneratePreSignedUrl(d.S3Key); + return attachmentVM; + }).ToList(); + }); + + var updateLogsTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.StatusUpdateLogs + .Include(sul => sul.UpdatedBy) + .Where(sul => sul.EntityId == paymentRequest.Id && sul.TenantId == tenantId) + .OrderByDescending(sul => sul.UpdatedAt) + .ToListAsync(); + }); + + await Task.WhenAll(nextStatusTask, documentTask, updateLogsTask); + + var nextStatuses = nextStatusTask.Result; + var attachmentVMs = documentTask.Result; + var updateLogs = updateLogsTask.Result; + + var statusIds = updateLogs.Select(sul => sul.StatusId).ToList(); + statusIds.AddRange(updateLogs.Select(sul => sul.NextStatusId).ToList()); + + statusIds = statusIds.Distinct().ToList(); + + var status = await _context.ExpensesStatusMaster.Where(es => statusIds.Contains(es.Id)).ToListAsync(); + + // Map main response model and populate additional fields + var response = _mapper.Map(paymentRequest); + response.PaymentRequestUID = $"{paymentRequest.UIDPrefix}/{paymentRequest.UIDPostfix:D5}"; + //if (paymentRequest.RecurringPayment != null) + // response.RecurringPaymentUID = $"{paymentRequest.RecurringPayment.UIDPrefix}/{paymentRequest.RecurringPayment.UIDPostfix:D5}"; + response.Attachments = attachmentVMs; + + // Assign nextStatuses only if: + // 1. The payment request was rejected by approver/reviewer AND the current user is the creator, OR + // 2. The payment request is in any other status (not rejected) + var isRejected = paymentRequest.ExpenseStatusId == RejectedByApprover + || paymentRequest.ExpenseStatusId == RejectedByReviewer; + + if ((!isRejected) || (isRejected && (loggedInEmployee.Id == paymentRequest.CreatedById || hasManagePermission))) + { + response.NextStatus = nextStatuses; + } + + response.UpdateLogs = updateLogs.Select(ul => + { + var statusVm = status.FirstOrDefault(es => es.Id == ul.StatusId); + var nextStatusVm = status.FirstOrDefault(es => es.Id == ul.NextStatusId); + + return new PaymentRequestUpdateLog + { + Id = ul.Id, + Comment = ul.Comment, + Status = _mapper.Map(statusVm), + NextStatus = _mapper.Map(nextStatusVm), + UpdatedAt = ul.UpdatedAt, + UpdatedBy = _mapper.Map(ul.UpdatedBy) + }; + }).ToList(); + + _logger.LogInfo("Payment request details fetched successfully for PaymentRequestId: {PaymentRequestId}, EmployeeId: {EmployeeId}", paymentRequest.Id, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(response, "Payment request fetched successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in GetPaymentRequestDetailsAsync for TenantId={TenantId}, EmployeeId={EmployeeId}: {Message}", tenantId, loggedInEmployee.Id, ex.Message); + return ApiResponse.ErrorResponse("An error occurred while fetching the payment request details.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End GetPaymentRequestDetailsAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.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 4f2d033..e51fe4e 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -19,6 +19,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces #region =================================================================== Payment Request Functions =================================================================== Task> GetPaymentRequestListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId); + Task> GetPaymentRequestDetailsAsync(Guid? id, string? paymentRequestUId, Employee loggedInEmployee, Guid tenantId); #endregion #region =================================================================== Payment Request Functions ===================================================================