using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; namespace PowderCoating.Application.Services; /// /// Derives sqft/hr throughput rates from a shop's equipment configuration. /// Used by the AI photo quote prompt (so Claude reasons from real shop speeds) /// and the Company Settings live preview (so the UI always shows the same rate /// the AI will use — single formula path, no client-side duplication). /// /// Both pressure pots and siphon cabinets are nozzle-primary: nozzle size /// determines throughput and CFM draw. CFM is not used in the rate formula. /// /// Sources: /// Pressure pot rates — averaged from two industry standard abrasive blast /// cleaning reference tables. /// Siphon cabinet rates — industry reference table for siphon-fed cabinets. /// Substrate multipliers — relative removal difficulty vs. paint baseline. /// public static class ShopCapabilityCalculator { // ── Public entry points ──────────────────────────────────────────────────── /// /// Returns the effective blast rate in sqft/hr for company-level operating costs. /// BlastRateSqFtPerHourOverride bypasses the formula when set. /// public static decimal GetBlastRateSqFtPerHour(CompanyOperatingCosts costs) { if (costs.BlastRateSqFtPerHourOverride.HasValue && costs.BlastRateSqFtPerHourOverride.Value > 0) return costs.BlastRateSqFtPerHourOverride.Value; return CalculateBlastRate(costs.BlastNozzleSize, costs.BlastSetupType, costs.PrimaryBlastSubstrate); } /// /// Returns the effective blast rate in sqft/hr for a named blast setup. /// BlastRateSqFtPerHourOverride bypasses the formula when set. /// public static decimal GetBlastRateSqFtPerHour(CompanyBlastSetup setup) { if (setup.BlastRateSqFtPerHourOverride.HasValue && setup.BlastRateSqFtPerHourOverride.Value > 0) return setup.BlastRateSqFtPerHourOverride.Value; return CalculateBlastRate(setup.BlastNozzleSize, setup.SetupType, setup.PrimarySubstrate); } /// /// Returns the effective coating application rate in sqft/hr. /// Override bypasses the formula when set. /// public static decimal GetCoatingRateSqFtPerHour(CompanyOperatingCosts costs) { if (costs.CoatingRateSqFtPerHourOverride.HasValue && costs.CoatingRateSqFtPerHourOverride.Value > 0) return costs.CoatingRateSqFtPerHourOverride.Value; return costs.CoatingGunType switch { CoatingGunType.Corona => 40m, CoatingGunType.Tribo => 35m, CoatingGunType.Both => 40m, _ => 40m }; } /// /// Returns default equipment field values for a given capability tier, applied /// during Setup Wizard tier selection so new shops get reasonable starting values. /// CFM defaults reflect typical compressor sizes for each tier; they appear in the /// UI for reference but are not used in the rate formula. /// public static (BlastSetupType SetupType, decimal Cfm, int NozzleSize, BlastSubstrateType Substrate) TierDefaults(ShopCapabilityTier tier) => tier switch { ShopCapabilityTier.Garage => (BlastSetupType.SiphonCabinet, 7m, 3, BlastSubstrateType.Mixed), ShopCapabilityTier.Small => (BlastSetupType.PressurePot, 49m, 3, BlastSubstrateType.Mixed), ShopCapabilityTier.Medium => (BlastSetupType.PressurePot, 90m, 4, BlastSubstrateType.Mixed), ShopCapabilityTier.Large => (BlastSetupType.PressurePot, 150m, 5, BlastSubstrateType.Mixed), _ => (BlastSetupType.SiphonCabinet, 7m, 3, BlastSubstrateType.Mixed) }; // ── Core formula (single path for all callers) ───────────────────────────── /// /// Nozzle-primary blast rate calculation. Nozzle size determines throughput; /// setup type routes to the appropriate reference table; substrate adjusts for /// removal difficulty. CFM is not used — it is a consequence of nozzle choice, /// not an independent variable in throughput. /// private static decimal CalculateBlastRate(int nozzle, BlastSetupType setupType, BlastSubstrateType substrate) { var baseRate = setupType switch { BlastSetupType.PressurePot => PressurePotRateByNozzle(nozzle), BlastSetupType.SiphonCabinet => SiphonCabinetRateByNozzle(nozzle), // Siphon pot: open gravity feed, no enclosure penalty, ~80% of pressure pot BlastSetupType.SiphonPot => Math.Round(PressurePotRateByNozzle(nozzle) * 0.80m, 1), // Wet blasting: water-media mix reduces impact velocity, ~60% of dry pressure pot BlastSetupType.WetBlasting => Math.Round(PressurePotRateByNozzle(nozzle) * 0.60m, 1), _ => 0m }; return Math.Round(baseRate * SubstrateMultiplier(substrate), 1); } /// /// Midpoint cleaning rates for a pressure pot at adequate air supply, by nozzle size. /// Averaged from two industry-standard abrasive blast cleaning reference tables. /// #1 (1/16"): 20-35 sqft/hr avg → 20 /// #2 (1/8"): 40-60 sqft/hr avg → 40 /// #3 (3/16"): 60-85 sqft/hr avg → 75 /// #4 (1/4"): 90-110 sqft/hr avg → 115 /// #5 (5/16"): 130-160 sqft/hr avg → 175 /// #6 (3/8"): 180-230 sqft/hr avg → 245 /// #7 (7/16"): 240-300 sqft/hr avg → 325 /// #8 (1/2"): 320-400 sqft/hr avg → 430 /// private static decimal PressurePotRateByNozzle(int nozzle) => nozzle switch { 1 => 20m, 2 => 40m, 3 => 75m, 4 => 115m, 5 => 175m, 6 => 245m, 7 => 325m, 8 => 430m, _ => 100m }; /// /// Midpoint cleaning rates for siphon-fed blast cabinets, by nozzle size. /// Source: industry reference table for siphon cabinet production rates. /// #1 (1/16"): 10-25 sqft/hr → 18 /// #2 (1/8"): 25-50 sqft/hr → 38 /// #3 (3/16"): 50-100 sqft/hr → 75 /// #4 (1/4"): 100-150 sqft/hr → 125 /// #5 (5/16"): 150-225 sqft/hr → 188 /// #6 (3/8"): 225-300 sqft/hr → 263 /// #7 (7/16"): 300-375 sqft/hr → 338 /// #8 (1/2"): 375-450 sqft/hr → 413 /// private static decimal SiphonCabinetRateByNozzle(int nozzle) => nozzle switch { 1 => 18m, 2 => 38m, 3 => 75m, 4 => 125m, 5 => 188m, 6 => 263m, 7 => 338m, 8 => 413m, _ => 80m }; /// /// Adjustment for substrate removal difficulty relative to paint (baseline = 1.0). /// Powder coat strips faster than paint; rust and scale requires multiple passes. /// private static decimal SubstrateMultiplier(BlastSubstrateType substrate) => substrate switch { BlastSubstrateType.PowderCoat => 1.25m, BlastSubstrateType.Paint => 1.00m, BlastSubstrateType.Mixed => 0.90m, BlastSubstrateType.RustAndScale => 0.70m, _ => 0.90m }; }