Added an API to get list of delivery challan by purchase invoice ID

This commit is contained in:
ashutosh.nehete 2025-11-27 14:42:38 +05:30
parent 41feb58d45
commit 0fe59223e2
4 changed files with 103 additions and 13 deletions

View File

@ -40,7 +40,7 @@ namespace Marco.Pms.Services.Controllers
/// <param name="cancellationToken">Token to propagate notification that operations should be canceled.</param>
/// <returns>A HTTP 200 OK response with a list of purchase invoices or an appropriate HTTP error code.</returns>
[HttpGet("list")]
public async Task<IActionResult> GetPurchaseInvoiceListAsync([FromQuery] string? searchString, [FromQuery] string? filter, CancellationToken cancellationToken, [FromQuery] bool isActive = true,
public async Task<IActionResult> 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<IActionResult> GetPurchaseInvoiceDetailsAsync(Guid id, CancellationToken cancellationToken)
public async Task<IActionResult> 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<IActionResult> GetDeliveryChallans(Guid purchaseInvoiceId, CancellationToken cancellationToken)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _purchaseInvoiceService.GetDeliveryChallansAsync(purchaseInvoiceId, loggedInEmployee, tenantId, cancellationToken);
return StatusCode(response.StatusCode, response);
}
/// <summary>
/// Adds a delivery challan.
/// </summary>

View File

@ -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
{

View File

@ -886,6 +886,86 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Delivery Challan Functions ===================================================================
public async Task<ApiResponse<List<DeliveryChallanVM>>> 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<List<DeliveryChallanVM>>.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<List<DeliveryChallanVM>>.SuccessResponse(new List<DeliveryChallanVM>(), "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<List<T>> 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<DeliveryChallanVM>(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<List<DeliveryChallanVM>>.SuccessResponse(responseList, "List of delivery challans fetched successfully.", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "GetDeliveryChallans: An error occurred. InvoiceId: {InvoiceId}", purchaseInvoiceId);
return ApiResponse<List<DeliveryChallanVM>>.ErrorResponse("Internal Server Error", "An unexpected error occurred while fetching delivery challans.", 500);
}
}
public async Task<ApiResponse<DeliveryChallanVM>> 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)
{

View File

@ -18,6 +18,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
#endregion
#region =================================================================== Delivery Challan Functions ===================================================================
Task<ApiResponse<List<DeliveryChallanVM>>> GetDeliveryChallansAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
#endregion