Filter unit dropdown by product allowed-unit categories
Adds a UnitCategoryFlags column to Product, FamilyProduct, and FamilyProductOverride so each product can advertise which unit categories it is typically packaged by (e.g. flour: Weight | Volume). The product endpoints round-trip the flag, search projects the effective value with the override applied, and the frontend QuantityInput soft-filters its dropdown by the selected product's flag, with a "show all units" escape hatch for ad-hoc overrides. No backend rejection on a unit outside the allowed set — the flag is purely a hint. Default value is None (no filter), so existing data is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -247,6 +247,115 @@ public class ProductEndpointsTests : AuthenticatedIntegrationTest
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Create_family_product_persists_allowed_unit_categories()
|
||||
{
|
||||
var response = await Client.PostAsJsonAsync("/api/products",
|
||||
new ProductEndpoints.CreateProductRequest("House Flour", null, null,
|
||||
UnitCategoryFlags.Weight | UnitCategoryFlags.Volume));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created);
|
||||
var stored = await UseDbAsync(db => db.FamilyProducts.SingleAsync());
|
||||
await Assert.That(stored.AllowedUnitCategories)
|
||||
.IsEqualTo(UnitCategoryFlags.Weight | UnitCategoryFlags.Volume);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_family_product_changes_allowed_unit_categories()
|
||||
{
|
||||
var familyId = await GetFamilyIdAsync();
|
||||
var product = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new FamilyProduct
|
||||
{
|
||||
FamilyId = familyId,
|
||||
Name = "Flour",
|
||||
AllowedUnitCategories = UnitCategoryFlags.Weight,
|
||||
};
|
||||
db.FamilyProducts.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var response = await Client.PutAsJsonAsync($"/api/products/family/{product.Id}",
|
||||
new ProductEndpoints.UpdateProductRequest(null, null, null,
|
||||
UnitCategoryFlags.Weight | UnitCategoryFlags.Volume));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
|
||||
var stored = await UseDbAsync(db => db.FamilyProducts.SingleAsync());
|
||||
await Assert.That(stored.AllowedUnitCategories)
|
||||
.IsEqualTo(UnitCategoryFlags.Weight | UnitCategoryFlags.Volume);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_global_product_writes_allowed_unit_categories_to_override()
|
||||
{
|
||||
var apples = await UseDbAsync(async db =>
|
||||
{
|
||||
var p = new Product
|
||||
{
|
||||
Name = "Milk",
|
||||
AllowedUnitCategories = UnitCategoryFlags.Volume,
|
||||
};
|
||||
db.Products.Add(p);
|
||||
await db.SaveChangesAsync();
|
||||
return p;
|
||||
});
|
||||
|
||||
var response = await Client.PutAsJsonAsync($"/api/products/global/{apples.Id}",
|
||||
new ProductEndpoints.UpdateProductRequest(null, null, null,
|
||||
UnitCategoryFlags.Volume | UnitCategoryFlags.Packaging));
|
||||
|
||||
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
|
||||
var ovr = await UseDbAsync(db => db.FamilyProductOverrides.SingleAsync());
|
||||
await Assert.That(ovr.AllowedUnitCategories)
|
||||
.IsEqualTo(UnitCategoryFlags.Volume | UnitCategoryFlags.Packaging);
|
||||
|
||||
// The global row remains unchanged.
|
||||
var global = await UseDbAsync(db => db.Products.SingleAsync(p => p.Id == apples.Id));
|
||||
await Assert.That(global.AllowedUnitCategories).IsEqualTo(UnitCategoryFlags.Volume);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Search_projects_effective_allowed_unit_categories()
|
||||
{
|
||||
var familyId = await GetFamilyIdAsync();
|
||||
await UseDbAsync(async db =>
|
||||
{
|
||||
db.Products.Add(new Product { Name = "Milk", AllowedUnitCategories = UnitCategoryFlags.Volume });
|
||||
db.Products.Add(new Product { Name = "Eggs", AllowedUnitCategories = UnitCategoryFlags.Count });
|
||||
db.FamilyProducts.Add(new FamilyProduct
|
||||
{
|
||||
FamilyId = familyId,
|
||||
Name = "House Flour",
|
||||
AllowedUnitCategories = UnitCategoryFlags.Weight,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Override Eggs to widen to Count | Packaging for this family.
|
||||
var eggs = db.Products.Single(p => p.Name == "Eggs");
|
||||
db.FamilyProductOverrides.Add(new FamilyProductOverride
|
||||
{
|
||||
FamilyId = familyId,
|
||||
ProductId = eggs.Id,
|
||||
AllowedUnitCategories = UnitCategoryFlags.Count | UnitCategoryFlags.Packaging,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var results = await Client.GetFromJsonAsync<List<ProductEndpoints.ProductDto>>("/api/products?q=");
|
||||
|
||||
var milk = results!.Single(r => r.Name == "Milk");
|
||||
await Assert.That(milk.AllowedUnitCategories).IsEqualTo(UnitCategoryFlags.Volume);
|
||||
|
||||
var eggs = results.Single(r => r.Name == "Eggs");
|
||||
await Assert.That(eggs.AllowedUnitCategories)
|
||||
.IsEqualTo(UnitCategoryFlags.Count | UnitCategoryFlags.Packaging);
|
||||
|
||||
var flour = results.Single(r => r.Name == "House Flour");
|
||||
await Assert.That(flour.AllowedUnitCategories).IsEqualTo(UnitCategoryFlags.Weight);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Endpoints_require_authentication()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user