Lazily enrich catalog specs from TDS on first use

Specific gravity, coverage, and ~55% of cure specs aren't in the Columbia feed.
Rather than read 2,400 TDS PDFs up front, enrich a catalog item the first time
it's actually used:

- FetchTdsCureSpecsAsync now also extracts specific gravity from the TDS.
- New EnsureCatalogTdsSpecsAsync fills a catalog item's specific gravity (and
  any missing cure temp/time) from its TDS, then derives theoretical coverage
  (192.3 / (SG x mils)). No-op once specific gravity is known or when there's no
  TDS; persists to the catalog so the work is done once and benefits everyone.
- Hooked into the catalog->inventory paths (CreateIncomingFromCatalog, the
  custom-powder receive enrichment, and ReceivePowderFromCatalog) so a powder's
  full specs land on both the catalog and the new inventory record. DashboardController
  gains the AI lookup service for this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 16:07:23 -04:00
parent 8401bd77e8
commit 059d94d4fe
4 changed files with 88 additions and 10 deletions
@@ -25,6 +25,7 @@ public class DashboardController : Controller
private readonly ICompanyConfigHealthService _configHealth;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ISubscriptionService _subscriptionService;
private readonly IInventoryAiLookupService _aiLookupService;
public DashboardController(
IUnitOfWork unitOfWork,
@@ -33,7 +34,8 @@ public class DashboardController : Controller
ITenantContext tenantContext,
ICompanyConfigHealthService configHealth,
UserManager<ApplicationUser> userManager,
ISubscriptionService subscriptionService)
ISubscriptionService subscriptionService,
IInventoryAiLookupService aiLookupService)
{
_unitOfWork = unitOfWork;
_logger = logger;
@@ -42,6 +44,7 @@ public class DashboardController : Controller
_configHealth = configHealth;
_userManager = userManager;
_subscriptionService = subscriptionService;
_aiLookupService = aiLookupService;
}
/// <summary>
@@ -868,8 +871,12 @@ public class DashboardController : Controller
InventoryItem item, string? colorCode, string? colorName, string? manufacturer)
{
var catalog = await FindCatalogByIdentityAsync(colorCode, colorName, manufacturer);
if (catalog != null)
ApplyCatalogToInventory(item, catalog);
if (catalog == null)
return;
// First use — lazily fill specific gravity / cure from the TDS before copying onto the item.
await _aiLookupService.EnsureCatalogTdsSpecsAsync(catalog);
ApplyCatalogToInventory(item, catalog);
}
/// <summary>
@@ -987,6 +994,9 @@ public class DashboardController : Controller
if (catalog == null)
return Json(new { success = false, needsDetails = true });
// First use — lazily fill specific gravity / cure from the TDS so the new record is complete.
await _aiLookupService.EnsureCatalogTdsSpecsAsync(catalog);
// Resolve the company's POWDER (coating) inventory category.
var categories = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId);
var coatingCategory = categories.FirstOrDefault(c => c.IsActive && c.IsCoating
@@ -1263,6 +1263,10 @@ public class InventoryController : Controller
if (catalogItem == null)
return Json(new { success = false, error = "Catalog item not found." });
// First use of this powder — lazily fill specific gravity / cure from its TDS so the new
// inventory record (and the catalog) carry complete specs. No-op once already enriched.
await _aiLookupService.EnsureCatalogTdsSpecsAsync(catalogItem);
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
// Find the default coating category to assign.