Merge remote-tracking branch 'origin/dev' into agent/dex/CUB-32-usage-logging-service
Some checks failed
Dev Build / build-test (pull_request) Failing after 55s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 4s

# Conflicts:
#	backend/Infrastructure/Data/ExtrudexDbContext.cs
#	backend/Infrastructure/Data/Migrations/ExtrudexDbContextModelSnapshot.cs
This commit is contained in:
2026-04-27 14:29:13 -04:00
51 changed files with 13115 additions and 246 deletions

View File

@@ -413,6 +413,92 @@ public class PrintJobsController : ControllerBase
return NoContent();
}
// ── GET /api/printjobs/{id}/cost-summary ──────────────────────────
/// <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.
/// </summary>
/// <param name="id">The unique identifier of the print job.</param>
/// <returns>A cost summary with breakdown and any warnings about missing data.</returns>
/// <response code="200">Returns the cost summary. Warnings field lists any missing data.</response>
/// <response code="404">If the print job with the given ID is not found.</response>
[HttpGet("{id:guid}/cost-summary")]
[ProducesResponseType(typeof(CostSummaryResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CostSummaryResponse>> 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<string>();
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 ────────────────────────────────────
/// <summary>