using Extrudex.Domain.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Extrudex.Infrastructure.Services; /// /// Detects low-stock filament spools by comparing the remaining weight percentage /// against a configurable threshold. The threshold can be set via: /// 1. EXTRUDEX_LOW_STOCK_THRESHOLD env var (highest priority, e.g. "25") /// 2. FilamentAlerts:LowStockThresholdPercent in appsettings.json /// 3. Default: 20% (a standard spool is "low" when ≤20% remains) /// public class LowStockDetector : ILowStockDetector { private readonly ILogger _logger; /// /// The percentage threshold below which a spool is considered low stock. /// For example, 20 means a spool is "low" when ≤20% of its filament remains. /// public decimal LowStockThresholdPercent { get; } /// /// Initializes a new instance of the class. /// Reads the low-stock threshold from configuration with env var override support. /// /// Application configuration for threshold settings. /// Logger for diagnostic output. public LowStockDetector(IConfiguration configuration, ILogger logger) { _logger = logger; // Priority: env var > appsettings > default (20%) var envThreshold = Environment.GetEnvironmentVariable("EXTRUDEX_LOW_STOCK_THRESHOLD"); var configThreshold = configuration.GetValue("FilamentAlerts:LowStockThresholdPercent"); if (!string.IsNullOrEmpty(envThreshold) && decimal.TryParse(envThreshold, out var parsedEnv)) { LowStockThresholdPercent = Math.Clamp(parsedEnv, 0m, 100m); _logger.LogInformation( "Low-stock threshold set from env var EXTRUDEX_LOW_STOCK_THRESHOLD: {Threshold}%", LowStockThresholdPercent); } else if (configThreshold.HasValue) { LowStockThresholdPercent = Math.Clamp(configThreshold.Value, 0m, 100m); _logger.LogInformation( "Low-stock threshold set from config FilamentAlerts:LowStockThresholdPercent: {Threshold}%", LowStockThresholdPercent); } else { LowStockThresholdPercent = 20m; _logger.LogInformation( "Low-stock threshold using default: {Threshold}%", LowStockThresholdPercent); } } /// public bool IsLowStock(decimal weightRemainingGrams, decimal weightTotalGrams) { if (weightTotalGrams <= 0m) { _logger.LogDebug( "Spool with total weight {Total}g cannot be evaluated for low stock — treating as not low", weightTotalGrams); return false; } var remainingPercent = GetRemainingWeightPercent(weightRemainingGrams, weightTotalGrams); var isLow = remainingPercent <= LowStockThresholdPercent; if (isLow) { _logger.LogDebug( "Spool is LOW STOCK: {Remaining}g / {Total}g = {Percent:F1}% (threshold: {Threshold}%)", weightRemainingGrams, weightTotalGrams, remainingPercent, LowStockThresholdPercent); } return isLow; } /// public decimal GetRemainingWeightPercent(decimal weightRemainingGrams, decimal weightTotalGrams) { if (weightTotalGrams <= 0m) return 0m; return Math.Round( (weightRemainingGrams / weightTotalGrams) * 100m, 1, MidpointRounding.AwayFromZero); } }