using Extrudex.Domain.Interfaces; using Extrudex.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Extrudex.Infrastructure.Services; /// /// Calculates the cost of goods sold (COGS) per print job using the spool's /// purchase price and the print job's derived grams consumed. /// /// Formula: /// cost_per_gram = purchase_price / weight_total_grams /// cost_per_print = grams_derived × cost_per_gram /// /// Handles missing data gracefully — if the spool has no purchase price or /// weight recorded, the result includes warnings and null cost fields /// instead of throwing exceptions. /// public class CostPerPrintService : ICostPerPrintService { private readonly ExtrudexDbContext _dbContext; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The database context for data access. /// The logger for diagnostic output. public CostPerPrintService(ExtrudexDbContext dbContext, ILogger logger) { _dbContext = dbContext; _logger = logger; } /// public async Task CalculateAsync(Guid printJobId, CancellationToken cancellationToken = default) { _logger.LogDebug("Calculating cost per print for job {PrintJobId}", printJobId); var job = await _dbContext.PrintJobs .Include(j => j.Spool) .ThenInclude(s => s!.MaterialBase) .FirstOrDefaultAsync(j => j.Id == printJobId, cancellationToken); if (job is null) { _logger.LogWarning("Print job {PrintJobId} not found for cost calculation", printJobId); return new CostPerPrintResult { PrintJobId = printJobId, Warnings = new List { $"Print job with ID '{printJobId}' not found." } }; } return BuildResult(job); } /// public async Task> CalculateBySpoolAsync( Guid spoolId, CancellationToken cancellationToken = default) { _logger.LogDebug("Calculating cost per print for all jobs on spool {SpoolId}", spoolId); var jobs = await _dbContext.PrintJobs .Include(j => j.Spool) .ThenInclude(s => s!.MaterialBase) .Where(j => j.SpoolId == spoolId) .OrderByDescending(j => j.CreatedAt) .ToListAsync(cancellationToken); if (jobs.Count == 0) { _logger.LogDebug("No print jobs found for spool {SpoolId}", spoolId); return Array.Empty(); } return jobs.Select(BuildResult).ToList(); } /// /// Builds a from a print job entity. /// Computes cost_per_gram and cost_per_print when all required data is available. /// Populates warnings when data is missing or incomplete. /// /// The print job entity with Spool navigation loaded. /// A cost calculation result with breakdown and any warnings. private CostPerPrintResult BuildResult(Domain.Entities.PrintJob job) { var warnings = new List(); var spool = job.Spool; // Map what we always have var result = new CostPerPrintResult { PrintJobId = job.Id, PrintName = job.PrintName, SpoolId = job.SpoolId, SpoolSerial = spool?.SpoolSerial ?? string.Empty, MmExtruded = job.MmExtruded, GramsDerived = job.GramsDerived, }; // Guard: spool must be loaded if (spool is null) { warnings.Add("Spool data is not available for this print job."); result.Warnings = warnings; return result; } // Capture purchase price result.PurchasePrice = spool.PurchasePrice; result.WeightTotalGrams = spool.WeightTotalGrams; // Check for missing purchase price if (!spool.PurchasePrice.HasValue) { warnings.Add( "Spool purchase price is not recorded. Cost calculation requires a purchase price on the spool."); } // Check for zero or negative weight — prevents division by zero if (spool.WeightTotalGrams <= 0) { warnings.Add( "Spool total weight is zero or not recorded. Cost calculation requires a positive weight_total_grams on the spool."); } // Check for zero grams derived if (job.GramsDerived <= 0) { warnings.Add( "Derived grams consumed is zero. Ensure mm_extruded, filament diameter, and material density are recorded for this print job."); } // If all data is present and valid, compute the cost if (spool.PurchasePrice.HasValue && spool.WeightTotalGrams > 0 && job.GramsDerived > 0) { var costPerGram = spool.PurchasePrice.Value / spool.WeightTotalGrams; result.CostPerGram = Math.Round(costPerGram, 6); result.CostPerPrint = Math.Round(job.GramsDerived * costPerGram, 4); _logger.LogDebug( "Cost calculated for job {PrintJobId}: {GramsDerived}g × {CostPerGram:C}/g = {CostPerPrint:C}", job.Id, job.GramsDerived, result.CostPerGram, result.CostPerPrint); } else { _logger.LogDebug( "Cost calculation incomplete for job {PrintJobId}: missing data (warnings: {WarningCount})", job.Id, warnings.Count); } result.Warnings = warnings; return result; } }