diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 6aa5305..5db4d4c 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -1146,9 +1146,9 @@ namespace Marco.Pms.Services.Helpers response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map(p)).FirstOrDefault() ?? new ProjectBasicMongoDB(); response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map(p)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); - response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); - response.ApprovedBy = approvedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); - response.ProcessedBy = processedBy.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); + response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); + response.ApprovedBy = approvedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); + response.ProcessedBy = processedBy.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); response.Status = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map(s.Status)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB(); if (response.Status.Id == string.Empty) { diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index a4c58b3..3d1b90d 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -112,7 +112,7 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap() .ForMember( dest => dest.Id, - opt => opt.MapFrom(src => Guid.Parse(src.Id))) + opt => opt.MapFrom(src => string.IsNullOrWhiteSpace(src.Id) ? Guid.Empty : Guid.Parse(src.Id))) .ForMember( dest => dest.JobRoleId, opt => opt.MapFrom(src => Guid.Parse(src.JobRoleId ?? ""))); diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 03a22f6..5b2420c 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -5,6 +5,11 @@ 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.MongoDBModels; +using Marco.Pms.Model.MongoDBModels.Employees; +using Marco.Pms.Model.MongoDBModels.Expenses; +using Marco.Pms.Model.MongoDBModels.Masters; +using Marco.Pms.Model.MongoDBModels.Project; using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; @@ -117,14 +122,13 @@ namespace Marco.Pms.Services.Service var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result, pageNumber, pageSize, expenseFilter, searchString); + // 3. --- Build Base Query and Apply Permissions --- + // Start with a base IQueryable. Filters will be chained onto this. + var expensesQuery = _context.Expenses + .Where(e => e.TenantId == tenantId); // Always filter by TenantId first. + if (cacheList == null) { - - // 3. --- Build Base Query and Apply Permissions --- - // Start with a base IQueryable. Filters will be chained onto this. - var expensesQuery = _context.Expenses - .Where(e => e.TenantId == tenantId); // Always filter by TenantId first. - await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId); // Apply permission-based filtering BEFORE any other filters or pagination. @@ -264,12 +268,14 @@ namespace Marco.Pms.Services.Service var expenseDetails = await _cache.GetExpenseDetailsById(id, tenantId); if (expenseDetails == null) { - expenseDetails = await _cache.AddExpenseByIdAsync(id, tenantId); - if (expenseDetails == null) + var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && 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); return ApiResponse.ErrorResponse("Expense Not Found", "Expense Not Found", 404); } + expenseDetails = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId); } var vm = _mapper.Map(expenseDetails); @@ -1182,6 +1188,136 @@ namespace Marco.Pms.Services.Service return expenseList; } + private async Task GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId) + { + var projectTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId); + }); + var paidByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId); + }); + var createdByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId); + }); + var reviewedByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId); + }); + var approvedByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId); + }); + var processedByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId); + }); + var expenseTypeTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId); + }); + var paymentModeTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId); + }); + var statusMappingTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ExpensesStatusMapping + .Include(s => s.Status) + .Include(s => s.NextStatus) + .AsNoTracking() + .Where(es => es.StatusId == model.StatusId && es.Status != null) + .GroupBy(s => s.StatusId) + .Select(g => new + { + StatusId = g.Key, + Status = g.Select(s => s.Status).FirstOrDefault(), + NextStatus = g.Select(s => s.NextStatus).ToList() + }).FirstOrDefaultAsync(); + }); + var statusTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ExpensesStatusMaster + .AsNoTracking() + .FirstOrDefaultAsync(es => es.Id == model.StatusId); + }); + var billAttachmentsTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.BillAttachments + .Include(ba => ba.Document) + .AsNoTracking() + .Where(ba => ba.ExpensesId == model.Id && ba.Document != null) + .GroupBy(ba => ba.ExpensesId) + .Select(g => new + { + ExpensesId = g.Key, + Documents = g.Select(ba => new DocumentMongoDB + { + DocumentId = ba.Document!.Id.ToString(), + FileName = ba.Document.FileName, + ContentType = ba.Document.ContentType, + S3Key = ba.Document.S3Key, + ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key + }).ToList() + }) + .FirstOrDefaultAsync(); + }); + + // Await all prerequisite checks at once. + await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask, + processedByTask, statusTask, billAttachmentsTask); + + var project = projectTask.Result; + var expenseType = expenseTypeTask.Result; + var paymentMode = paymentModeTask.Result; + var statusMapping = statusMappingTask.Result; + var paidBy = paidByTask.Result; + var createdBy = createdByTask.Result; + var reviewedBy = reviewedByTask.Result; + var approvedBy = approvedByTask.Result; + var processedBy = processedByTask.Result; + var billAttachment = billAttachmentsTask.Result; + + + var response = _mapper.Map(model); + + response.Project = _mapper.Map(project); + response.PaidBy = _mapper.Map(paidBy); + response.CreatedBy = _mapper.Map(createdBy); + response.ReviewedBy = _mapper.Map(reviewedBy); + response.ApprovedBy = _mapper.Map(approvedBy); + response.ProcessedBy = _mapper.Map(processedBy); + if (statusMapping != null) + { + response.Status = _mapper.Map(statusMapping.Status); + response.NextStatus = _mapper.Map>(statusMapping.NextStatus); + } + if (response.Status == null) + { + var status = statusTask.Result; + response.Status = _mapper.Map(status); + } + response.PaymentMode = _mapper.Map(paymentMode); + response.ExpensesType = _mapper.Map(expenseType); + if (billAttachment != null) response.Documents = billAttachment.Documents; + + return response; + + } + /// /// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).