Add unit-of-measure catalog foundation

Phase 1 of structured quantities + UoM. Introduces a global UnitOfMeasure
catalog (Code-keyed for stable backend lookup of canonical units like
"each") and FamilyUnitOfMeasure for family-scoped customs, mirroring the
product-catalog pattern. Endpoints expose the merged effective catalog
plus CRUD for family customs. Abbreviation uniqueness is enforced per
table at the DB layer and across tables at the API layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Josh Rogers
2026-05-12 21:17:30 -05:00
parent 6c8f0167e5
commit 559d80c104
13 changed files with 1763 additions and 0 deletions
@@ -137,6 +137,49 @@ namespace YesChef.Api.Migrations
b.ToTable("FamilyProductOverrides");
});
modelBuilder.Entity("YesChef.Api.Entities.FamilyUnitOfMeasure", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Abbreviation")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int>("Category")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("FamilyId")
.HasColumnType("integer");
b.Property<string>("PluralName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SingularName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<int>("SortOrder")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("FamilyId", "Abbreviation")
.IsUnique();
b.ToTable("FamilyUnitsOfMeasure");
});
modelBuilder.Entity("YesChef.Api.Entities.Invite", b =>
{
b.Property<int>("Id")
@@ -566,6 +609,57 @@ namespace YesChef.Api.Migrations
b.ToTable("StoreSections");
});
modelBuilder.Entity("YesChef.Api.Entities.UnitOfMeasure", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Abbreviation")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<int>("Category")
.HasColumnType("integer");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsBase")
.HasColumnType("boolean");
b.Property<string>("PluralName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SingularName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<int>("SortOrder")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Abbreviation")
.IsUnique();
b.HasIndex("Code")
.IsUnique();
b.ToTable("UnitsOfMeasure");
});
modelBuilder.Entity("YesChef.Api.Entities.User", b =>
{
b.Property<int>("Id")
@@ -654,6 +748,17 @@ namespace YesChef.Api.Migrations
b.Navigation("Product");
});
modelBuilder.Entity("YesChef.Api.Entities.FamilyUnitOfMeasure", b =>
{
b.HasOne("YesChef.Api.Entities.Family", "Family")
.WithMany()
.HasForeignKey("FamilyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Family");
});
modelBuilder.Entity("YesChef.Api.Entities.Invite", b =>
{
b.HasOne("YesChef.Api.Entities.User", "ConsumedByUser")