From 0b246724d9b1fece73ec56b1085a91d93d9c1d4c Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 20 Aug 2026 11:44:07 -0300 Subject: [PATCH] fix: align vendor trades with confirmed taxonomy --- ...0820150000_SH247_CanonicalTradeTaxonomy.cs | 84 +++++++++++++++++++ .../Models/TradeCatalog.cs | 29 +++---- .../SH249TradesVocabularyMigrationTests.cs | 21 +++-- 3 files changed, 112 insertions(+), 22 deletions(-) create mode 100644 Data.SeaHavenIndustries/Migrations/20260820150000_SH247_CanonicalTradeTaxonomy.cs 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.