From 0fe59223e22e2fc20a866d1287e1d534d12c8626 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 27 Nov 2025 14:42:38 +0530 Subject: [PATCH] Added an API to get list of delivery challan by purchase invoice ID --- .../Controllers/PurchaseInvoiceController.cs | 12 ++- .../Helpers/CacheUpdateHelper.cs | 21 ++--- .../Service/PurchaseInvoiceService.cs | 82 ++++++++++++++++++- .../IPurchaseInvoiceService.cs | 1 + 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs index 5d67aa1..398c646 100644 --- a/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs +++ b/Marco.Pms.Services/Controllers/PurchaseInvoiceController.cs @@ -40,7 +40,7 @@ namespace Marco.Pms.Services.Controllers /// Token to propagate notification that operations should be canceled. /// A HTTP 200 OK response with a list of purchase invoices or an appropriate HTTP error code. [HttpGet("list")] - public async Task GetPurchaseInvoiceListAsync([FromQuery] string? searchString, [FromQuery] string? filter, CancellationToken cancellationToken, [FromQuery] bool isActive = true, + public async Task GetPurchaseInvoiceList([FromQuery] string? searchString, [FromQuery] string? filter, CancellationToken cancellationToken, [FromQuery] bool isActive = true, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1) { // Get the currently logged-in employee @@ -54,7 +54,7 @@ namespace Marco.Pms.Services.Controllers } [HttpGet("details/{id}")] - public async Task GetPurchaseInvoiceDetailsAsync(Guid id, CancellationToken cancellationToken) + public async Task GetPurchaseInvoiceDetails(Guid id, CancellationToken cancellationToken) { // Get the currently logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -125,6 +125,14 @@ namespace Marco.Pms.Services.Controllers #region =================================================================== Delivery Challan Functions =================================================================== + [HttpGet("delivery-challan/list/{purchaseInvoiceId}")] + public async Task GetDeliveryChallans(Guid purchaseInvoiceId, CancellationToken cancellationToken) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _purchaseInvoiceService.GetDeliveryChallansAsync(purchaseInvoiceId, loggedInEmployee, tenantId, cancellationToken); + return StatusCode(response.StatusCode, response); + } + /// /// Adds a delivery challan. /// diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index ce78e79..4dd5377 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -116,7 +116,7 @@ namespace Marco.Pms.Services.Helpers using var context = _dbContextFactory.CreateDbContext(); return await context.ProjectAllocations .AsNoTracking() - .CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive); // Server-side count is efficient + .CountAsync(pa => pa.ProjectId == project.Id && pa.TenantId == project.TenantId && pa.IsActive); // Server-side count is efficient }); // This task fetches the entire infrastructure hierarchy and performs aggregations in the database. @@ -127,26 +127,26 @@ namespace Marco.Pms.Services.Helpers // 1. Fetch all hierarchical data using projections. // This is still a chain, but it's inside one task and much faster due to projections. var buildings = await context.Buildings.AsNoTracking() - .Where(b => b.ProjectId == project.Id) + .Where(b => b.ProjectId == project.Id && b.TenantId == project.TenantId) .Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) .ToListAsync(); var buildingIds = buildings.Select(b => b.Id).ToList(); var floors = await context.Floor.AsNoTracking() - .Where(f => buildingIds.Contains(f.BuildingId)) + .Where(f => buildingIds.Contains(f.BuildingId) && f.TenantId == project.TenantId) .Select(f => new { f.Id, f.BuildingId, f.FloorName }) .ToListAsync(); var floorIds = floors.Select(f => f.Id).ToList(); var workAreas = await context.WorkAreas.AsNoTracking() - .Where(wa => floorIds.Contains(wa.FloorId)) + .Where(wa => floorIds.Contains(wa.FloorId) && wa.TenantId == project.TenantId) .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) .ToListAsync(); var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); // 2. THE KEY OPTIMIZATION: Aggregate work items in the database. var workSummaries = await context.WorkItems.AsNoTracking() - .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .Where(wi => workAreaIds.Contains(wi.WorkAreaId) && wi.TenantId == project.TenantId) .GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server .Select(g => new // Let the DB do the SUM { @@ -281,6 +281,7 @@ namespace Marco.Pms.Services.Helpers var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList(); var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList(); var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList(); + var tenantIds = projects.Select(p => p.TenantId).Distinct().ToList(); // --- Step 1: Fetch all required data in maximum parallel --- // Each task uses its own DbContext and selects only the required columns (projection). @@ -320,7 +321,7 @@ namespace Marco.Pms.Services.Helpers // Server-side aggregation and projection into a dictionary return await context.ProjectAllocations .AsNoTracking() - .Where(pa => projectIds.Contains(pa.ProjectId) && pa.IsActive) + .Where(pa => projectIds.Contains(pa.ProjectId) && tenantIds.Contains(pa.TenantId) && pa.IsActive) .GroupBy(pa => pa.ProjectId) .Select(g => new { ProjectId = g.Key, Count = g.Count() }) .ToDictionaryAsync(x => x.ProjectId, x => x.Count); @@ -331,7 +332,7 @@ namespace Marco.Pms.Services.Helpers using var context = _dbContextFactory.CreateDbContext(); return await context.Buildings .AsNoTracking() - .Where(b => projectIds.Contains(b.ProjectId)) + .Where(b => projectIds.Contains(b.ProjectId) && tenantIds.Contains(b.TenantId)) .Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) // Projection .ToListAsync(); }); @@ -345,7 +346,7 @@ namespace Marco.Pms.Services.Helpers using var context = _dbContextFactory.CreateDbContext(); return await context.Floor .AsNoTracking() - .Where(f => buildingIds.Contains(f.BuildingId)) + .Where(f => buildingIds.Contains(f.BuildingId) && tenantIds.Contains(f.TenantId)) .Select(f => new { f.Id, f.BuildingId, f.FloorName }) // Projection .ToListAsync(); }); @@ -359,7 +360,7 @@ namespace Marco.Pms.Services.Helpers using var context = _dbContextFactory.CreateDbContext(); return await context.WorkAreas .AsNoTracking() - .Where(wa => floorIds.Contains(wa.FloorId)) + .Where(wa => floorIds.Contains(wa.FloorId) && tenantIds.Contains(wa.TenantId)) .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) // Projection .ToListAsync(); }); @@ -376,7 +377,7 @@ namespace Marco.Pms.Services.Helpers // Let the DB do the SUM. This is much faster and transfers less data. return await context.WorkItems .AsNoTracking() - .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .Where(wi => workAreaIds.Contains(wi.WorkAreaId) && tenantIds.Contains(wi.TenantId)) .GroupBy(wi => wi.WorkAreaId) .Select(g => new { diff --git a/Marco.Pms.Services/Service/PurchaseInvoiceService.cs b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs index f3db011..a31d997 100644 --- a/Marco.Pms.Services/Service/PurchaseInvoiceService.cs +++ b/Marco.Pms.Services/Service/PurchaseInvoiceService.cs @@ -886,6 +886,86 @@ namespace Marco.Pms.Services.Service #endregion #region =================================================================== Delivery Challan Functions =================================================================== + + public async Task>> GetDeliveryChallansAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct) + { + // 1. Setup Context + // Using a factory ensures a clean context for this specific unit of work. + await using var context = await _dbContextFactory.CreateDbContextAsync(ct); + + try + { + _logger.LogInfo("GetDeliveryChallans: Fetching challans. InvoiceId: {InvoiceId}, Tenant: {TenantId}", purchaseInvoiceId, tenantId); + + // 2. Optimized Validation + // Use AnyAsync() instead of FirstOrDefaultAsync(). + // We only need to know if it *exists*, we don't need to load the data into memory. + var isInvoiceValid = await context.PurchaseInvoiceDetails + .AsNoTracking() + .AnyAsync(pid => pid.Id == purchaseInvoiceId && pid.TenantId == tenantId, ct); + + if (!isInvoiceValid) + { + _logger.LogWarning("GetDeliveryChallans: Invoice not found. InvoiceId: {InvoiceId}", purchaseInvoiceId); + return ApiResponse>.ErrorResponse("Invalid Purchase Invoice", "The specified purchase invoice does not exist.", 404); + } + + // 3. Data Retrieval + // Fetch only valid records with necessary related data. + var deliveryChallanEntities = await context.DeliveryChallanDetails + .AsNoTracking() + .Include(dc => dc.PurchaseInvoice) + .Include(dc => dc.Attachment).ThenInclude(pia => pia!.Document) + .Where(dc => dc.PurchaseInvoiceId == purchaseInvoiceId + && dc.TenantId == tenantId + && dc.Attachment != null + && dc.Attachment.Document != null) // Ensure strict data integrity + .ToListAsync(ct); + + // 4. Early Exit for Empty Lists + // Returns an empty list with 200 OK immediately, avoiding unnecessary mapping/looping. + if (!deliveryChallanEntities.Any()) + { + _logger.LogInfo("GetDeliveryChallans: No challans found for InvoiceId: {InvoiceId}", purchaseInvoiceId); + return ApiResponse>.SuccessResponse(new List(), "No delivery challans found.", 200); + } + + // 5. Mapping and Transformation + // We map the entities to View Models first, then apply business logic (S3 URLs). + // Using Map> is generally more efficient than mapping inside a Select loop for complex objects. + + // Enhance VMs with Signed URLs + // We iterate through the already-mapped list to populate non-database fields. + // Zip or standard for-loop could be used, but since we mapped a list, we need to match them up. + // Note: Automapper preserves order, so index matching works, but iterating the Source Entity to populate the Dest VM is safer. + + var responseList = deliveryChallanEntities.Select(dc => + { + var result = _mapper.Map(dc); + if (dc.Attachment?.Document != null) + { + result.Attachment!.PreSignedUrl = _s3Service.GeneratePreSignedUrl(dc.Attachment.Document.S3Key); + + // Fallback logic for thumbnail + var thumbKey = !string.IsNullOrEmpty(dc.Attachment.Document.ThumbS3Key) + ? dc.Attachment.Document.ThumbS3Key + : dc.Attachment.Document.S3Key; + + result.Attachment.ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(thumbKey); + } + return result; + }).ToList(); + + _logger.LogInfo("GetDeliveryChallans: Successfully returned {Count} items.", responseList.Count); + + return ApiResponse>.SuccessResponse(responseList, "List of delivery challans fetched successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "GetDeliveryChallans: An error occurred. InvoiceId: {InvoiceId}", purchaseInvoiceId); + return ApiResponse>.ErrorResponse("Internal Server Error", "An unexpected error occurred while fetching delivery challans.", 500); + } + } public async Task> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct) { // 1. Input Validation - Fail Fast @@ -923,7 +1003,7 @@ namespace Marco.Pms.Services.Service // Note: We project only what we need or map later to avoid EF translation issues with complex Mappers. var purchaseInvoiceEntity = await context.PurchaseInvoiceDetails .AsNoTracking() - .FirstOrDefaultAsync(pid => pid.Id == model.PurchaseInvoiceId && pid.TenantId == tenantId, ct); + .FirstOrDefaultAsync(pid => pid.Id == model.PurchaseInvoiceId && pid.IsActive && pid.TenantId == tenantId, ct); if (purchaseInvoiceEntity == null) { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs index 1225fee..583d40e 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IPurchaseInvoiceService.cs @@ -18,6 +18,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces #endregion #region =================================================================== Delivery Challan Functions =================================================================== + Task>> GetDeliveryChallansAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); Task> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct); #endregion