CUB-37: implement cost-per-print calculation service
Some checks failed
Dev Build / build-test (pull_request) Failing after 48s
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 3s

This commit is contained in:
2026-04-27 17:57:57 +00:00
parent 8168d25bdf
commit 6aa31f4be3
6 changed files with 498 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ 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;
@@ -19,16 +20,22 @@ 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, ILogger<PrintJobsController> logger)
public PrintJobsController(
ExtrudexDbContext dbContext,
ICostPerPrintService costService,
ILogger<PrintJobsController> logger)
{
_dbContext = dbContext;
_costService = costService;
_logger = logger;
}
@@ -413,6 +420,34 @@ public class PrintJobsController : ControllerBase
return NoContent();
}
// ── POST /api/printjobs/{id}/cost ─────────────────────────────
/// <summary>
/// Calculates the cost of goods sold (COGS) for a specific print job.
/// Uses the spools purchase price and the print jobs derived grams consumed
/// to produce a cost breakdown. Returns warnings instead of errors when
/// cost data is missing or incomplete.
/// </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>
/// <response code="404">If the print job with the given ID is not found.</response>
[HttpPost("{id:guid}/cost")]
[ProducesResponseType(typeof(CostPerPrintResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CostPerPrintResponse>> CalculateCost(Guid id)
{
_logger.LogDebug("Calculating cost for print job {Id}", id);
var result = await _costService.CalculateAsync(id);
// If the job was not found, return 404
if (result.Warnings.Any(w => w.Contains("not found")))
return NotFound(new { error = $"Print job with ID '{id}' not found." });
return Ok(MapCostToResponse(result));
}
// ── Gram Derivation Formula ────────────────────────────────────
/// <summary>
@@ -509,4 +544,22 @@ 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
};
}