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