diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index 651f9df..878e1b9 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -234,6 +234,14 @@ namespace Marco.Pms.Services.Controllers var response = await _expensesService.GetRecurringPaymentListAsync(searchString, filter, isActive, pageSize, pageNumber, loggedInEmployee, tenantId); return StatusCode(response.StatusCode, response); } + + [HttpGet("get/recurring-payment/details/{id?}")] + public async Task GetRecurringPaymentDetailsAsync(Guid? id, [FromQuery] string? recurringPaymentUId) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _expensesService.GetRecurringPaymentDetailsAsync(id, recurringPaymentUId, 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 19d614a..1f25bb6 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -2655,6 +2655,136 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("End GetRecurringPaymentListAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); } } + public async Task> GetRecurringPaymentDetailsAsync(Guid? id, string? recurringPaymentUId, Employee loggedInEmployee, Guid tenantId) + { + _logger.LogInfo("Start GetRecurringPaymentDetailsAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId} with Id: {Id}, UID: {UID}", + loggedInEmployee.Id, tenantId, id ?? Guid.Empty, recurringPaymentUId ?? ""); + + try + { + // Validate input: require at least one identifier + if (!id.HasValue && string.IsNullOrWhiteSpace(recurringPaymentUId)) + { + _logger.LogWarning("Invalid parameters: Both Id and RecurringPaymentUID are null or empty."); + return ApiResponse.ErrorResponse("At least one parameter (Id or RecurringPaymentUID) must be provided.", "Invalid argument.", 400); + } + + // Concurrent permission checks for view-self, view-all, and manage recurring payments + var hasViewSelfPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ViewSelfRecurring, loggedInEmployee.Id); + }); + + var hasViewAllPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ViewAllRecurring, loggedInEmployee.Id); + }); + + var hasManagePermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ManageRecurring, loggedInEmployee.Id); + }); + + await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask, hasManagePermissionTask); + + bool hasViewSelfPermission = hasViewSelfPermissionTask.Result; + bool hasViewAllPermission = hasViewAllPermissionTask.Result; + bool hasManagePermission = hasManagePermissionTask.Result; + + // Deny access if user lacks all relevant permissions + if (!hasViewSelfPermission && !hasViewAllPermission && !hasManagePermission) + { + _logger.LogWarning("Access DENIED: Employee {EmployeeId} has no permission to view recurring payments.", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view any recurring payment.", 200); + } + + // Query recurring payment by Id or UID with navigation and tenant checks + var recurringPayment = await _context.RecurringPayments + .Include(rp => rp.Currency) + .Include(rp => rp.Project) + .Include(rp => rp.ExpenseCategory) + .Include(rp => rp.Status) + .Include(rp => rp.CreatedBy).ThenInclude(e => e!.JobRole) + .Include(rp => rp.UpdatedBy).ThenInclude(e => e!.JobRole) + .Where(rp => + (rp.Id == id || (rp.UIDPrefix + "/" + rp.UIDPostfix.ToString().PadLeft(5, '0')) == recurringPaymentUId) && + rp.TenantId == tenantId && + rp.Currency != null && + rp.ExpenseCategory != null && + rp.Status != null && + rp.CreatedBy != null && + rp.CreatedBy.JobRole != null) + .FirstOrDefaultAsync(); + + if (recurringPayment == null) + { + _logger.LogWarning("Recurring Payment not found: Id={Id}, UID={UID}, TenantId={TenantId}", id ?? Guid.Empty, recurringPaymentUId ?? "N/A", tenantId); + return ApiResponse.ErrorResponse("Recurring Payment not found.", "Recurring payment not found.", 404); + } + + // If user has only view-self permission and the recurring payment belongs to another employee, deny access + bool selfCheck = hasViewSelfPermission && !hasViewAllPermission && !hasManagePermission && + recurringPayment.CreatedById != loggedInEmployee.Id; + + if (selfCheck) + { + _logger.LogWarning("Access DENIED: Employee {EmployeeId} lacks permission to view RecurringPayment {RecurringPaymentId} created by another employee.", + loggedInEmployee.Id, recurringPayment.Id); + return ApiResponse.SuccessResponse(new { }, "You do not have permission to view this recurring payment.", 200); + } + + // Concurrently fetch employees notified on this recurring payment and relevant active payment requests + var employeeTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + var emails = recurringPayment.NotifyTo.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + return await context.Employees + .Include(e => e.JobRole) + .Where(e => emails.Contains(e.Email) && e.TenantId == tenantId && e.IsActive) + .ToListAsync(); + }); + + var paymentRequestTask = Task.Run(async () => + { + await using var context = await _dbContextFactory.CreateDbContextAsync(); + return await context.PaymentRequests + .Where(pr => pr.RecurringPaymentId == recurringPayment.Id && pr.TenantId == tenantId && pr.IsActive) + .Select(pr => _mapper.Map(pr)) + .ToListAsync(); + }); + + await Task.WhenAll(employeeTask, paymentRequestTask); + + var employees = employeeTask.Result; + var paymentRequests = paymentRequestTask.Result; + + // Map main response DTO and enrich with notification employees and payment requests + var response = _mapper.Map(recurringPayment); + response.NotifyTo = _mapper.Map>(employees); + response.PaymentRequests = paymentRequests; + + _logger.LogInfo("Recurring payment details fetched successfully for RecurringPaymentId: {RecurringPaymentId} by EmployeeId: {EmployeeId}", + recurringPayment.Id, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(response, "Recurring payment details fetched successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in GetRecurringPaymentDetailsAsync for TenantId={TenantId}, EmployeeId={EmployeeId}: {Message}", tenantId, loggedInEmployee.Id, ex.Message); + return ApiResponse.ErrorResponse("An error occurred while fetching the recurring payment details.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End GetRecurringPaymentDetailsAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); + } + } #endregion diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index 67265b3..03352b0 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -31,6 +31,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces #region =================================================================== Recurring Payment Functions =================================================================== Task> GetRecurringPaymentListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId); + Task> GetRecurringPaymentDetailsAsync(Guid? id, string? recurringPaymentUId, Employee loggedInEmployee, Guid tenantId); #endregion