Self-heal inventory catalog links during sync

The sync propagation now also backfills the catalog link: any inventory item
with no PowderCatalogItemId that matches a catalog row by Manufacturer +
ManufacturerPartNumber (the catalog SKU) gets linked and picks up the catalog
price/product data. Only links on a confident match (exact SKU + matching
vendor, or a single unambiguous candidate), so it never mis-links.

This backfills items created before linking existed, automatically, on every
environment (dev and prod) with no manual step or one-off script — legacy items
link on the next sync, new items still link at create time. Cost basis,
quantity, notes, and image remain untouched.

Tests: links an unlinked item by manufacturer+part number; leaves it unlinked
when the part number has no catalog match. Full suite 278 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 08:32:33 -04:00
parent a6538d9638
commit 148a3f465e
2 changed files with 139 additions and 1 deletions
@@ -120,6 +120,83 @@ public class PowderCatalogPropagationTests
Assert.Null(refreshed!.CatalogReferencePrice); // stays null -> quoting falls back to UnitCost
}
[Fact]
public async Task Propagate_LinksUnlinkedItem_ByManufacturerAndPartNumber()
{
await using var context = CreateContext();
var catalog = new PowderCatalogItem
{
VendorName = "Columbia Coatings",
Sku = "CS1693053",
ColorName = "Joker Jewel",
Source = "Columbia Coatings API",
UnitPrice = 28m,
};
context.PowderCatalogItems.Add(catalog);
await context.SaveChangesAsync();
var inv = new InventoryItem
{
CompanyId = 1,
SKU = "POWD-2606-0009",
Name = "Joker Jewel",
Manufacturer = "Columbia Coatings",
ManufacturerPartNumber = "CS1693053", // matches catalog SKU
PowderCatalogItemId = null, // not linked yet (legacy item)
UnitCost = 20m,
};
context.InventoryItems.Add(inv);
await context.SaveChangesAsync();
var service = new PowderCatalogUpsertService(
new UnitOfWork(context),
Mock.Of<ILogger<PowderCatalogUpsertService>>());
await service.PropagateToLinkedInventoryAsync();
var refreshed = await context.InventoryItems.FindAsync(inv.Id);
Assert.Equal(catalog.Id, refreshed!.PowderCatalogItemId); // self-healed link
Assert.Equal(28m, refreshed.CatalogReferencePrice); // and got the price
Assert.Equal(20m, refreshed.UnitCost); // cost untouched
}
[Fact]
public async Task Propagate_DoesNotLink_WhenPartNumberDoesNotMatch()
{
await using var context = CreateContext();
context.PowderCatalogItems.Add(new PowderCatalogItem
{
VendorName = "Columbia Coatings",
Sku = "CS1693053",
ColorName = "Joker Jewel",
UnitPrice = 28m,
});
await context.SaveChangesAsync();
var inv = new InventoryItem
{
CompanyId = 1,
SKU = "POWD-2606-0010",
Name = "Something Else",
Manufacturer = "Columbia Coatings",
ManufacturerPartNumber = "NOPE-999", // no catalog match
PowderCatalogItemId = null,
};
context.InventoryItems.Add(inv);
await context.SaveChangesAsync();
var service = new PowderCatalogUpsertService(
new UnitOfWork(context),
Mock.Of<ILogger<PowderCatalogUpsertService>>());
await service.PropagateToLinkedInventoryAsync();
var refreshed = await context.InventoryItems.FindAsync(inv.Id);
Assert.Null(refreshed!.PowderCatalogItemId); // stays unlinked
}
private static ApplicationDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()