Auto-receive catalog powders and fix soft-deleted SKU collision

Two fixes to the "Got It" powder receive flow:

1. Skip the modal when the powder is in the master catalog. Clicking "Got It"
   now first calls ReceivePowderFromCatalog, which — if the powder resolves in
   the catalog — creates a fully populated inventory record (specs, cure, SDS/
   TDS, image, pricing) and marks the coat received, no modal. Only when the
   powder isn't in the catalog does it fall back to the manual entry modal.
   The catalog match/apply and the receive finalize (opening txn, mark received,
   sibling-coat linking) are extracted into shared helpers used by both the
   modal save and the auto-receive path.

2. Fix a crash re-receiving a previously-deleted powder. The unique index
   IX_InventoryItems_CompanyId_SKU had no filter, so a soft-deleted item still
   reserved its SKU; re-creating it generated the same SKU and violated the
   constraint. The index is now filtered on IsDeleted = 0, matching the app's
   soft-delete semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:42:29 -04:00
parent 99b22d2ad2
commit 115ccf7d5e
6 changed files with 11702 additions and 75 deletions
@@ -1511,6 +1511,9 @@ modelBuilder.Entity<Job>()
modelBuilder.Entity<InventoryItem>()
.HasIndex(i => new { i.CompanyId, i.SKU })
.IsUnique()
// Filter on IsDeleted so soft-deleted items don't reserve their SKU and block a new
// (or re-created) item from reusing it — matching the app's soft-delete semantics.
.HasFilter("[IsDeleted] = 0")
.HasDatabaseName("IX_InventoryItems_CompanyId_SKU");
modelBuilder.Entity<Company>()
@@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PowderCoating.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class FilterInventorySkuUniqueIndexOnSoftDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_InventoryItems_CompanyId_SKU",
table: "InventoryItems");
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 1,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4251));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 2,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4258));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 3,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4259));
migrationBuilder.CreateIndex(
name: "IX_InventoryItems_CompanyId_SKU",
table: "InventoryItems",
columns: new[] { "CompanyId", "SKU" },
unique: true,
filter: "[IsDeleted] = 0");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_InventoryItems_CompanyId_SKU",
table: "InventoryItems");
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 1,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(5997));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 2,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6002));
migrationBuilder.UpdateData(
table: "PricingTiers",
keyColumn: "Id",
keyValue: 3,
column: "CreatedAt",
value: new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6003));
migrationBuilder.CreateIndex(
name: "IX_InventoryItems_CompanyId_SKU",
table: "InventoryItems",
columns: new[] { "CompanyId", "SKU" },
unique: true);
}
}
}
@@ -4244,7 +4244,8 @@ namespace PowderCoating.Infrastructure.Migrations
b.HasIndex("CompanyId", "SKU")
.IsUnique()
.HasDatabaseName("IX_InventoryItems_CompanyId_SKU");
.HasDatabaseName("IX_InventoryItems_CompanyId_SKU")
.HasFilter("[IsDeleted] = 0");
b.HasIndex("CompanyId", "QuantityOnHand", "ReorderPoint")
.HasDatabaseName("IX_InventoryItems_CompanyId_Quantity_Reorder");
@@ -7234,7 +7235,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 1,
CompanyId = 0,
CreatedAt = new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(5997),
CreatedAt = new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4251),
Description = "Standard pricing for regular customers",
DiscountPercent = 0m,
IsActive = true,
@@ -7245,7 +7246,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 2,
CompanyId = 0,
CreatedAt = new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6002),
CreatedAt = new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4258),
Description = "5% discount for preferred customers",
DiscountPercent = 5m,
IsActive = true,
@@ -7256,7 +7257,7 @@ namespace PowderCoating.Infrastructure.Migrations
{
Id = 3,
CompanyId = 0,
CreatedAt = new DateTime(2026, 6, 17, 14, 44, 40, 147, DateTimeKind.Utc).AddTicks(6003),
CreatedAt = new DateTime(2026, 6, 17, 16, 33, 6, 662, DateTimeKind.Utc).AddTicks(4259),
Description = "10% discount for premium customers",
DiscountPercent = 10m,
IsActive = true,