Extract shared catalog enrichment into EnrichFromCatalogAsync helper
AiLookup and ScanLabel were running separate catalog lookup + auto-contribute code paths. Both now go through EnrichFromCatalogAsync so any future change to catalog logic only needs to be made once. - EnrichFromCatalogAsync: private helper that finds a matching PowderCatalogItem by SKU + manufacturer, overwrites AI-inferred spec fields with catalog values (catalog is authoritative), fills gaps for URL/price fields with ??=, and optionally auto-contributes new entries to the platform catalog. Returns (wasInCatalog, addedToCatalog) for callers that show UI badges. - AiLookup: now calls EnrichFromCatalogAsync then ApplyTdsCureFallbackAsync before returning — same enrichment pipeline as ScanLabel. - ScanLabel: replaced ~50-line inline catalog block with two helper calls. Return statement simplified from catalogMatch?.X ?? aiResult.X to just aiResult.X since EnrichFromCatalogAsync already merged catalog values in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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." });
|
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);
|
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);
|
return Json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,6 +710,92 @@ public class InventoryController : Controller
|
|||||||
return Json(result);
|
return Json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up <paramref name="result"/> 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 <paramref name="autoContribute"/> 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 <paramref name="result"/> in place.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If cure temperature or cure time is still missing after the primary lookup but a TDS URL
|
/// 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.
|
/// 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)
|
if (!aiResult.Success)
|
||||||
return Json(new { success = false, errorMessage = aiResult.ErrorMessage });
|
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 sku = aiResult.ManufacturerPartNumber?.Trim();
|
||||||
var manufacturer = (aiResult.Manufacturer ?? aiResult.VendorName)?.Trim();
|
var manufacturer = (aiResult.Manufacturer ?? aiResult.VendorName)?.Trim();
|
||||||
var colorName = aiResult.ColorName?.Trim();
|
var colorName = aiResult.ColorName?.Trim();
|
||||||
|
|
||||||
PowderCatalogItem? catalogMatch = null;
|
// Catalog lookup, merge, and auto-contribute — same logic as AiLookup button
|
||||||
if (!string.IsNullOrEmpty(sku) && !string.IsNullOrEmpty(manufacturer))
|
var (wasInCatalog, addedToCatalog) = await EnrichFromCatalogAsync(aiResult, autoContribute: true);
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
var wasInCatalog = catalogMatch != null;
|
// TDS cure fallback — same logic as AiLookup button
|
||||||
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;
|
|
||||||
await ApplyTdsCureFallbackAsync(aiResult, colorName);
|
await ApplyTdsCureFallbackAsync(aiResult, colorName);
|
||||||
|
|
||||||
// Check if this product already exists in the tenant's inventory.
|
// Check if this product already exists in the tenant's inventory.
|
||||||
@@ -913,18 +946,18 @@ public class InventoryController : Controller
|
|||||||
manufacturerPartNumber = sku,
|
manufacturerPartNumber = sku,
|
||||||
colorName = colorName,
|
colorName = colorName,
|
||||||
description = aiResult.Description,
|
description = aiResult.Description,
|
||||||
finish = catalogMatch?.Finish ?? aiResult.Finish,
|
finish = aiResult.Finish,
|
||||||
cureTemperatureF = catalogMatch?.CureTemperatureF ?? aiResult.CureTemperatureF,
|
cureTemperatureF = aiResult.CureTemperatureF,
|
||||||
cureTimeMinutes = catalogMatch?.CureTimeMinutes ?? aiResult.CureTimeMinutes,
|
cureTimeMinutes = aiResult.CureTimeMinutes,
|
||||||
colorFamilies = catalogMatch?.ColorFamilies ?? aiResult.ColorFamilies,
|
colorFamilies = aiResult.ColorFamilies,
|
||||||
requiresClearCoat = catalogMatch?.RequiresClearCoat ?? aiResult.RequiresClearCoat,
|
requiresClearCoat = aiResult.RequiresClearCoat,
|
||||||
coverageSqFtPerLb = catalogMatch?.CoverageSqFtPerLb ?? aiResult.CoverageSqFtPerLb,
|
coverageSqFtPerLb = aiResult.CoverageSqFtPerLb,
|
||||||
transferEfficiency = catalogMatch?.TransferEfficiency ?? aiResult.TransferEfficiency,
|
transferEfficiency = aiResult.TransferEfficiency,
|
||||||
unitPrice = catalogMatch?.UnitPrice ?? aiResult.UnitCostPerLb ?? 0m,
|
unitPrice = aiResult.UnitCostPerLb ?? 0m,
|
||||||
imageUrl = catalogMatch?.ImageUrl ?? aiResult.ImageUrl,
|
imageUrl = aiResult.ImageUrl,
|
||||||
productUrl = catalogMatch?.ProductUrl ?? aiResult.SpecPageUrl,
|
productUrl = aiResult.SpecPageUrl,
|
||||||
sdsUrl = catalogMatch?.SdsUrl ?? aiResult.SdsUrl,
|
sdsUrl = aiResult.SdsUrl,
|
||||||
tdsUrl = catalogMatch?.TdsUrl ?? aiResult.TdsUrl,
|
tdsUrl = aiResult.TdsUrl,
|
||||||
vendorName = manufacturer,
|
vendorName = manufacturer,
|
||||||
wasInCatalog = wasInCatalog,
|
wasInCatalog = wasInCatalog,
|
||||||
addedToCatalog = addedToCatalog,
|
addedToCatalog = addedToCatalog,
|
||||||
|
|||||||
Reference in New Issue
Block a user