Added an API to get purchase invoice overview

This commit is contained in:
ashutosh.nehete 2025-12-05 15:40:26 +05:30
parent fdb08fae89
commit 6bcc67bb63

View File

@ -1225,7 +1225,11 @@ namespace Marco.Pms.Services.Controllers
{ {
// Correlation ID pattern for distributed tracing (if you use one) // Correlation ID pattern for distributed tracing (if you use one)
var correlationId = HttpContext.TraceIdentifier; var correlationId = HttpContext.TraceIdentifier;
if (tenantId == Guid.Empty)
{
_logger.LogWarning("Invalid request: TenantId is empty on progression endpoint");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400));
}
_logger.LogInfo("Started GetCollectionOverviewAsync. CorrelationId: {CorrelationId}, ProjectId: {ProjectId}", correlationId, projectId ?? Guid.Empty); _logger.LogInfo("Started GetCollectionOverviewAsync. CorrelationId: {CorrelationId}, ProjectId: {ProjectId}", correlationId, projectId ?? Guid.Empty);
try try
@ -1455,6 +1459,156 @@ namespace Marco.Pms.Services.Controllers
} }
} }
[HttpGet("purchase-invoice-overview")]
public async Task<IActionResult> 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<object>.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<object>(),
ProjectBreakdown = Array.Empty<object>(),
TopSupplier = (object?)null
};
return Ok(ApiResponse<object>.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<object>.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<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing the purchase invoice overview.", 500));
}
}
/// <summary> /// <summary>
/// Gets infrastructure projects for a tenant as a lightweight view model. /// Gets infrastructure projects for a tenant as a lightweight view model.
/// </summary> /// </summary>