Fix incoming powder inventory: defer creation to approval, deduplicate, fix category

Three bugs fixed:

1. Wrong timing — inventory items with IsIncoming=true were auto-created during
   quote save (in QuotePricingAssemblyService). Now deferred to quote approval so
   inventory only reflects powders the shop is actually going to process.

2. Duplicate records — same powder on multiple items in one quote created multiple
   inventory records. Now grouped by PowderCatalogItemId: one record per unique
   catalog powder, all matching coats linked to the same record.

3. Wrong category — category resolution used first IsCoating=true by DisplayOrder,
   which could land items in Cerakote or other unintended categories. Now prefers
   CategoryCode==POWDER explicitly, with DisplayOrder fallback.

Changes:
- QuoteItemCoat: add PowderCatalogItemId int? — persists catalog reference at quote
  save time so the approval path knows what to create
- QuotePricingAssemblyService.BuildQuoteItemCoatsAsync: store PowderCatalogItemId
  on coat instead of calling CreateIncomingInventoryItemAsync immediately
- QuotePricingAssemblyService.CreateIncomingInventoryItemAsync: signature changed
  from (coatDto, companyId) to (catalogItemId, companyId); category lookup prefers
  POWDER code; no longer clears PowderCostPerLb on the DTO
- QuotePricingAssemblyService.EnsureIncomingInventoryForApprovedQuoteAsync: new
  public method called at approval — loads pending coats, groups by catalog ID,
  creates one inventory item per group, links all coats in each group
- IQuotePricingAssemblyService: exposes EnsureIncomingInventoryForApprovedQuoteAsync
- QuotesController.ApproveQuote: calls EnsureIncomingInventory after save
- QuotesController.ChangeQuoteStatus: calls EnsureIncomingInventory on Approved
- QuoteApprovalController: injects IQuotePricingAssemblyService; calls
  EnsureIncomingInventory in ApproveInternal (customer-facing portal path)
- InventoryController.CreateIncomingFromCatalog: same category fix (prefers POWDER)
- Migration: AddPowderCatalogItemIdToCoat (nullable int on QuoteItemCoats)
- Tests: updated AddAsIncoming test to verify deferred behavior; new deduplication test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 10:12:24 -04:00
parent 9dd36238bb
commit 972123c7a2
11 changed files with 11230 additions and 52 deletions
@@ -6868,7 +6868,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 1,
CompanyId = 0,
CreatedAt = new DateTime(2026, 5, 27, 12, 41, 26, 605, DateTimeKind.Utc).AddTicks(3040),
CreatedAt = new DateTime(2026, 5, 27, 13, 46, 47, 552, DateTimeKind.Utc).AddTicks(8956),
Description = "Standard pricing for regular customers",
DiscountPercent = 0m,
IsActive = true,
@@ -6879,7 +6879,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 2,
CompanyId = 0,
CreatedAt = new DateTime(2026, 5, 27, 12, 41, 26, 605, DateTimeKind.Utc).AddTicks(3052),
CreatedAt = new DateTime(2026, 5, 27, 13, 46, 47, 552, DateTimeKind.Utc).AddTicks(8962),
Description = "5% discount for preferred customers",
DiscountPercent = 5m,
IsActive = true,
@@ -6890,7 +6890,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 3,
CompanyId = 0,
CreatedAt = new DateTime(2026, 5, 27, 12, 41, 26, 605, DateTimeKind.Utc).AddTicks(3054),
CreatedAt = new DateTime(2026, 5, 27, 13, 46, 47, 552, DateTimeKind.Utc).AddTicks(8964),
Description = "10% discount for premium customers",
DiscountPercent = 10m,
IsActive = true,
@@ -7582,6 +7582,9 @@ namespace PowderCoating.Infrastructure.Migrations
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<int?>("PowderCatalogItemId")
.HasColumnType("int");
b.Property<decimal?>("PowderCostPerLb")
.HasColumnType("decimal(18,2)");