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:
@@ -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>()
|
||||
|
||||
Reference in New Issue
Block a user