From 411e63db1becb0d4d149f33d4118ef3f1154c706 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 4 Nov 2025 18:45:57 +0530 Subject: [PATCH] Added the get list recurring payment template API --- .../Filters/RecurringPaymentFilter.cs | 14 ++ .../Controllers/ExpenseController.cs | 4 +- Marco.Pms.Services/Service/ExpensesService.cs | 208 ++++++++++++++++-- .../ServiceInterfaces/IExpensesService.cs | 2 +- 4 files changed, 201 insertions(+), 27 deletions(-) create mode 100644 Marco.Pms.Model/Filters/RecurringPaymentFilter.cs diff --git a/Marco.Pms.Model/Filters/RecurringPaymentFilter.cs b/Marco.Pms.Model/Filters/RecurringPaymentFilter.cs new file mode 100644 index 0000000..3c6a6c9 --- /dev/null +++ b/Marco.Pms.Model/Filters/RecurringPaymentFilter.cs @@ -0,0 +1,14 @@ +namespace Marco.Pms.Model.Filters +{ + public class RecurringPaymentFilter + { + public List? ProjectIds { get; set; } + public List? StatusIds { get; set; } + public List? CreatedByIds { get; set; } + public List? CurrencyIds { get; set; } + public List? ExpenseCategoryIds { get; set; } + public List? Payees { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index 5030d26..260d9cf 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -215,10 +215,10 @@ namespace Marco.Pms.Services.Controllers #region =================================================================== Recurring Payment Functions =================================================================== [HttpGet("get/recurring-payment/list")] - public async Task GetRecurringRequestList([FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] bool isActive = true, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1) + public async Task GetRecurringPaymentList([FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] bool isActive = true, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var response = await _expensesService.GetRecurringRequestListAsync(searchString, filter, isActive, pageSize, pageNumber, loggedInEmployee, tenantId); + var response = await _expensesService.GetRecurringPaymentListAsync(searchString, filter, isActive, pageSize, pageNumber, loggedInEmployee, tenantId); return StatusCode(response.StatusCode, response); } diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 7cebd65..e5b6a8c 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -2316,34 +2316,156 @@ namespace Marco.Pms.Services.Service #region =================================================================== Recurring Payment Functions =================================================================== - public async Task> GetRecurringRequestListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId) + public async Task> GetRecurringPaymentListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId) { - var recurringPaymentQuery = _context.RecurringPayments - .Where(rp => rp.TenantId == tenantId && rp.IsActive == isActive); + _logger.LogInfo("Start GetRecurringPaymentListAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}, PageNumber: {PageNumber}, PageSize: {PageSize}", + loggedInEmployee.Id, tenantId, pageNumber, pageSize); - var recurringPayments = await recurringPaymentQuery - .ToListAsync(); - - var totalEntities = await recurringPaymentQuery.CountAsync(); - - var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize); - - var results = recurringPayments.Select(rp => + try { - var result = _mapper.Map(rp); - result.RecurringPaymentUId = $"{rp.UIDPrefix}/{rp.UIDPostfix:D5}"; - return result; - }).ToList(); + // Check permissions concurrently: view self and view all 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 response = new + var hasViewAllPermissionTask = Task.Run(async () => + { + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + return await permissionService.HasPermission(PermissionsMaster.ViewAllRecurring, loggedInEmployee.Id); + }); + + await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask); + + bool hasViewSelfPermission = hasViewSelfPermissionTask.Result; + bool hasViewAllPermission = hasViewAllPermissionTask.Result; + + // Deny access if user has no relevant permissions + if (!hasViewAllPermission && !hasViewSelfPermission) + { + _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); + } + + // Base query with required navigation properties and tenant + active status filters + var recurringPaymentQuery = _context.RecurringPayments + .Include(rp => rp.Currency) + .Include(rp => rp.ExpenseCategory) + .Include(rp => rp.Status) + .Include(rp => rp.Project) + .Include(rp => rp.CreatedBy).ThenInclude(e => e!.JobRole) + .Where(rp => rp.TenantId == tenantId && rp.IsActive == isActive); + + // If user has only view-self permission, restrict to their own payments + if (hasViewSelfPermission && !hasViewAllPermission) + { + recurringPaymentQuery = recurringPaymentQuery.Where(rp => rp.CreatedById == loggedInEmployee.Id); + } + + // Deserialize and apply advanced filters + RecurringPaymentFilter? recurringPaymentFilter = TryDeserializeRecurringPaymentFilter(filter); + + if (recurringPaymentFilter != null) + { + if (recurringPaymentFilter.ProjectIds?.Any() ?? false) + { + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => rp.ProjectId.HasValue && recurringPaymentFilter.ProjectIds.Contains(rp.ProjectId.Value)); + } + if (recurringPaymentFilter.StatusIds?.Any() ?? false) + { + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => recurringPaymentFilter.StatusIds.Contains(rp.StatusId)); + } + if (recurringPaymentFilter.CreatedByIds?.Any() ?? false) + { + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => recurringPaymentFilter.CreatedByIds.Contains(rp.CreatedById)); + } + if (recurringPaymentFilter.CurrencyIds?.Any() ?? false) + { + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => recurringPaymentFilter.CurrencyIds.Contains(rp.CurrencyId)); + } + if (recurringPaymentFilter.ExpenseCategoryIds?.Any() ?? false) + { + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => rp.ExpenseCategoryId.HasValue && recurringPaymentFilter.ExpenseCategoryIds.Contains(rp.ExpenseCategoryId.Value)); + } + if (recurringPaymentFilter.Payees?.Any() ?? false) + { + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => recurringPaymentFilter.Payees.Contains(rp.Payee)); + } + if (recurringPaymentFilter.StartDate.HasValue && recurringPaymentFilter.EndDate.HasValue) + { + DateTime startDate = recurringPaymentFilter.StartDate.Value.Date; + DateTime endDate = recurringPaymentFilter.EndDate.Value.Date; + + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => rp.CreatedAt.Date >= startDate && rp.CreatedAt.Date <= endDate); + } + } + + // Apply search string filter if provided + if (!string.IsNullOrWhiteSpace(searchString)) + { + recurringPaymentQuery = recurringPaymentQuery + .Where(rp => + rp.Payee.Contains(searchString) || + rp.Title.Contains(searchString) || + (rp.UIDPrefix + "/" + rp.UIDPostfix.ToString().PadLeft(5, '0')).Contains(searchString) + ); + } + + // Get total count of matching records for pagination + var totalEntities = await recurringPaymentQuery.CountAsync(); + + // Calculate total pages (ceil) + var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize); + + // Fetch paginated data ordered by creation date desc + var recurringPayments = await recurringPaymentQuery + .OrderByDescending(rp => rp.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + // Map entities to view models and set recurring payment UID + var results = recurringPayments.Select(rp => + { + var vm = _mapper.Map(rp); + vm.RecurringPaymentUId = $"{rp.UIDPrefix}/{rp.UIDPostfix:D5}"; + return vm; + }).ToList(); + + var response = new + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalEntities = totalEntities, + Data = results, + }; + + _logger.LogInfo("Recurring payments fetched successfully: {Count} records for TenantId: {TenantId}, Page: {PageNumber}/{TotalPages}", + results.Count, tenantId, pageNumber, totalPages); + + return ApiResponse.SuccessResponse(response, $"{results.Count} recurring payments fetched successfully.", 200); + } + catch (Exception ex) { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalEntities = totalEntities, - Data = results, - }; - return ApiResponse.SuccessResponse(response, $"{results.Count} Recurring payments fetched successfully", 200); + _logger.LogError(ex, "Error in GetRecurringPaymentListAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}: {Message}", loggedInEmployee.Id, tenantId, ex.Message); + return ApiResponse.ErrorResponse("An error occurred while fetching recurring payments.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End GetRecurringPaymentListAsync called by EmployeeId: {EmployeeId}", loggedInEmployee.Id); + } } + public async Task> CreateRecurringPaymentAsync(RecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("Start CreateRecurringPaymentAsync called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", loggedInEmployee.Id, tenantId); @@ -2856,7 +2978,7 @@ namespace Marco.Pms.Services.Service } catch (JsonException ex) { - _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter); + _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializePaymentRequestFilter), filter); // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients). try @@ -2871,7 +2993,45 @@ namespace Marco.Pms.Services.Service catch (JsonException ex1) { // If both attempts fail, log the final error and return null. - _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter); + _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializePaymentRequestFilter), filter); + return null; + } + } + return expenseFilter; + } + private RecurringPaymentFilter? TryDeserializeRecurringPaymentFilter(string? filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return null; + } + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + RecurringPaymentFilter? expenseFilter = null; + + try + { + // First, try to deserialize directly. This is the expected case (e.g., from a web client). + expenseFilter = JsonSerializer.Deserialize(filter, options); + } + catch (JsonException ex) + { + _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeRecurringPaymentFilter), filter); + + // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients). + try + { + // Unescape the string first, then deserialize the result. + string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + if (!string.IsNullOrWhiteSpace(unescapedJsonString)) + { + expenseFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + } + } + catch (JsonException ex1) + { + // If both attempts fail, log the final error and return null. + _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeRecurringPaymentFilter), filter); return null; } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index dc51fda..bf25095 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -29,7 +29,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces #endregion #region =================================================================== Recurring Payment Functions =================================================================== - Task> GetRecurringRequestListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId); + Task> GetRecurringPaymentListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId); Task> CreateRecurringPaymentAsync(RecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId); Task> EditRecurringPaymentAsync(Guid id, RecurringTemplateDto model, Employee loggedInEmployee, Guid tenantId); #endregion