fix: align vendor trades with confirmed taxonomy

This commit is contained in:
Alexandre Brandizzi 2026-08-20 11:44:07 -03:00
parent 6eda0cd0f8
commit 0b246724d9
3 changed files with 112 additions and 22 deletions

View file

@ -0,0 +1,84 @@
using Data.SeaHavenIndustries;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.SeaHavenIndustries.Migrations
{
/// <summary>
/// Replaces SH-249's provisional prototype vocabulary with the 14-item
/// client-confirmed taxonomy recorded in SH-247.
/// </summary>
[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 });
}
}
}

View file

@ -1,26 +1,27 @@
namespace Data.SeaHavenIndustries namespace Data.SeaHavenIndustries
{ {
// SH-249 PROVISIONAL canonical trade vocabulary. Sourced from the // SH-247 client-confirmed canonical trade taxonomy. Adam confirmed these
// in-repo prototype seed (Api.SeaHavenIndustries/Program.cs, disabled // 14 values after the 2026-07-29 validation meeting; they supersede both
// development seeding) because no authoritative Product list exists in // SH-24's service-leaf list and the provisional prototype seed originally
// the repo. Product must confirm the list before release; after the // shipped by SH-249. Keep migrations and this catalog in sync.
// 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).
public static class TradeCatalog public static class TradeCatalog
{ {
public static readonly IReadOnlyList<string> CanonicalTrades = new[] public static readonly IReadOnlyList<string> CanonicalTrades = new[]
{ {
"Plumbing", "Plumbing & Water Systems",
"Electrical", "Electrical",
"Doors & Docks",
"Fire & Life Safety",
"Security & Locksmith",
"HVAC", "HVAC",
"Fire/Life Safety", "Exterior & Grounds",
"General", "Building Envelope",
"Roofing", "Interior Finishes",
"Signage",
"Conveyance & MHE",
"Janitorial", "Janitorial",
"Painting", "Specialty",
"Locksmith", "General Building & Handyman"
"Carpentry"
}; };
public static string Normalize(string name) => name.Trim().ToLowerInvariant(); public static string Normalize(string name) => name.Trim().ToLowerInvariant();

View file

@ -8,9 +8,8 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace SeaHavenIndustries.Tests; namespace SeaHavenIndustries.Tests;
// SH-249: provider-backed migration evidence for the Trades reference table. // SH-249/SH-247: provider-backed evidence for the Trades reference table and
// Applies the real migration operations (Up, then Down) against SQLite and // the client-confirmed replacement taxonomy.
// asserts the seeded canonical vocabulary matches TradeCatalog.
public class SH249TradesVocabularyMigrationTests public class SH249TradesVocabularyMigrationTests
{ {
[Fact] [Fact]
@ -24,7 +23,8 @@ public class SH249TradesVocabularyMigrationTests
.Options; .Options;
await using var context = new ApplicationDbContext(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 var seeded = await context.Trades
.OrderBy(t => t.SortOrder) .OrderBy(t => t.SortOrder)
@ -50,8 +50,10 @@ public class SH249TradesVocabularyMigrationTests
.Options; .Options;
await using var context = new ApplicationDbContext(options); await using var context = new ApplicationDbContext(options);
await ApplyMigrationAsync(context, "Up"); await ApplyMigrationAsync(context, new SH249_TradesVocabulary(), "Up");
await ApplyMigrationAsync(context, "Down"); 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(); await using var command = connection.CreateCommand();
command.CommandText = command.CommandText =
@ -64,12 +66,15 @@ public class SH249TradesVocabularyMigrationTests
// Mirrors Phase4SearchIndexesSqlServerTests: invoke the protected Up/Down // Mirrors Phase4SearchIndexesSqlServerTests: invoke the protected Up/Down
// body, translate through the configured provider's SQL generator, and // body, translate through the configured provider's SQL generator, and
// execute the produced commands against the live database. // 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 builder = new MigrationBuilder(context.Database.ProviderName!);
var operation = typeof(Migration).GetMethod(direction, BindingFlags.Instance | BindingFlags.NonPublic) var operation = typeof(Migration).GetMethod(direction, BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException($"Migration.{direction} not found."); ?? throw new InvalidOperationException($"Migration.{direction} not found.");
operation.Invoke(new SH249_TradesVocabulary(), [builder]); operation.Invoke(migration, [builder]);
var sqlGenerator = context.GetService<IMigrationsSqlGenerator>(); var sqlGenerator = context.GetService<IMigrationsSqlGenerator>();
// InsertData operations need model metadata for column type mapping. // InsertData operations need model metadata for column type mapping.