Added an API to get purchase invoice overview
This commit is contained in:
parent
fdb08fae89
commit
6bcc67bb63
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user