diff --git a/backend/API/Controllers/PrintJobsController.cs b/backend/API/Controllers/PrintJobsController.cs index 226430e..5cb802e 100644 --- a/backend/API/Controllers/PrintJobsController.cs +++ b/backend/API/Controllers/PrintJobsController.cs @@ -413,6 +413,92 @@ public class PrintJobsController : ControllerBase return NoContent(); } + // ── GET /api/printjobs/{id}/cost-summary ────────────────────────── + + /// + /// Gets the material cost summary for a specific print job. + /// Calculates total material cost from filament usage (grams derived) + /// and the spool's purchase price. Returns warnings instead of errors + /// when cost data is unavailable. + /// + /// The unique identifier of the print job. + /// A cost summary with breakdown and any warnings about missing data. + /// Returns the cost summary. Warnings field lists any missing data. + /// If the print job with the given ID is not found. + [HttpGet("{id:guid}/cost-summary")] + [ProducesResponseType(typeof(CostSummaryResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetCostSummary(Guid id) + { + _logger.LogDebug("Getting cost summary for print job {Id}", id); + + var job = await _dbContext.PrintJobs + .Include(j => j.Spool) + .ThenInclude(s => s!.MaterialBase) + .FirstOrDefaultAsync(j => j.Id == id); + + if (job is null) + { + _logger.LogWarning("Print job {Id} not found for cost summary", id); + return NotFound(new { error = $"Print job with ID '{id}' not found." }); + } + + var warnings = new List(); + var spool = job.Spool; + + // Build response with what we have + var response = new CostSummaryResponse + { + PrintJobId = job.Id, + PrintName = job.PrintName, + SpoolId = job.SpoolId, + SpoolSerial = spool?.SpoolSerial ?? string.Empty, + SpoolBrand = spool?.Brand ?? string.Empty, + SpoolColorName = spool?.ColorName ?? string.Empty, + MmExtruded = job.MmExtruded, + GramsDerived = job.GramsDerived, + SpoolPurchasePrice = spool?.PurchasePrice, + SpoolWeightTotalGrams = spool?.WeightTotalGrams, + StoredCostPerPrint = job.CostPerPrint + }; + + // Validate spool data availability + if (spool is null) + { + warnings.Add("Spool data is not available for this print job. Cost cannot be calculated."); + response.Warnings = warnings; + return Ok(response); + } + + // Check if we can calculate cost + if (!spool.PurchasePrice.HasValue) + { + warnings.Add("Spool purchase price is not set. Cost per gram and total material cost cannot be calculated."); + } + + if (spool.WeightTotalGrams <= 0) + { + warnings.Add("Spool total weight is zero or invalid. Cost per gram and total material cost cannot be calculated."); + } + + // If we have enough data, calculate the cost + if (spool.PurchasePrice.HasValue && spool.WeightTotalGrams > 0) + { + var pricePerGram = spool.PurchasePrice.Value / spool.WeightTotalGrams; + response.PricePerGram = Math.Round(pricePerGram, 4); + response.TotalMaterialCost = Math.Round(job.GramsDerived * pricePerGram, 4); + } + + // Warn if grams derived is zero but mm extruded is non-zero + if (job.GramsDerived == 0 && job.MmExtruded > 0) + { + warnings.Add("GramsDerived is zero despite MmExtruded being non-zero. Cost may be inaccurate. Consider re-deriving grams from filament parameters."); + } + + response.Warnings = warnings; + return Ok(response); + } + // ── Gram Derivation Formula ──────────────────────────────────── /// diff --git a/backend/API/DTOs/PrintJobs/CostSummaryResponse.cs b/backend/API/DTOs/PrintJobs/CostSummaryResponse.cs new file mode 100644 index 0000000..0c2f9ed --- /dev/null +++ b/backend/API/DTOs/PrintJobs/CostSummaryResponse.cs @@ -0,0 +1,55 @@ +namespace Extrudex.API.DTOs.PrintJobs; + +/// +/// Response DTO for the cost summary of a print job. +/// Provides a breakdown of material cost based on filament usage +/// and spool pricing data. If cost data is incomplete, warnings +/// are returned instead of throwing an error. +/// +public class CostSummaryResponse +{ + /// Unique identifier of the print job. + public Guid PrintJobId { get; set; } + + /// Human-readable name of the print job. + public string PrintName { get; set; } = string.Empty; + + /// Foreign key to the spool used for this print job. + public Guid SpoolId { get; set; } + + /// Serial number of the spool. + public string SpoolSerial { get; set; } = string.Empty; + + /// Brand of the spool. + public string SpoolBrand { get; set; } = string.Empty; + + /// Color name of the spool. + public string SpoolColorName { get; set; } = string.Empty; + + /// Total millimeters of filament extruded during this print. + public decimal MmExtruded { get; set; } + + /// Derived grams consumed for this print job. + public decimal GramsDerived { get; set; } + + /// Purchase price of the full spool, if available. + public decimal? SpoolPurchasePrice { get; set; } + + /// Total weight of the spool in grams when full. + public decimal? SpoolWeightTotalGrams { get; set; } + + /// Calculated price per gram (purchase price / total weight), if available. + public decimal? PricePerGram { get; set; } + + /// Calculated total material cost for this print job, if available. + public decimal? TotalMaterialCost { get; set; } + + /// The CostPerPrint stored on the print job entity, if set. + public decimal? StoredCostPerPrint { get; set; } + + /// + /// Warnings about missing data that prevent cost calculation. + /// Empty if all data is available and cost was calculated successfully. + /// + public List Warnings { get; set; } = new(); +} \ No newline at end of file