108 lines
4.3 KiB
C#
108 lines
4.3 KiB
C#
|
|
using Extrudex.API.DTOs.PrintJobs;
|
||
|
|
using Extrudex.Domain.Interfaces;
|
||
|
|
using Microsoft.AspNetCore.Mvc;
|
||
|
|
|
||
|
|
namespace Extrudex.API.Controllers;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Controller for cost analysis endpoints. Provides spool-level
|
||
|
|
/// cost breakdowns and aggregated COGS reporting.
|
||
|
|
/// </summary>
|
||
|
|
[ApiController]
|
||
|
|
[Route("api/cost-analysis")]
|
||
|
|
public class CostAnalysisController : ControllerBase
|
||
|
|
{
|
||
|
|
private readonly ICostPerPrintService _costService;
|
||
|
|
private readonly ILogger<CostAnalysisController> _logger;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Initializes a new instance of the <see cref="CostAnalysisController"/> class.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="costService">The cost-per-print calculation service.</param>
|
||
|
|
/// <param name="logger">The logger for diagnostic output.</param>
|
||
|
|
public CostAnalysisController(
|
||
|
|
ICostPerPrintService costService,
|
||
|
|
ILogger<CostAnalysisController> logger)
|
||
|
|
{
|
||
|
|
_costService = costService;
|
||
|
|
_logger = logger;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── POST /api/cost-analysis/spool ────────────────────────────
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Calculates cost breakdowns for all print jobs associated with a specific spool.
|
||
|
|
/// Returns per-job costs plus an aggregated total. Jobs with missing cost data
|
||
|
|
/// include warnings and null cost fields — the endpoint never throws for missing data.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="request">The request containing the spool identifier.</param>
|
||
|
|
/// <returns>A spool-level cost summary with per-job breakdowns.</returns>
|
||
|
|
/// <response code="200">Returns the spool cost breakdown with per-job details.</response>
|
||
|
|
/// <response code="404">If the spool has no print jobs.</response>
|
||
|
|
[HttpPost("spool")]
|
||
|
|
[ProducesResponseType(typeof(SpoolCostResponse), StatusCodes.Status200OK)]
|
||
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||
|
|
public async Task<ActionResult<SpoolCostResponse>> CalculateSpoolCost([FromBody] SpoolCostRequest request)
|
||
|
|
{
|
||
|
|
_logger.LogDebug("Calculating cost breakdown for spool {SpoolId}", request.SpoolId);
|
||
|
|
|
||
|
|
var results = await _costService.CalculateBySpoolAsync(request.SpoolId);
|
||
|
|
|
||
|
|
if (results.Count == 0)
|
||
|
|
{
|
||
|
|
return NotFound(new { error = $"No print jobs found for spool with ID '{request.SpoolId}'." });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build the spool-level summary
|
||
|
|
var firstResult = results[0];
|
||
|
|
var jobResponses = results.Select(MapCostToResponse).ToList();
|
||
|
|
|
||
|
|
// Aggregate total cost and grams — only include jobs that have a valid cost
|
||
|
|
var calculableJobs = results.Where(r => r.CostPerPrint.HasValue).ToList();
|
||
|
|
var totalCost = calculableJobs.Count == results.Count
|
||
|
|
? Math.Round(calculableJobs.Sum(r => r.CostPerPrint!.Value), 4)
|
||
|
|
: (decimal?)null;
|
||
|
|
|
||
|
|
var aggregateWarnings = new List<string>();
|
||
|
|
if (calculableJobs.Count < results.Count)
|
||
|
|
{
|
||
|
|
aggregateWarnings.Add(
|
||
|
|
$"{results.Count - calculableJobs.Count} of {results.Count} print jobs have missing cost data. " +
|
||
|
|
"Total cost reflects only jobs with complete data.");
|
||
|
|
}
|
||
|
|
|
||
|
|
var response = new SpoolCostResponse
|
||
|
|
{
|
||
|
|
SpoolId = request.SpoolId,
|
||
|
|
SpoolSerial = firstResult.SpoolSerial,
|
||
|
|
PurchasePrice = firstResult.PurchasePrice,
|
||
|
|
WeightTotalGrams = firstResult.WeightTotalGrams,
|
||
|
|
CostPerGram = firstResult.CostPerGram,
|
||
|
|
TotalGramsConsumed = results.Sum(r => r.GramsDerived),
|
||
|
|
TotalCost = totalCost,
|
||
|
|
JobCount = results.Count,
|
||
|
|
Jobs = jobResponses,
|
||
|
|
Warnings = aggregateWarnings
|
||
|
|
};
|
||
|
|
|
||
|
|
return Ok(response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Maps a domain CostPerPrintResult to an API CostPerPrintResponse DTO.
|
||
|
|
/// </summary>
|
||
|
|
private static CostPerPrintResponse MapCostToResponse(CostPerPrintResult r) => new()
|
||
|
|
{
|
||
|
|
PrintJobId = r.PrintJobId,
|
||
|
|
PrintName = r.PrintName,
|
||
|
|
SpoolId = r.SpoolId,
|
||
|
|
SpoolSerial = r.SpoolSerial,
|
||
|
|
MmExtruded = r.MmExtruded,
|
||
|
|
GramsDerived = r.GramsDerived,
|
||
|
|
PurchasePrice = r.PurchasePrice,
|
||
|
|
WeightTotalGrams = r.WeightTotalGrams,
|
||
|
|
CostPerGram = r.CostPerGram,
|
||
|
|
CostPerPrint = r.CostPerPrint,
|
||
|
|
Warnings = r.Warnings
|
||
|
|
};
|
||
|
|
}
|