using Extrudex.API.DTOs.PrintJobs;
using Extrudex.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace Extrudex.API.Controllers;
///
/// Controller for cost analysis endpoints. Provides spool-level
/// cost breakdowns and aggregated COGS reporting.
///
[ApiController]
[Route("api/cost-analysis")]
public class CostAnalysisController : ControllerBase
{
private readonly ICostPerPrintService _costService;
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
/// The cost-per-print calculation service.
/// The logger for diagnostic output.
public CostAnalysisController(
ICostPerPrintService costService,
ILogger logger)
{
_costService = costService;
_logger = logger;
}
// ── POST /api/cost-analysis/spool ────────────────────────────
///
/// 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.
///
/// The request containing the spool identifier.
/// A spool-level cost summary with per-job breakdowns.
/// Returns the spool cost breakdown with per-job details.
/// If the spool has no print jobs.
[HttpPost("spool")]
[ProducesResponseType(typeof(SpoolCostResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task> 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();
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);
}
///
/// Maps a domain CostPerPrintResult to an API CostPerPrintResponse DTO.
///
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
};
}