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." });
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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,
|
||||
|
||||
Reference in New Issue
Block a user