From bb14204d65c86166f29e52ccafc14fbdf387a3b0 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 3 Nov 2025 10:47:55 +0530 Subject: [PATCH 1/2] Added crude get payment request list API --- .../Filters/PaymentRequestFilter.cs | 13 + .../TenantModels/SubscriptionPlan.cs | 2 +- .../Controllers/ExpenseController.cs | 33 ++- Marco.Pms.Services/Service/ExpensesService.cs | 233 ++++++++++++------ .../ServiceInterfaces/IExpensesService.cs | 16 +- 5 files changed, 203 insertions(+), 94 deletions(-) create mode 100644 Marco.Pms.Model/Filters/PaymentRequestFilter.cs diff --git a/Marco.Pms.Model/Filters/PaymentRequestFilter.cs b/Marco.Pms.Model/Filters/PaymentRequestFilter.cs new file mode 100644 index 0000000..8b404af --- /dev/null +++ b/Marco.Pms.Model/Filters/PaymentRequestFilter.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.Filters +{ + public class PaymentRequestFilter + { + 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 DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + } +} diff --git a/Marco.Pms.Model/TenantModels/SubscriptionPlan.cs b/Marco.Pms.Model/TenantModels/SubscriptionPlan.cs index a23bf87..3016e4c 100644 --- a/Marco.Pms.Model/TenantModels/SubscriptionPlan.cs +++ b/Marco.Pms.Model/TenantModels/SubscriptionPlan.cs @@ -10,6 +10,6 @@ public enum PLAN_FREQUENCY { - MONTHLY = 0, QUARTERLY = 1, HALF_YEARLY = 2, YEARLY = 3, DAILY = 4 + MONTHLY = 0, QUARTERLY = 1, HALF_YEARLY = 2, YEARLY = 3, DAILY = 4, WEEKLY = 5 } } diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index 3824f42..3c8b41d 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -48,7 +48,7 @@ namespace Marco.Pms.Services.Controllers } [HttpGet("details")] - public async Task GetExpenseDetails([FromQuery] Guid? id,[FromQuery] string? financeUId) + public async Task GetExpenseDetails([FromQuery] Guid? id, [FromQuery] string? financeUId) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _expensesService.GetExpenseDetailsAsync(id, financeUId, loggedInEmployee, tenantId); @@ -123,18 +123,17 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } - [HttpGet("/get/transactions/{employeeId}")] - public async Task GetAdvancePaymentTransaction(Guid employeeId) - { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var response = await _expensesService.GetAdvancePaymentTransactionAsync(employeeId); - - return StatusCode(response.StatusCode, response); - - } #endregion #region =================================================================== Payment Request Functions =================================================================== + + [HttpGet("get/payment-requests/list")] + public async Task GetPaymentRequestList([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.GetPaymentRequestListAsync(searchString, filter, isActive, pageSize, pageNumber, loggedInEmployee, tenantId); + return StatusCode(response.StatusCode, response); + } [HttpPost("payment-request/create")] public async Task CreatePaymentRequest([FromBody] PaymentRequestDto model) { @@ -160,5 +159,19 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } #endregion + + #region =================================================================== Advance Payment Functions =================================================================== + + [HttpGet("/get/transactions/{employeeId}")] + public async Task GetAdvancePaymentTransaction(Guid employeeId) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _expensesService.GetAdvancePaymentTransactionAsync(employeeId); + + return StatusCode(response.StatusCode, response); + + } + #endregion + } } diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index f725b7c..24b22dd 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -5,6 +5,7 @@ using Marco.Pms.Model.Dtos.Expenses; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Expenses; +using Marco.Pms.Model.Filters; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.MongoDBModels.Employees; using Marco.Pms.Model.MongoDBModels.Expenses; @@ -286,23 +287,15 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Error Occured", ExceptionMapper(ex), 500); } } - public async Task> GetExpenseDetailsAsync(Guid? id,string? financeUId, Employee loggedInEmployee, Guid tenantId) + public async Task> GetExpenseDetailsAsync(Guid? id, string? financeUId, Employee loggedInEmployee, Guid tenantId) { try { - - if(string.IsNullOrWhiteSpace(financeUId)) - return ApiResponse.ErrorResponse("Finance UID is required.", null, 400); - - var parts = financeUId.Split('-'); - if (parts.Length != 2) - return ApiResponse.ErrorResponse("Invalid Finance UID format.", null, 400); - - var prefix = parts[0]; - var postfixStr = parts[1]; - - if (!int.TryParse(postfixStr, out int postfix)) - return ApiResponse.ErrorResponse("Invalid Finance UID number format.", null, 400); + if (!id.HasValue && string.IsNullOrWhiteSpace(financeUId)) + { + _logger.LogWarning("User do not provided id nor expenseUId"); + return ApiResponse.ErrorResponse("Id or ExpenseUId atleast one must be provided", "Id or ExpenseUId atleast one must be provided", 400); + } ExpenseDetailsMongoDB? expenseDetails = null; if (expenseDetails == null) { @@ -317,11 +310,18 @@ namespace Marco.Pms.Services.Service .Include(e => e.PaymentMode) .Include(e => e.ExpensesType) .Include(e => e.Status) - .AsNoTracking().FirstOrDefaultAsync(e =>(e.Id == id || e.ExpenseUId == financeUId ) && e.TenantId == tenantId); + .AsNoTracking().FirstOrDefaultAsync(e => (e.Id == id || e.ExpenseUId == financeUId) && e.TenantId == tenantId); if (expense == null) { - _logger.LogWarning("User attempted to fetch expense details with ID {ExpenseId}, but not found in both database and cache", id); + if (id.HasValue) + { + _logger.LogWarning("User attempted to fetch expense details with ID {ExpenseId}, but not found in both database and cache", id); + } + else if (!string.IsNullOrWhiteSpace(financeUId)) + { + _logger.LogWarning("User attempted to fetch expense details with expenseUId {ExpenseUId}, but not found in both database and cache", financeUId); + } return ApiResponse.ErrorResponse("Expense Not Found", "Expense Not Found", 404); } expenseDetails = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId); @@ -387,7 +387,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError(ex, "An unhandled exception occurred while fetching an expense details {ExpenseId}.", id); + _logger.LogError(ex, "An unhandled exception occurred while fetching an expense details {ExpenseId} or {ExpenseUId}.", id ?? Guid.Empty, financeUId ?? ""); return ApiResponse.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500); } } @@ -436,60 +436,6 @@ namespace Marco.Pms.Services.Service } } - public async Task> GetAdvancePaymentTransactionAsync(Guid employeeId) - { - try - { - var transactions = await _context.AdvancePaymentTransactions - .Include(t => t.Employee) - .Include(t => t.CreatedBy) - .Where(t => t.EmployeeId == employeeId && t.IsActive) - .OrderByDescending(t => t.CreatedAt) - //.Select(t => new AdvancePaymentTransactionVM - //{ - // Id = t.Id, - // FinanceUId = t.FinanceUIdPrefix + "/" + t.FinanceUIdPostfix, - // Amount = t.Amount, - // CreatedAt = t.CreatedAt, - // IsActive = t.IsActive, - // Employee = t.Employee == null ? null : new BasicEmployeeVM - // { - // Id = t.Employee.Id, - // FirstName = t.Employee.FirstName, - // LastName = t.Employee.LastName, - // }, - // CreatedBy = t.CreatedBy == null ? null : new BasicEmployeeVM - // { - // Id = t.CreatedBy.Id, - // FirstName = t.Employee.FirstName, - // LastName = t.Employee.LastName, - // } - //}) - .ToListAsync(); - - if (transactions == null || !transactions.Any()) - return ApiResponse.ErrorResponse("No advance payment transactions found.", null, 404); - - var response = transactions.Select(transaction => - { - var result = _mapper.Map(transaction); - result.FinanceUId = transaction.FinanceUIdPrefix + "/" + transaction.FinanceUIdPostfix.ToString("D5"); - return result; - }).ToList(); - - - - return ApiResponse.SuccessResponse(response, "Advance payment transaction fetched successfully", 200); - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception occurred while fetching the list of Advance Payment Transactions."); - return ApiResponse.ErrorResponse("Internal exception occurred.", ExceptionMapper(ex), 500); - } - } - - - #endregion #region =================================================================== Post Functions =================================================================== @@ -1151,6 +1097,71 @@ namespace Marco.Pms.Services.Service #region =================================================================== Payment Request Functions =================================================================== + public async Task> GetPaymentRequestListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId) + { + var paymentRequestQuery = _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) + .Where(pr => pr.TenantId == tenantId && + pr.IsActive == isActive && + pr.Currency != null && + pr.ExpenseCategory != null && + pr.ExpenseStatus != null && + pr.CreatedBy != null && + pr.CreatedBy.JobRole != null); + + //PaymentRequestFilter? paymentRequestFilter = TryDeserializePaymentRequestFilter(filter); + + //if (paymentRequestFilter != null) + //{ + // if (paymentRequestFilter.ProjectIds?.Any() ?? false) + // { + // paymentRequestQuery = paymentRequestQuery + // .Where(pr => pr.ProjectId.HasValue && + // paymentRequestFilter.ProjectIds.Contains(pr.ProjectId.Value)); + // } + // if (paymentRequestFilter.StatusIds?.Any() ?? false) + // { + // paymentRequestQuery = paymentRequestQuery + // .Where(pr => paymentRequestFilter.StatusIds.Contains(pr.ExpenseStatusId)); + // } + // if (paymentRequestFilter.CreatedByIds?.Any() ?? false) + // { + // paymentRequestQuery = paymentRequestQuery + // .Where(pr => paymentRequestFilter.CreatedByIds.Contains(pr.CreatedById)); + // } + // if (paymentRequestFilter.ExpenseCategoryIds?.Any() ?? false) + // { + // paymentRequestQuery = paymentRequestQuery + // .Where(pr => paymentRequestFilter.ExpenseCategoryIds.Contains(pr.ExpenseCategoryId)); + // } + //} + + var totalEntites = await paymentRequestQuery.CountAsync(); + + var paymentRequests = await paymentRequestQuery + .OrderByDescending(e => e.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize).ToListAsync(); + + + var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize); + + var response = paymentRequests.Select(pr => + { + var result = _mapper.Map(pr); + result.PaymentRequestUID = $"{pr.UIDPrefix}/{pr.UIDPostfix:D5}"; + return result; + }).ToList(); + + return ApiResponse.SuccessResponse(response, $"{0} Payment request fetched successfully", 200); + } + public async Task> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("Start CreatePaymentRequestAsync for EmployeeId: {EmployeeId} TenantId: {TenantId}", loggedInEmployee.Id, tenantId); @@ -1482,13 +1493,43 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("End EditPaymentRequestAsync for PaymentRequestId: {PaymentRequestId}", id); } } - #endregion - #region =================================================================== Payment Request Functions =================================================================== + #region =================================================================== Advance Payment Functions =================================================================== + public async Task> GetAdvancePaymentTransactionAsync(Guid employeeId) + { + try + { + var transactions = await _context.AdvancePaymentTransactions + .Include(t => t.Employee) + .Include(t => t.CreatedBy) + .Where(t => t.EmployeeId == employeeId && t.IsActive) + .OrderByDescending(t => t.CreatedAt) + .ToListAsync(); + + if (transactions == null || !transactions.Any()) + return ApiResponse.ErrorResponse("No advance payment transactions found.", null, 404); + + var response = transactions.Select(transaction => + { + var result = _mapper.Map(transaction); + result.FinanceUId = $"{transaction.FinanceUIdPrefix}/{transaction.FinanceUIdPostfix:D5}"; + return result; + }).ToList(); + + + + return ApiResponse.SuccessResponse(response, "Advance payment transaction fetched successfully", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occurred while fetching the list of Advance Payment Transactions."); + return ApiResponse.ErrorResponse("Internal exception occurred.", ExceptionMapper(ex), 500); + } + } #endregion - #region =================================================================== Payment Request Functions =================================================================== + #region =================================================================== Recurring Payment Functions =================================================================== #endregion #region =================================================================== Helper Functions =================================================================== @@ -1499,7 +1540,6 @@ namespace Marco.Pms.Services.Service var m = Regex.Match(id ?? string.Empty, @"(\d+)$"); return m.Success ? int.Parse(m.Value) : int.MinValue; // put invalid IDs at the bottom } - private static object ExceptionMapper(Exception ex) { return new @@ -1711,11 +1751,6 @@ namespace Marco.Pms.Services.Service } - /// - /// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string). - /// - /// The JSON filter string from the request. - /// An object or null if deserialization fails. private ExpensesFilter? TryDeserializeFilter(string? filter) { if (string.IsNullOrWhiteSpace(filter)) @@ -1754,6 +1789,44 @@ namespace Marco.Pms.Services.Service } return expenseFilter; } + private PaymentRequestFilter? TryDeserializePaymentRequestFilter(string? filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return null; + } + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + PaymentRequestFilter? 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(TryDeserializeFilter), 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(TryDeserializeFilter), filter); + return null; + } + } + return expenseFilter; + } /// /// Processes and uploads attachments concurrently, then adds the resulting entities to the main DbContext. diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index ee6e48f..66575a0 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -6,6 +6,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces { public interface IExpensesService { + #region =================================================================== Expenses Functions =================================================================== Task> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber); Task> GetExpenseDetailsAsync(Guid? id, string? financeUId, Employee loggedInEmployee, Guid tenantId); Task> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId); @@ -14,11 +15,20 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId); Task> UpdateExpanseAsync(Guid id, UpdateExpensesDto model, Employee loggedInEmployee, Guid tenantId); Task> DeleteExpanseAsync(Guid id, Employee loggedInEmployee, Guid tenantId); + #endregion - Task> GetAdvancePaymentTransactionAsync(Guid id); - - + #region =================================================================== Payment Request Functions =================================================================== + Task> GetPaymentRequestListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId); Task> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId); Task> EditPaymentRequestAsync(Guid id, PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId); + #endregion + + + #region =================================================================== Advance Payment Functions =================================================================== + Task> GetAdvancePaymentTransactionAsync(Guid id); + #endregion + + #region =================================================================== Recurring Payment Functions =================================================================== + #endregion } } From 93ff8aefb51453ff5a415d13ad0bf439535a7b7c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 3 Nov 2025 11:14:37 +0530 Subject: [PATCH 2/2] Optimized the get list of payment request API --- .../Filters/PaymentRequestFilter.cs | 1 + Marco.Pms.Services/Service/ExpensesService.cs | 176 ++++++++++++------ 2 files changed, 118 insertions(+), 59 deletions(-) diff --git a/Marco.Pms.Model/Filters/PaymentRequestFilter.cs b/Marco.Pms.Model/Filters/PaymentRequestFilter.cs index 8b404af..12b2bf6 100644 --- a/Marco.Pms.Model/Filters/PaymentRequestFilter.cs +++ b/Marco.Pms.Model/Filters/PaymentRequestFilter.cs @@ -7,6 +7,7 @@ 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/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 24b22dd..0e7d022 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -1096,72 +1096,130 @@ namespace Marco.Pms.Services.Service #endregion #region =================================================================== Payment Request Functions =================================================================== - public async Task> GetPaymentRequestListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId) { - var paymentRequestQuery = _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) - .Where(pr => pr.TenantId == tenantId && - pr.IsActive == isActive && - pr.Currency != null && - pr.ExpenseCategory != null && - pr.ExpenseStatus != null && - pr.CreatedBy != null && - pr.CreatedBy.JobRole != null); + _logger.LogInfo("Start GetPaymentRequestListAsync: TenantId={TenantId}, PageNumber={PageNumber}, PageSize={PageSize}, EmployeeId={EmployeeId}", + tenantId, pageNumber, pageSize, loggedInEmployee.Id); - //PaymentRequestFilter? paymentRequestFilter = TryDeserializePaymentRequestFilter(filter); - - //if (paymentRequestFilter != null) - //{ - // if (paymentRequestFilter.ProjectIds?.Any() ?? false) - // { - // paymentRequestQuery = paymentRequestQuery - // .Where(pr => pr.ProjectId.HasValue && - // paymentRequestFilter.ProjectIds.Contains(pr.ProjectId.Value)); - // } - // if (paymentRequestFilter.StatusIds?.Any() ?? false) - // { - // paymentRequestQuery = paymentRequestQuery - // .Where(pr => paymentRequestFilter.StatusIds.Contains(pr.ExpenseStatusId)); - // } - // if (paymentRequestFilter.CreatedByIds?.Any() ?? false) - // { - // paymentRequestQuery = paymentRequestQuery - // .Where(pr => paymentRequestFilter.CreatedByIds.Contains(pr.CreatedById)); - // } - // if (paymentRequestFilter.ExpenseCategoryIds?.Any() ?? false) - // { - // paymentRequestQuery = paymentRequestQuery - // .Where(pr => paymentRequestFilter.ExpenseCategoryIds.Contains(pr.ExpenseCategoryId)); - // } - //} - - var totalEntites = await paymentRequestQuery.CountAsync(); - - var paymentRequests = await paymentRequestQuery - .OrderByDescending(e => e.CreatedAt) - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize).ToListAsync(); - - - var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize); - - var response = paymentRequests.Select(pr => + try { - var result = _mapper.Map(pr); - result.PaymentRequestUID = $"{pr.UIDPrefix}/{pr.UIDPostfix:D5}"; - return result; - }).ToList(); + // Initial query including the necessary navigation properties and basic multi-tenant/security constraints + var paymentRequestQuery = _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) + .Where(pr => pr.TenantId == tenantId && + pr.IsActive == isActive && + pr.Currency != null && + pr.ExpenseCategory != null && + pr.ExpenseStatus != null && + pr.CreatedBy != null && + pr.CreatedBy.JobRole != null); - return ApiResponse.SuccessResponse(response, $"{0} Payment request fetched successfully", 200); + // Deserialize and apply advanced filter if provided + PaymentRequestFilter? paymentRequestFilter = TryDeserializePaymentRequestFilter(filter); + + if (paymentRequestFilter != null) + { + if (paymentRequestFilter.ProjectIds?.Any() ?? false) + { + paymentRequestQuery = paymentRequestQuery + .Where(pr => pr.ProjectId.HasValue && paymentRequestFilter.ProjectIds.Contains(pr.ProjectId.Value)); + } + if (paymentRequestFilter.StatusIds?.Any() ?? false) + { + paymentRequestQuery = paymentRequestQuery + .Where(pr => paymentRequestFilter.StatusIds.Contains(pr.ExpenseStatusId)); + } + if (paymentRequestFilter.CreatedByIds?.Any() ?? false) + { + paymentRequestQuery = paymentRequestQuery + .Where(pr => paymentRequestFilter.CreatedByIds.Contains(pr.CreatedById)); + } + if (paymentRequestFilter.CurrencyIds?.Any() ?? false) + { + paymentRequestQuery = paymentRequestQuery + .Where(pr => paymentRequestFilter.CurrencyIds.Contains(pr.CurrencyId)); + } + if (paymentRequestFilter.ExpenseCategoryIds?.Any() ?? false) + { + paymentRequestQuery = paymentRequestQuery + .Where(pr => pr.ExpenseCategoryId.HasValue && paymentRequestFilter.ExpenseCategoryIds.Contains(pr.ExpenseCategoryId.Value)); + } + if (paymentRequestFilter.Payees?.Any() ?? false) + { + paymentRequestQuery = paymentRequestQuery + .Where(pr => paymentRequestFilter.Payees.Contains(pr.Payee)); + } + if (paymentRequestFilter.StartDate.HasValue && paymentRequestFilter.EndDate.HasValue) + { + DateTime startDate = paymentRequestFilter.StartDate.Value.Date; + DateTime endDate = paymentRequestFilter.EndDate.Value.Date; + + paymentRequestQuery = paymentRequestQuery + .Where(pr => pr.CreatedAt.Date >= startDate && pr.CreatedAt.Date <= endDate); + } + } + + // Full-text search by payee, title, or UID + if (!string.IsNullOrWhiteSpace(searchString)) + { + paymentRequestQuery = paymentRequestQuery + .Where(pr => + pr.Payee.Contains(searchString) || + pr.Title.Contains(searchString) || + ($"{pr.UIDPrefix}/{pr.UIDPostfix:D5}").Contains(searchString) + ); + } + + // Get total count for pagination + var totalEntities = await paymentRequestQuery.CountAsync(); + + // Fetch paginated result set + var paymentRequests = await paymentRequestQuery + .OrderByDescending(e => e.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize); + + var results = paymentRequests.Select(pr => + { + var result = _mapper.Map(pr); + result.PaymentRequestUID = $"{pr.UIDPrefix}/{pr.UIDPostfix:D5}"; + return result; + }).ToList(); + + var response = new + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalEntities = totalEntities, + Data = results, + }; + + _logger.LogInfo("GetPaymentRequestListAsync: {ResultCount} payment requests fetched successfully for TenantId={TenantId} Page={PageNumber}/{TotalPages}", + results.Count, tenantId, pageNumber, totalPages); + + return ApiResponse.SuccessResponse(response, $"{results.Count} payment requests fetched successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred in GetPaymentRequestListAsync for TenantId={TenantId}, EmployeeId={EmployeeId}: {Message}", tenantId, loggedInEmployee.Id, ex.Message); + return ApiResponse.ErrorResponse("An error occurred while fetching payment requests.", ex.Message, 500); + } + finally + { + _logger.LogInfo("End GetPaymentRequestListAsync for TenantId={TenantId}, EmployeeId={EmployeeId}", tenantId, loggedInEmployee.Id); + } } + public async Task> CreatePaymentRequestAsync(PaymentRequestDto model, Employee loggedInEmployee, Guid tenantId) { _logger.LogInfo("Start CreatePaymentRequestAsync for EmployeeId: {EmployeeId} TenantId: {TenantId}", loggedInEmployee.Id, tenantId);