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 }; }