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