From 6bcc67bb636930e2416b3c896dbb91e1143b71c5 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 5 Dec 2025 15:40:26 +0530 Subject: [PATCH] Added an API to get purchase invoice overview --- .../Controllers/DashboardController.cs | 156 +++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 964160e..bf5ec4e 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -1225,7 +1225,11 @@ namespace Marco.Pms.Services.Controllers { // Correlation ID pattern for distributed tracing (if you use one) var correlationId = HttpContext.TraceIdentifier; - + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Invalid request: TenantId is empty on progression endpoint"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } _logger.LogInfo("Started GetCollectionOverviewAsync. CorrelationId: {CorrelationId}, ProjectId: {ProjectId}", correlationId, projectId ?? Guid.Empty); try @@ -1455,6 +1459,156 @@ namespace Marco.Pms.Services.Controllers } } + [HttpGet("purchase-invoice-overview")] + public async Task GetPurchaseInvoiceOverview() + { + // Correlation id for tracing this request across services/logs. + var correlationId = HttpContext.TraceIdentifier; + + _logger.LogInfo("GetPurchaseInvoiceOverview started. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); + + // Basic guard: invalid tenant. + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetPurchaseInvoiceOverview rejected due to empty TenantId. CorrelationId: {CorrelationId}", correlationId); + + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "The tenant identifier provided is invalid or missing.", 400)); + } + + try + { + // Fetch current employee context (if needed for authorization/audit). + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Run project queries in parallel to reduce latency. + var infraProjectTask = GetInfraProjectsAsync(tenantId); + var serviceProjectTask = GetServiceProjectsAsync(tenantId); + + await Task.WhenAll(infraProjectTask, serviceProjectTask); + + var projects = infraProjectTask.Result + .Union(serviceProjectTask.Result) + .ToList(); + + _logger.LogDebug("GetPurchaseInvoiceOverview loaded projects. Count: {ProjectCount}, TenantId: {TenantId}, CorrelationId: {CorrelationId}", projects.Count, tenantId, correlationId); + + // Query purchase invoices for the tenant. + var purchaseInvoices = await _context.PurchaseInvoiceDetails + .Include(pid => pid.Supplier) + .Include(pid => pid.Status) + .AsNoTracking() + .Where(pid => pid.TenantId == tenantId && pid.IsActive) + .ToListAsync(); + + _logger.LogInfo("GetPurchaseInvoiceOverview loaded invoices. InvoiceCount: {InvoiceCount}, TenantId: {TenantId}, CorrelationId: {CorrelationId}", + purchaseInvoices.Count, tenantId, correlationId); + + if (!purchaseInvoices.Any()) + { + // No invoices is not an error; return an empty but well-structured overview. + _logger.LogInfo("GetPurchaseInvoiceOverview: No active purchase invoices found. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); + + var emptyResponse = new + { + TotalInvoices = 0, + TotalValue = 0m, + AverageValue = 0m, + StatusBreakdown = Array.Empty(), + ProjectBreakdown = Array.Empty(), + TopSupplier = (object?)null + }; + + return Ok(ApiResponse.SuccessResponse( + emptyResponse, + "No active purchase invoices found for the specified tenant.", + StatusCodes.Status200OK)); + } + + var totalInvoices = purchaseInvoices.Count; + var totalValue = purchaseInvoices.Sum(pid => pid.BaseAmount); + + // Guard against divide-by-zero (in case BaseAmount is all zero). + var averageValue = totalInvoices > 0 + ? totalValue / totalInvoices + : 0; + + // Status-wise aggregation + var statusBreakdown = purchaseInvoices + .Where(pid => pid.Status != null) + .GroupBy(pid => pid.StatusId) + .Select(g => new + { + Id = g.Key, + Name = g.First().Status!.DisplayName, + Count = g.Count(), + TotalValue = g.Sum(pid => pid.BaseAmount), + Percentage = totalValue > 0 + ? Math.Round(g.Sum(pid => pid.BaseAmount) / totalValue * 100, 2) + : 0 + }) + .OrderByDescending(x => x.TotalValue) + .ToList(); + + // Project-wise aggregation (top 3 by value) + var projectBreakdown = purchaseInvoices + .GroupBy(pid => pid.ProjectId) + .Select(g => new + { + Id = g.Key, + Name = projects.FirstOrDefault(p => p.Id == g.Key)?.Name ?? "Unknown Project", + Count = g.Count(), + TotalValue = g.Sum(pid => pid.BaseAmount), + Percentage = totalValue > 0 + ? Math.Round(g.Sum(pid => pid.BaseAmount) / totalValue * 100, 2) + : 0 + }) + .OrderByDescending(pid => pid.TotalValue) + .Take(3) + .ToList(); + + // Top supplier by total value + var supplierBreakdown = purchaseInvoices + .Where(pid => pid.Supplier != null) + .GroupBy(pid => pid.SupplierId) + .Select(g => new + { + Id = g.Key, + Name = g.First().Supplier!.Name, + Count = g.Count(), + TotalValue = g.Sum(pid => pid.BaseAmount), + Percentage = totalValue > 0 + ? Math.Round(g.Sum(pid => pid.BaseAmount) / totalValue * 100, 2) + : 0 + }) + .OrderByDescending(pid => pid.TotalValue) + .FirstOrDefault(); + + var response = new + { + TotalInvoices = totalInvoices, + TotalValue = Math.Round(totalValue, 2), + AverageValue = Math.Round(averageValue, 2), + StatusBreakdown = statusBreakdown, + ProjectBreakdown = projectBreakdown, + TopSupplier = supplierBreakdown + }; + + _logger.LogInfo("GetPurchaseInvoiceOverview completed successfully. TenantId: {TenantId}, TotalInvoices: {TotalInvoices}, TotalValue: {TotalValue}, CorrelationId: {CorrelationId}", + tenantId, totalInvoices, totalValue, correlationId); + + return Ok(ApiResponse.SuccessResponse(response, "Purchase invoice overview retrieved successfully.", 200)); + } + catch (Exception ex) + { + // Capture complete context for diagnostics, but ensure no sensitive data is logged. + _logger.LogError(ex, "Error occurred while processing GetPurchaseInvoiceOverview. TenantId: {TenantId}, CorrelationId: {CorrelationId}", + tenantId, correlationId); + + // Do not expose internal details to clients. Return a generic 500 response. + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing the purchase invoice overview.", 500)); + } + } + /// /// Gets infrastructure projects for a tenant as a lightweight view model. ///