Fix facility overhead missing from invoices on quote-based jobs

For quote-based jobs, invoice creation now reads fee components (oven,
facility overhead, shop supplies, rush fee) from the job's
PricingBreakdownJson snapshot rather than the source quote. The
FacilityOverheadCost column was added to Quotes in May 2026; older
quotes have 0 there even though overhead was included in their total,
causing invoices to silently drop the overhead charge. The job snapshot
is updated on every save so it always reflects the current pricing.
Tax rate and discount still come from the source quote as agreed terms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 22:18:52 -04:00
parent 51a5268bc2
commit eaab0af51f
@@ -464,16 +464,22 @@ public class InvoicesController : Controller
// If the job came from a quote, carry over the quote-level costs and agreed terms.
// The quote SubTotal = sum(items) + oven batch cost + shop supplies.
// Job items only capture per-item prices, so oven & shop supplies need a separate line.
// Read directly from the quote snapshot — never try to reverse-engineer from job.FinalPrice
// because FinalPrice is recalculated on every item edit and can drift from the original quote.
// For fee components, prefer the job's own breakdown snapshot (updated every time the job
// is saved) over the source quote — the quote's FacilityOverheadCost was only added in
// migration AddQuotePricingSnapshotFields (May 2026); older quotes have 0 there even though
// overhead was included in the quote total. Tax and discount still come from the quote
// because those represent the customer-approved agreed terms.
if (sourceQuote != null)
{
// Bundle all quote-level charges so the invoice subtotal matches the quote total.
// FacilityOverheadCost is included — it is a real cost baked into the quoted price.
var processingFees = sourceQuote.OvenBatchCost
+ sourceQuote.FacilityOverheadCost
+ sourceQuote.ShopSuppliesAmount
+ sourceQuote.RushFee;
// Prefer job breakdown values for dynamic fee components; fall back to quote for
// compatibility with jobs that were never re-saved after the May 2026 migration.
var ovenCost = jobBreakdown != null ? jobBreakdown.OvenBatchCost : sourceQuote.OvenBatchCost;
var overhead = jobBreakdown != null ? jobBreakdown.FacilityOverheadCost : sourceQuote.FacilityOverheadCost;
var shopSupplies = jobBreakdown != null ? jobBreakdown.ShopSuppliesAmount : sourceQuote.ShopSuppliesAmount;
var rushFee = jobBreakdown != null ? jobBreakdown.RushFee : sourceQuote.RushFee;
// Bundle all quote-level charges so the invoice subtotal matches the job total.
var processingFees = ovenCost + overhead + shopSupplies + rushFee;
if (processingFees > 0.01m)
{