diff --git a/Data.SeaHavenIndustries/Migrations/20260820150000_SH247_CanonicalTradeTaxonomy.cs b/Data.SeaHavenIndustries/Migrations/20260820150000_SH247_CanonicalTradeTaxonomy.cs
new file mode 100644
index 0000000..134492c
--- /dev/null
+++ b/Data.SeaHavenIndustries/Migrations/20260820150000_SH247_CanonicalTradeTaxonomy.cs
@@ -0,0 +1,84 @@
+using Data.SeaHavenIndustries;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Data.SeaHavenIndustries.Migrations
+{
+ ///
+ /// Replaces SH-249's provisional prototype vocabulary with the 14-item
+ /// client-confirmed taxonomy recorded in SH-247.
+ ///
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260820150000_SH247_CanonicalTradeTaxonomy")]
+ public partial class SH247_CanonicalTradeTaxonomy : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ UpdateTrade(migrationBuilder, 1, "Plumbing & Water Systems", 1);
+ UpdateTrade(migrationBuilder, 2, "Electrical", 2);
+ UpdateTrade(migrationBuilder, 3, "Doors & Docks", 3);
+ UpdateTrade(migrationBuilder, 4, "Fire & Life Safety", 4);
+ UpdateTrade(migrationBuilder, 5, "Security & Locksmith", 5);
+ UpdateTrade(migrationBuilder, 6, "HVAC", 6);
+ UpdateTrade(migrationBuilder, 7, "Exterior & Grounds", 7);
+ UpdateTrade(migrationBuilder, 8, "Building Envelope", 8);
+ UpdateTrade(migrationBuilder, 9, "Interior Finishes", 9);
+ UpdateTrade(migrationBuilder, 10, "Signage", 10);
+
+ migrationBuilder.InsertData(
+ table: "Trades",
+ columns: new[] { "Id", "Name", "NormalizedName", "SortOrder", "IsActive" },
+ columnTypes: new[] { "int", "nvarchar(128)", "nvarchar(128)", "int", "bit" },
+ values: new object[,]
+ {
+ { 11, "Conveyance & MHE", "conveyance & mhe", 11, true },
+ { 12, "Janitorial", "janitorial", 12, true },
+ { 13, "Specialty", "specialty", 13, true },
+ { 14, "General Building & Handyman", "general building & handyman", 14, true }
+ });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ DeleteTrade(migrationBuilder, 11);
+ DeleteTrade(migrationBuilder, 12);
+ DeleteTrade(migrationBuilder, 13);
+ DeleteTrade(migrationBuilder, 14);
+
+ // Free the HVAC normalized key before restoring it to row 3.
+ UpdateTrade(migrationBuilder, 6, "Roofing", 6);
+ UpdateTrade(migrationBuilder, 1, "Plumbing", 1);
+ UpdateTrade(migrationBuilder, 2, "Electrical", 2);
+ UpdateTrade(migrationBuilder, 3, "HVAC", 3);
+ UpdateTrade(migrationBuilder, 4, "Fire/Life Safety", 4);
+ UpdateTrade(migrationBuilder, 5, "General", 5);
+ UpdateTrade(migrationBuilder, 7, "Janitorial", 7);
+ UpdateTrade(migrationBuilder, 8, "Painting", 8);
+ UpdateTrade(migrationBuilder, 9, "Locksmith", 9);
+ UpdateTrade(migrationBuilder, 10, "Carpentry", 10);
+ }
+
+ private static void UpdateTrade(MigrationBuilder migrationBuilder, int id, string name, int sortOrder)
+ {
+ migrationBuilder.UpdateData(
+ table: "Trades",
+ keyColumns: new[] { "Id" },
+ keyColumnTypes: new[] { "int" },
+ keyValues: new object[] { id },
+ columns: new[] { "Name", "NormalizedName", "SortOrder", "IsActive" },
+ columnTypes: new[] { "nvarchar(128)", "nvarchar(128)", "int", "bit" },
+ values: new object[] { name, TradeCatalog.Normalize(name), sortOrder, true });
+ }
+
+ private static void DeleteTrade(MigrationBuilder migrationBuilder, int id)
+ {
+ migrationBuilder.DeleteData(
+ table: "Trades",
+ keyColumns: new[] { "Id" },
+ keyColumnTypes: new[] { "int" },
+ keyValues: new object[] { id });
+ }
+ }
+}
diff --git a/Data.SeaHavenIndustries/Models/TradeCatalog.cs b/Data.SeaHavenIndustries/Models/TradeCatalog.cs
index f35ba4f..20469a0 100644
--- a/Data.SeaHavenIndustries/Models/TradeCatalog.cs
+++ b/Data.SeaHavenIndustries/Models/TradeCatalog.cs
@@ -1,26 +1,27 @@
namespace Data.SeaHavenIndustries
{
- // SH-249 PROVISIONAL canonical trade vocabulary. Sourced from the
- // in-repo prototype seed (Api.SeaHavenIndustries/Program.cs, disabled
- // development seeding) because no authoritative Product list exists in
- // the repo. Product must confirm the list before release; after the
- // SH249_TradesVocabulary migration ships, extend the vocabulary only
- // through new migrations (and keep this catalog in sync — tests assert
- // the seeded table matches this catalog).
+ // SH-247 client-confirmed canonical trade taxonomy. Adam confirmed these
+ // 14 values after the 2026-07-29 validation meeting; they supersede both
+ // SH-24's service-leaf list and the provisional prototype seed originally
+ // shipped by SH-249. Keep migrations and this catalog in sync.
public static class TradeCatalog
{
public static readonly IReadOnlyList CanonicalTrades = new[]
{
- "Plumbing",
+ "Plumbing & Water Systems",
"Electrical",
+ "Doors & Docks",
+ "Fire & Life Safety",
+ "Security & Locksmith",
"HVAC",
- "Fire/Life Safety",
- "General",
- "Roofing",
+ "Exterior & Grounds",
+ "Building Envelope",
+ "Interior Finishes",
+ "Signage",
+ "Conveyance & MHE",
"Janitorial",
- "Painting",
- "Locksmith",
- "Carpentry"
+ "Specialty",
+ "General Building & Handyman"
};
public static string Normalize(string name) => name.Trim().ToLowerInvariant();
diff --git a/SeaHavenIndustries.Tests/SH249TradesVocabularyMigrationTests.cs b/SeaHavenIndustries.Tests/SH249TradesVocabularyMigrationTests.cs
index 7d55632..3e2eeaf 100644
--- a/SeaHavenIndustries.Tests/SH249TradesVocabularyMigrationTests.cs
+++ b/SeaHavenIndustries.Tests/SH249TradesVocabularyMigrationTests.cs
@@ -8,9 +8,8 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace SeaHavenIndustries.Tests;
-// SH-249: provider-backed migration evidence for the Trades reference table.
-// Applies the real migration operations (Up, then Down) against SQLite and
-// asserts the seeded canonical vocabulary matches TradeCatalog.
+// SH-249/SH-247: provider-backed evidence for the Trades reference table and
+// the client-confirmed replacement taxonomy.
public class SH249TradesVocabularyMigrationTests
{
[Fact]
@@ -24,7 +23,8 @@ public class SH249TradesVocabularyMigrationTests
.Options;
await using var context = new ApplicationDbContext(options);
- await ApplyMigrationAsync(context, "Up");
+ await ApplyMigrationAsync(context, new SH249_TradesVocabulary(), "Up");
+ await ApplyMigrationAsync(context, new SH247_CanonicalTradeTaxonomy(), "Up");
var seeded = await context.Trades
.OrderBy(t => t.SortOrder)
@@ -50,8 +50,10 @@ public class SH249TradesVocabularyMigrationTests
.Options;
await using var context = new ApplicationDbContext(options);
- await ApplyMigrationAsync(context, "Up");
- await ApplyMigrationAsync(context, "Down");
+ await ApplyMigrationAsync(context, new SH249_TradesVocabulary(), "Up");
+ await ApplyMigrationAsync(context, new SH247_CanonicalTradeTaxonomy(), "Up");
+ await ApplyMigrationAsync(context, new SH247_CanonicalTradeTaxonomy(), "Down");
+ await ApplyMigrationAsync(context, new SH249_TradesVocabulary(), "Down");
await using var command = connection.CreateCommand();
command.CommandText =
@@ -64,12 +66,15 @@ public class SH249TradesVocabularyMigrationTests
// Mirrors Phase4SearchIndexesSqlServerTests: invoke the protected Up/Down
// body, translate through the configured provider's SQL generator, and
// execute the produced commands against the live database.
- private static async Task ApplyMigrationAsync(ApplicationDbContext context, string direction)
+ private static async Task ApplyMigrationAsync(
+ ApplicationDbContext context,
+ Migration migration,
+ string direction)
{
var builder = new MigrationBuilder(context.Database.ProviderName!);
var operation = typeof(Migration).GetMethod(direction, BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException($"Migration.{direction} not found.");
- operation.Invoke(new SH249_TradesVocabulary(), [builder]);
+ operation.Invoke(migration, [builder]);
var sqlGenerator = context.GetService();
// InsertData operations need model metadata for column type mapping.