Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,149 @@
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
namespace PowderCoating.Application.Services;
/// <summary>
/// Derives sqft/hr throughput rates from a shop's equipment configuration.
/// Used in two places: the AI photo quote prompt (so Claude reasons from real shop
/// speeds) and the calculated-item wizard (to show a suggested blast time hint).
///
/// Formula:
/// BlastRate = BaseByCfm(cfm) × NozzleMultiplier × SetupMultiplier × SubstrateMultiplier
///
/// Base rates by CFM represent a pressure pot at #5 nozzle removing paint.
/// All multipliers are relative to that baseline.
/// </summary>
public static class ShopCapabilityCalculator
{
// ── Blast rate derivation ─────────────────────────────────────────────────
/// <summary>
/// Returns the effective blast rate in sqft/hr.
/// If <see cref="CompanyOperatingCosts.BlastRateSqFtPerHourOverride"/> is set, returns it directly.
/// Otherwise derives from CFM, nozzle, setup type, and substrate.
/// Returns 0 when CFM is not configured (shop hasn't calibrated yet).
/// </summary>
public static decimal GetBlastRateSqFtPerHour(CompanyOperatingCosts costs)
{
if (costs.BlastRateSqFtPerHourOverride.HasValue && costs.BlastRateSqFtPerHourOverride.Value > 0)
return costs.BlastRateSqFtPerHourOverride.Value;
if (costs.CompressorCfm <= 0)
return 0m;
var baseRate = BaseByCfm(costs.CompressorCfm);
var nozzle = NozzleMultiplier(costs.BlastNozzleSize);
var setup = SetupMultiplier(costs.BlastSetupType);
var substrate = SubstrateMultiplier(costs.PrimaryBlastSubstrate);
return Math.Round(baseRate * nozzle * setup * substrate, 1);
}
/// <summary>
/// Returns the effective blast rate in sqft/hr for a named <see cref="CompanyBlastSetup"/>.
/// Identical logic to the <see cref="CompanyOperatingCosts"/> overload — uses override if set,
/// otherwise derives from the setup's equipment specs.
/// </summary>
public static decimal GetBlastRateSqFtPerHour(CompanyBlastSetup setup)
{
if (setup.BlastRateSqFtPerHourOverride.HasValue && setup.BlastRateSqFtPerHourOverride.Value > 0)
return setup.BlastRateSqFtPerHourOverride.Value;
if (setup.CompressorCfm <= 0)
return 0m;
var baseRate = BaseByCfm(setup.CompressorCfm);
var nozzle = NozzleMultiplier(setup.BlastNozzleSize);
var setupMult = SetupMultiplier(setup.SetupType);
var substrate = SubstrateMultiplier(setup.PrimarySubstrate);
return Math.Round(baseRate * nozzle * setupMult * substrate, 1);
}
/// <summary>
/// Returns the effective coating application rate in sqft/hr.
/// If override is set, returns it directly.
/// Otherwise derives a sensible default from gun type.
/// </summary>
public static decimal GetCoatingRateSqFtPerHour(CompanyOperatingCosts costs)
{
if (costs.CoatingRateSqFtPerHourOverride.HasValue && costs.CoatingRateSqFtPerHourOverride.Value > 0)
return costs.CoatingRateSqFtPerHourOverride.Value;
// Corona and tribo guns are roughly similar on flat parts; tribo edges out on complex geometry.
// Without more equipment data (voltage, gun model) we use a single reasonable default.
return costs.CoatingGunType switch
{
CoatingGunType.Corona => 40m,
CoatingGunType.Tribo => 35m, // slower on flat but better on complex; conservative default
CoatingGunType.Both => 40m,
_ => 40m
};
}
/// <summary>
/// Returns default equipment field values for a given capability tier.
/// Applied during Setup Wizard tier selection so the shop gets reasonable
/// starting values even if they never visit the Quoting Calibration tab.
/// </summary>
public static (BlastSetupType SetupType, decimal Cfm, int NozzleSize, BlastSubstrateType Substrate)
TierDefaults(ShopCapabilityTier tier) => tier switch
{
ShopCapabilityTier.Garage => (BlastSetupType.SiphonCabinet, 7m, 4, BlastSubstrateType.Mixed),
ShopCapabilityTier.Small => (BlastSetupType.PressurePot, 40m, 5, BlastSubstrateType.Mixed),
ShopCapabilityTier.Medium => (BlastSetupType.PressurePot, 80m, 5, BlastSubstrateType.Mixed),
ShopCapabilityTier.Large => (BlastSetupType.PressurePot, 150m, 6, BlastSubstrateType.Mixed),
_ => (BlastSetupType.SiphonCabinet, 7m, 4, BlastSubstrateType.Mixed)
};
// ── Private helpers ───────────────────────────────────────────────────────
/// <summary>
/// Base sqft/hr at a pressure pot, #5 nozzle, removing paint.
/// Calibrated so that real-world examples produce expected results:
/// - 7 CFM siphon cabinet → ~2 sqft/hr (garage coater, 3+ hrs/wheel)
/// - 40 CFM pressure pot → ~15 sqft/hr (small shop, ~30 min/wheel)
/// - 80 CFM pressure pot → ~25 sqft/hr (medium shop)
/// - 150 CFM pressure pot → ~40 sqft/hr (large shop, ~10 min/wheel)
/// </summary>
private static decimal BaseByCfm(decimal cfm) => cfm switch
{
< 10 => 5m,
< 20 => 9m,
< 40 => 15m,
< 80 => 25m,
< 120 => 35m,
_ => 45m
};
private static decimal NozzleMultiplier(int nozzle) => nozzle switch
{
2 => 0.35m,
3 => 0.55m,
4 => 0.75m,
5 => 1.00m,
6 => 1.30m,
7 => 1.65m,
8 => 2.00m,
_ => 1.00m
};
private static decimal SetupMultiplier(BlastSetupType setup) => setup switch
{
BlastSetupType.SiphonCabinet => 0.50m, // enclosed, low pressure, repositioning time
BlastSetupType.SiphonPot => 0.70m,
BlastSetupType.PressurePot => 1.00m, // baseline
BlastSetupType.WetBlasting => 0.60m,
_ => 1.00m
};
private static decimal SubstrateMultiplier(BlastSubstrateType substrate) => substrate switch
{
BlastSubstrateType.PowderCoat => 1.25m, // faster to remove than paint
BlastSubstrateType.Paint => 1.00m, // baseline
BlastSubstrateType.Mixed => 0.90m,
BlastSubstrateType.RustAndScale => 0.70m, // requires more passes
_ => 0.90m
};
}