From 42a8c089d5569afc64c03ca10183078cdba8e1eb Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Mon, 11 May 2026 21:10:05 -0400 Subject: [PATCH] Fix AI photo quote always returning shop minimum price MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs caused AI estimates to collapse to the shop minimum floor: 1. Coating rate with no guard: when a shop hadn't calibrated their coating gun (rate = 0), the prompt injected '~0 sqft/hr' paired with 'MUST use shop-specific rates' — Claude returned near-zero estimatedMinutes, zeroing labor cost and triggering the floor. Fixed to mirror the existing blast-rate guard: rate=0 now sends a fallback instruction to use conservative industry-average times. 2. Per-item minutes divided by quantity: both the system prompt and user prompt explicitly tell Claude to return estimatedMinutes 'per single item', but CalculatePricingPreview() was dividing by qty anyway. For qty > 1 this halved (or more) the labor cost, again pushing toward the floor. Removed the incorrect divide. Co-Authored-By: Claude Sonnet 4.6 --- .../Services/AiQuoteService.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs b/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs index a75d13c..2de6e73 100644 --- a/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs +++ b/src/PowderCoating.Infrastructure/Services/AiQuoteService.cs @@ -435,7 +435,15 @@ Only ask follow-up questions if truly needed — prefer to make reasonable assum shopSpeedLine = "- Shop blast rate: not calibrated — use conservative industry-average times for this shop tier"; } - var coatingSpeedLine = $"- THIS SHOP'S coating application rate: ~{coatingRate:F0} sqft/hr"; + string coatingSpeedLine; + if (coatingRate > 0) + coatingSpeedLine = $"- THIS SHOP'S coating application rate: ~{coatingRate:F0} sqft/hr — use this to derive coating time (surface area ÷ coating rate), NOT generic industry averages"; + else + coatingSpeedLine = "- Shop coating rate: not calibrated — use conservative industry-average coating times for this shop tier"; + + var rateInstruction = (blastRate > 0 || coatingRate > 0) + ? "IMPORTANT: For estimatedMinutes, you MUST use this shop's specific rates above where provided, not generic industry speeds." + : "IMPORTANT: For estimatedMinutes, use conservative industry-average times appropriate for a professional powder coating shop."; return $@"Please analyze the item(s) in the photo(s) for powder coating estimation. @@ -453,7 +461,7 @@ Company operating costs for your reference: {shopSpeedLine} {coatingSpeedLine} -IMPORTANT: For estimatedMinutes, you MUST use this shop's specific blast and coating rates above, not generic industry speeds. +{rateInstruction} Sandblasting time = surface area of item ÷ shop blast rate (sqft/hr), adjusted for part complexity (harder-to-reach areas take more passes). Coating time = surface area ÷ shop coating rate, adjusted for masking and complexity. Include racking/unracking, inspection, and any material-specific prep (preheat handling, chemical stripping) as ACTIVE labor time. @@ -547,9 +555,9 @@ Respond with the JSON object only."; _ => 0 }; - // Labor cost — AI returns total batch minutes, so divide by quantity to get per-item minutes. - // The unit price × quantity must equal the total batch labor cost. - var rawPerItemMinutes = aiResult.EstimatedMinutes / Math.Max(1m, (decimal)request.Quantity); + // Labor cost — AI returns per-item minutes (both system prompt and user prompt say "per single item"). + // Unit price is per item; the caller multiplies by quantity for the line total. + var rawPerItemMinutes = aiResult.EstimatedMinutes; var minFloorApplied = materialMinMinutes > 0 && rawPerItemMinutes < materialMinMinutes; var perItemMinutes = minFloorApplied ? materialMinMinutes : rawPerItemMinutes; var laborHours = perItemMinutes / 60m;