Add facility overhead (rent + utilities) to operating costs and pricing engine
Adds MonthlyRent, MonthlyUtilities, and MonthlyBillableHours to CompanyOperatingCosts so fixed shop occupancy costs are recovered on every quote. The pricing engine converts these into a per-hour rate and applies it as a transparent "Facility Overhead" line between oven batch cost and shop supplies. UI added in Company Settings Operating Costs tab and Setup Wizard Step 3; migration AddFacilityOverheadFields applied. Help docs and AI knowledge base updated to cover the new fields and the revised quote pricing calculation order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,14 @@ namespace PowderCoating.Application.DTOs.Company
|
||||
public decimal DerivedBlastRateSqFtPerHour { get; set; }
|
||||
/// <summary>Derived coating rate — shown to the user as a sanity-check value.</summary>
|
||||
public decimal DerivedCoatingRateSqFtPerHour { get; set; }
|
||||
|
||||
// Facility Overhead
|
||||
public decimal MonthlyRent { get; set; }
|
||||
public decimal MonthlyUtilities { get; set; }
|
||||
public int MonthlyBillableHours { get; set; } = 160;
|
||||
|
||||
/// <summary>Derived facility overhead rate = (MonthlyRent + MonthlyUtilities) / MonthlyBillableHours.</summary>
|
||||
public decimal FacilityOverheadRatePerHour { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -228,6 +236,19 @@ namespace PowderCoating.Application.DTOs.Company
|
||||
|
||||
[Range(0, 500)]
|
||||
public decimal ComplexityExtremePercent { get; set; } = 25m;
|
||||
|
||||
// Facility Overhead
|
||||
[Range(0, 1000000, ErrorMessage = "Monthly rent must be between 0 and 1,000,000")]
|
||||
[Display(Name = "Monthly Rent ($)")]
|
||||
public decimal MonthlyRent { get; set; } = 0m;
|
||||
|
||||
[Range(0, 1000000, ErrorMessage = "Monthly utilities must be between 0 and 1,000,000")]
|
||||
[Display(Name = "Monthly Utilities ($)")]
|
||||
public decimal MonthlyUtilities { get; set; } = 0m;
|
||||
|
||||
[Range(1, 10000, ErrorMessage = "Billable hours must be between 1 and 10,000")]
|
||||
[Display(Name = "Billable Hours/Month")]
|
||||
public int MonthlyBillableHours { get; set; } = 160;
|
||||
}
|
||||
|
||||
/// <summary>DTO for updating the company AI profile text used for AI Photo Quote calibration.</summary>
|
||||
|
||||
@@ -606,6 +606,9 @@ public class QuotePricingBreakdownDto
|
||||
public int OvenBatches { get; set; }
|
||||
public int OvenCycleMinutes { get; set; }
|
||||
|
||||
public decimal FacilityOverheadCost { get; set; }
|
||||
public decimal FacilityOverheadRatePerHour { get; set; }
|
||||
|
||||
public decimal Total { get; set; }
|
||||
|
||||
// Cost Breakdown Details
|
||||
@@ -822,6 +825,10 @@ public class QuotePricingResult
|
||||
public int OvenBatches { get; set; }
|
||||
public int OvenCycleMinutes { get; set; }
|
||||
|
||||
// Facility overhead (rent + utilities apportioned by estimated job hours)
|
||||
public decimal FacilityOverheadCost { get; set; }
|
||||
public decimal FacilityOverheadRatePerHour { get; set; }
|
||||
|
||||
// Detailed breakdown for transparency
|
||||
public decimal MaterialCosts { get; set; }
|
||||
public decimal LaborCosts { get; set; }
|
||||
|
||||
@@ -112,6 +112,19 @@ public class WizardStep2Dto
|
||||
|
||||
[Display(Name = "Shop Capability Tier")]
|
||||
public ShopCapabilityTier ShopCapabilityTier { get; set; } = ShopCapabilityTier.Small;
|
||||
|
||||
// Facility Overhead
|
||||
[Range(0, 1000000)]
|
||||
[Display(Name = "Monthly Rent ($)")]
|
||||
public decimal MonthlyRent { get; set; } = 0m;
|
||||
|
||||
[Range(0, 1000000)]
|
||||
[Display(Name = "Monthly Utilities ($)")]
|
||||
public decimal MonthlyUtilities { get; set; } = 0m;
|
||||
|
||||
[Range(1, 10000)]
|
||||
[Display(Name = "Billable Hours/Month")]
|
||||
public int MonthlyBillableHours { get; set; } = 160;
|
||||
}
|
||||
|
||||
// ─── Step 3: Branding & Numbering ───────────────────────────────────────────
|
||||
|
||||
@@ -37,7 +37,11 @@ public class CompanyProfile : Profile
|
||||
.ForMember(dest => dest.DerivedBlastRateSqFtPerHour,
|
||||
opt => opt.MapFrom(src => ShopCapabilityCalculator.GetBlastRateSqFtPerHour(src)))
|
||||
.ForMember(dest => dest.DerivedCoatingRateSqFtPerHour,
|
||||
opt => opt.MapFrom(src => ShopCapabilityCalculator.GetCoatingRateSqFtPerHour(src)));
|
||||
opt => opt.MapFrom(src => ShopCapabilityCalculator.GetCoatingRateSqFtPerHour(src)))
|
||||
.ForMember(dest => dest.FacilityOverheadRatePerHour,
|
||||
opt => opt.MapFrom(src => src.MonthlyBillableHours > 0
|
||||
? (src.MonthlyRent + src.MonthlyUtilities) / src.MonthlyBillableHours
|
||||
: 0m));
|
||||
CreateMap<UpdateOperatingCostsDto, CompanyOperatingCosts>();
|
||||
CreateMap<UpdateBlastProfileDto, CompanyOperatingCosts>();
|
||||
|
||||
|
||||
@@ -542,6 +542,8 @@ public class PricingCalculationService : IPricingCalculationService
|
||||
ItemsSubtotal = 0,
|
||||
ShopSuppliesAmount = 0,
|
||||
ShopSuppliesPercent = 0,
|
||||
FacilityOverheadCost = 0,
|
||||
FacilityOverheadRatePerHour = 0,
|
||||
OverheadCosts = 0,
|
||||
OverheadPercent = 0,
|
||||
ProfitMargin = 0,
|
||||
@@ -683,14 +685,27 @@ public class PricingCalculationService : IPricingCalculationService
|
||||
|
||||
var itemsAndOvenSubtotal = itemsSubtotal + ovenBatchCost;
|
||||
|
||||
// 5. SHOP SUPPLIES (percentage of items + oven subtotal)
|
||||
// 5. FACILITY OVERHEAD (rent + utilities apportioned by estimated job hours)
|
||||
var facilityOverheadRatePerHour = 0m;
|
||||
var facilityOverheadCost = 0m;
|
||||
if (costs.MonthlyBillableHours > 0 && (costs.MonthlyRent + costs.MonthlyUtilities) > 0)
|
||||
{
|
||||
facilityOverheadRatePerHour = (costs.MonthlyRent + costs.MonthlyUtilities) / costs.MonthlyBillableHours;
|
||||
var totalEstimatedMinutes = items.Sum(i => (decimal)i.EstimatedMinutes * i.Quantity);
|
||||
facilityOverheadCost = facilityOverheadRatePerHour * (totalEstimatedMinutes / 60m);
|
||||
_logger.LogInformation(
|
||||
"Facility overhead: ${Rate:F2}/hr × {Min:F0} min = ${Cost:F2}",
|
||||
facilityOverheadRatePerHour, totalEstimatedMinutes, facilityOverheadCost);
|
||||
}
|
||||
|
||||
// 6. SHOP SUPPLIES (percentage of items + oven subtotal — does not include facility overhead)
|
||||
var shopSuppliesPercent = costs.ShopSuppliesRate;
|
||||
var shopSuppliesAmount = itemsAndOvenSubtotal * (shopSuppliesPercent / 100m);
|
||||
|
||||
// 6. SUBTOTAL BEFORE DISCOUNT (items + oven + shop supplies)
|
||||
var subtotalBeforeDiscount = itemsAndOvenSubtotal + shopSuppliesAmount;
|
||||
// 7. SUBTOTAL BEFORE DISCOUNT (items + oven + facility overhead + shop supplies)
|
||||
var subtotalBeforeDiscount = itemsAndOvenSubtotal + facilityOverheadCost + shopSuppliesAmount;
|
||||
|
||||
// 7. CUSTOMER PRICING TIER DISCOUNT (if applicable)
|
||||
// 8. CUSTOMER PRICING TIER DISCOUNT (if applicable)
|
||||
var pricingTierDiscountPercent = 0m;
|
||||
var pricingTierDiscountAmount = 0m;
|
||||
|
||||
@@ -782,6 +797,8 @@ public class PricingCalculationService : IPricingCalculationService
|
||||
OvenBatchCost = Math.Round(ovenBatchCost, 2),
|
||||
OvenBatches = effectiveBatches,
|
||||
OvenCycleMinutes = effectiveCycleMinutes,
|
||||
FacilityOverheadCost = Math.Round(facilityOverheadCost, 2),
|
||||
FacilityOverheadRatePerHour = Math.Round(facilityOverheadRatePerHour, 4),
|
||||
ShopSuppliesAmount = Math.Round(shopSuppliesAmount, 2),
|
||||
ShopSuppliesPercent = Math.Round(shopSuppliesPercent, 2),
|
||||
OverheadCosts = Math.Round(overheadCosts, 2),
|
||||
|
||||
Reference in New Issue
Block a user