Merge remote-tracking branch 'origin/dev' into fix-pr-18
# Conflicts: # backend/API/Controllers/PrintJobsController.cs
This commit is contained in:
@@ -2,7 +2,6 @@ using Extrudex.API.DTOs;
|
||||
using Extrudex.API.DTOs.PrintJobs;
|
||||
using Extrudex.Domain.Entities;
|
||||
using Extrudex.Domain.Enums;
|
||||
using Extrudex.Domain.Interfaces;
|
||||
using Extrudex.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -20,22 +19,16 @@ namespace Extrudex.API.Controllers;
|
||||
public class PrintJobsController : ControllerBase
|
||||
{
|
||||
private readonly ExtrudexDbContext _dbContext;
|
||||
private readonly ICostPerPrintService _costService;
|
||||
private readonly ILogger<PrintJobsController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PrintJobsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context for data access.</param>
|
||||
/// <param name="costService">The cost-per-print calculation service.</param>
|
||||
/// <param name="logger">The logger for diagnostic output.</param>
|
||||
public PrintJobsController(
|
||||
ExtrudexDbContext dbContext,
|
||||
ICostPerPrintService costService,
|
||||
ILogger<PrintJobsController> logger)
|
||||
public PrintJobsController(ExtrudexDbContext dbContext, ILogger<PrintJobsController> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_costService = costService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -420,32 +413,90 @@ public class PrintJobsController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// ── POST /api/printjobs/{id}/cost ─────────────────────────────
|
||||
// ── GET /api/printjobs/{id}/cost-summary ──────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the cost of goods sold (COGS) for a specific print job.
|
||||
/// Uses the spool’s purchase price and the print job’s derived grams consumed
|
||||
/// to produce a cost breakdown. Returns warnings instead of errors when
|
||||
/// cost data is missing or incomplete.
|
||||
/// 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 breakdown with warnings if data is incomplete.</returns>
|
||||
/// <response code="200">Returns the cost breakdown for the print job.</response>
|
||||
/// <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>
|
||||
[HttpPost("{id:guid}/cost")]
|
||||
[ProducesResponseType(typeof(CostPerPrintResponse), StatusCodes.Status200OK)]
|
||||
[HttpGet("{id:guid}/cost-summary")]
|
||||
[ProducesResponseType(typeof(CostSummaryResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CostPerPrintResponse>> CalculateCost(Guid id)
|
||||
public async Task<ActionResult<CostSummaryResponse>> GetCostSummary(Guid id)
|
||||
{
|
||||
_logger.LogDebug("Calculating cost for print job {Id}", id);
|
||||
_logger.LogDebug("Getting cost summary for print job {Id}", id);
|
||||
|
||||
var result = await _costService.CalculateAsync(id);
|
||||
var job = await _dbContext.PrintJobs
|
||||
.Include(j => j.Spool)
|
||||
.ThenInclude(s => s!.MaterialBase)
|
||||
.FirstOrDefaultAsync(j => j.Id == id);
|
||||
|
||||
// If the job was not found, return 404
|
||||
if (result.Warnings.Any(w => w.Contains("not found")))
|
||||
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." });
|
||||
}
|
||||
|
||||
return Ok(MapCostToResponse(result));
|
||||
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 ────────────────────────────────────
|
||||
@@ -544,22 +595,4 @@ public class PrintJobsController : ControllerBase
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps a domain CostPerPrintResult to an API CostPerPrintResponse DTO.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user