diff --git a/src/PowderCoating.Web/Controllers/InventoryController.cs b/src/PowderCoating.Web/Controllers/InventoryController.cs
index 34305e0..69f8de2 100644
--- a/src/PowderCoating.Web/Controllers/InventoryController.cs
+++ b/src/PowderCoating.Web/Controllers/InventoryController.cs
@@ -679,7 +679,11 @@ public class InventoryController : Controller
return Json(new { success = false, errorMessage = "AI Inventory Assist is not enabled for your account. Contact your administrator." });
var result = await _aiLookupService.LookupAsync(manufacturer, colorName, colorCode, partNumber);
- if (result.Success) await ApplyTdsCureFallbackAsync(result, colorName);
+ if (result.Success)
+ {
+ await EnrichFromCatalogAsync(result, autoContribute: true);
+ await ApplyTdsCureFallbackAsync(result, colorName);
+ }
return Json(result);
}
@@ -706,6 +710,92 @@ public class InventoryController : Controller
return Json(result);
}
+ ///
+ /// Looks up in the platform powder catalog by SKU + manufacturer.
+ /// If a match is found, catalog values overwrite Claude-inferred ones for spec fields
+ /// (catalog is the authoritative source) and fill gaps for URL/price fields.
+ /// If no match and is true, inserts a new catalog entry
+ /// so future lookups resolve instantly without an API call.
+ /// Returns (wasInCatalog, addedToCatalog) so callers can surface UI badges.
+ /// Mutates in place.
+ ///
+ private async Task<(bool wasInCatalog, bool addedToCatalog)> EnrichFromCatalogAsync(
+ InventoryAiLookupResult result, bool autoContribute)
+ {
+ var sku = result.ManufacturerPartNumber?.Trim();
+ var manufacturer = (result.Manufacturer ?? result.VendorName)?.Trim();
+ var colorName = result.ColorName?.Trim();
+
+ PowderCatalogItem? match = null;
+ if (!string.IsNullOrEmpty(sku) && !string.IsNullOrEmpty(manufacturer))
+ {
+ var skuLower = sku.ToLower();
+ var mfrLower = manufacturer.ToLower();
+ var hits = await _unitOfWork.PowderCatalog.FindAsync(p =>
+ p.Sku.ToLower() == skuLower && p.VendorName.ToLower().Contains(mfrLower));
+ match = hits.FirstOrDefault();
+ }
+
+ if (match != null)
+ {
+ // Catalog is authoritative for spec fields — overwrite AI inference
+ if (match.Finish != null) result.Finish = match.Finish;
+ if (match.CureTemperatureF != null) result.CureTemperatureF = match.CureTemperatureF;
+ if (match.CureTimeMinutes != null) result.CureTimeMinutes = match.CureTimeMinutes;
+ if (match.ColorFamilies != null) result.ColorFamilies = match.ColorFamilies;
+ if (match.RequiresClearCoat != null) result.RequiresClearCoat = match.RequiresClearCoat;
+ if (match.CoverageSqFtPerLb != null) result.CoverageSqFtPerLb = match.CoverageSqFtPerLb;
+ if (match.TransferEfficiency != null) result.TransferEfficiency = match.TransferEfficiency;
+ // URL / price fields: fill gaps only — AI may have found something better
+ result.ImageUrl ??= match.ImageUrl;
+ result.SpecPageUrl ??= match.ProductUrl;
+ result.SdsUrl ??= match.SdsUrl;
+ result.TdsUrl ??= match.TdsUrl;
+ if (match.UnitPrice > 0) result.UnitCostPerLb ??= match.UnitPrice;
+ return (true, false);
+ }
+
+ if (!autoContribute
+ || string.IsNullOrEmpty(sku)
+ || string.IsNullOrEmpty(manufacturer)
+ || string.IsNullOrEmpty(colorName))
+ return (false, false);
+
+ // Auto-contribute: insert into platform catalog so future lookups/scans resolve instantly
+ try
+ {
+ var newItem = new PowderCatalogItem
+ {
+ VendorName = manufacturer,
+ Sku = sku,
+ ColorName = colorName,
+ CureTemperatureF = result.CureTemperatureF,
+ CureTimeMinutes = result.CureTimeMinutes,
+ Finish = result.Finish,
+ ColorFamilies = result.ColorFamilies,
+ RequiresClearCoat = result.RequiresClearCoat,
+ CoverageSqFtPerLb = result.CoverageSqFtPerLb,
+ TransferEfficiency = result.TransferEfficiency,
+ ImageUrl = result.ImageUrl,
+ ProductUrl = result.SpecPageUrl,
+ SdsUrl = result.SdsUrl,
+ TdsUrl = result.TdsUrl,
+ IsUserContributed = true,
+ CreatedAt = DateTime.UtcNow,
+ };
+ await _unitOfWork.PowderCatalog.AddAsync(newItem);
+ await _unitOfWork.CompleteAsync();
+ _logger.LogInformation("Auto-contributed new catalog entry: {Manufacturer} {Sku}", manufacturer, sku);
+ return (false, true);
+ }
+ catch (Exception ex)
+ {
+ // Unique constraint violation means another request beat us — not an error
+ _logger.LogInformation("Catalog auto-insert skipped (likely duplicate): {Message}", ex.Message);
+ return (false, false);
+ }
+ }
+
///
/// If cure temperature or cure time is still missing after the primary lookup but a TDS URL
/// was returned, fetches that page and asks Claude to extract only the cure schedule.
@@ -799,71 +889,14 @@ public class InventoryController : Controller
if (!aiResult.Success)
return Json(new { success = false, errorMessage = aiResult.ErrorMessage });
- // Search catalog by SKU first (most precise), then fall back to color name
var sku = aiResult.ManufacturerPartNumber?.Trim();
var manufacturer = (aiResult.Manufacturer ?? aiResult.VendorName)?.Trim();
var colorName = aiResult.ColorName?.Trim();
- PowderCatalogItem? catalogMatch = null;
- if (!string.IsNullOrEmpty(sku) && !string.IsNullOrEmpty(manufacturer))
- {
- var skuLower = sku.ToLower();
- var mfrLower = manufacturer.ToLower();
- var skuMatches = await _unitOfWork.PowderCatalog.FindAsync(p =>
- p.Sku.ToLower() == skuLower && p.VendorName.ToLower().Contains(mfrLower));
- catalogMatch = skuMatches.FirstOrDefault();
- }
+ // Catalog lookup, merge, and auto-contribute — same logic as AiLookup button
+ var (wasInCatalog, addedToCatalog) = await EnrichFromCatalogAsync(aiResult, autoContribute: true);
- var wasInCatalog = catalogMatch != null;
- var addedToCatalog = false;
-
- // Auto-contribute: insert into platform catalog if we have the minimum viable fields
- // and this SKU isn't already there
- if (!wasInCatalog
- && !string.IsNullOrEmpty(sku)
- && !string.IsNullOrEmpty(manufacturer)
- && !string.IsNullOrEmpty(colorName))
- {
- try
- {
- var newItem = new PowderCatalogItem
- {
- VendorName = manufacturer,
- Sku = sku,
- ColorName = colorName,
- CureTemperatureF = aiResult.CureTemperatureF,
- CureTimeMinutes = aiResult.CureTimeMinutes,
- Finish = aiResult.Finish,
- ColorFamilies = aiResult.ColorFamilies,
- RequiresClearCoat = aiResult.RequiresClearCoat,
- CoverageSqFtPerLb = aiResult.CoverageSqFtPerLb,
- TransferEfficiency= aiResult.TransferEfficiency,
- ImageUrl = aiResult.ImageUrl,
- ProductUrl = aiResult.SpecPageUrl,
- SdsUrl = aiResult.SdsUrl,
- TdsUrl = aiResult.TdsUrl,
- IsUserContributed = true,
- CreatedAt = DateTime.UtcNow,
- };
- await _unitOfWork.PowderCatalog.AddAsync(newItem);
- await _unitOfWork.CompleteAsync();
- addedToCatalog = true;
- _logger.LogInformation("Label scan contributed new catalog entry: {Manufacturer} {Sku}", manufacturer, sku);
- }
- catch (Exception ex)
- {
- // Unique constraint violation means another request beat us — not an error
- _logger.LogInformation("Catalog auto-insert skipped (likely duplicate): {Message}", ex.Message);
- }
- }
-
- // If cure specs are still missing but we have a TDS URL, try reading it.
- // Prefer the catalog's TDS URL if present; the catalog record is more reliable.
- // Temporarily merge the catalog TDS URL into aiResult so ApplyTdsCureFallbackAsync
- // sees the best available URL and writes the result back into aiResult.
- if (catalogMatch?.CureTemperatureF != null) aiResult.CureTemperatureF = catalogMatch.CureTemperatureF;
- if (catalogMatch?.CureTimeMinutes != null) aiResult.CureTimeMinutes = catalogMatch.CureTimeMinutes;
- aiResult.TdsUrl ??= catalogMatch?.TdsUrl;
+ // TDS cure fallback — same logic as AiLookup button
await ApplyTdsCureFallbackAsync(aiResult, colorName);
// Check if this product already exists in the tenant's inventory.
@@ -913,18 +946,18 @@ public class InventoryController : Controller
manufacturerPartNumber = sku,
colorName = colorName,
description = aiResult.Description,
- finish = catalogMatch?.Finish ?? aiResult.Finish,
- cureTemperatureF = catalogMatch?.CureTemperatureF ?? aiResult.CureTemperatureF,
- cureTimeMinutes = catalogMatch?.CureTimeMinutes ?? aiResult.CureTimeMinutes,
- colorFamilies = catalogMatch?.ColorFamilies ?? aiResult.ColorFamilies,
- requiresClearCoat = catalogMatch?.RequiresClearCoat ?? aiResult.RequiresClearCoat,
- coverageSqFtPerLb = catalogMatch?.CoverageSqFtPerLb ?? aiResult.CoverageSqFtPerLb,
- transferEfficiency = catalogMatch?.TransferEfficiency ?? aiResult.TransferEfficiency,
- unitPrice = catalogMatch?.UnitPrice ?? aiResult.UnitCostPerLb ?? 0m,
- imageUrl = catalogMatch?.ImageUrl ?? aiResult.ImageUrl,
- productUrl = catalogMatch?.ProductUrl ?? aiResult.SpecPageUrl,
- sdsUrl = catalogMatch?.SdsUrl ?? aiResult.SdsUrl,
- tdsUrl = catalogMatch?.TdsUrl ?? aiResult.TdsUrl,
+ finish = aiResult.Finish,
+ cureTemperatureF = aiResult.CureTemperatureF,
+ cureTimeMinutes = aiResult.CureTimeMinutes,
+ colorFamilies = aiResult.ColorFamilies,
+ requiresClearCoat = aiResult.RequiresClearCoat,
+ coverageSqFtPerLb = aiResult.CoverageSqFtPerLb,
+ transferEfficiency = aiResult.TransferEfficiency,
+ unitPrice = aiResult.UnitCostPerLb ?? 0m,
+ imageUrl = aiResult.ImageUrl,
+ productUrl = aiResult.SpecPageUrl,
+ sdsUrl = aiResult.SdsUrl,
+ tdsUrl = aiResult.TdsUrl,
vendorName = manufacturer,
wasInCatalog = wasInCatalog,
addedToCatalog = addedToCatalog,