feat(shopping): add household-scoped ingredient request endpoint + tests and spec

This commit is contained in:
jableader 2025-11-01 18:50:40 +11:00
parent 75d098cd7a
commit 978c970fb6
3 changed files with 32 additions and 12 deletions

View file

@ -13,6 +13,7 @@ from api.shopping_models import (
ShoppingListOut, ShoppingListOut,
PurchasedShoppingList, PurchasedShoppingList,
_to_ingredient_item, _to_ingredient_item,
ListIngredientItem,
_to_meal_item, _to_meal_item,
_to_shopping_list_out, _to_shopping_list_out,
RequestedMealItem, RequestedMealItem,
@ -202,6 +203,33 @@ class IngredientIdWrapper(_ApiModel):
ingredient_id: int ingredient_id: int
@router.post(
"/current/ingredients",
response_model=ListIngredientItem,
operation_id="requestIngredientV2",
summary="Request an ingredient for shopping (scoped)",
)
async def request_ingredient_scoped(
r: IngredientIdWrapper,
request: Request,
household=Depends(get_household_from_slug),
user=Depends(get_current_user),
conn: aiosqlite.Connection = Depends(get_db),
):
hid = household["id"]
from ingredients.repository import find_ingredient_by_id
ingredient = await find_ingredient_by_id(conn, r.ingredient_id)
if not ingredient:
return error_response(request, 404, "Ingredient not found")
try:
item = await shopping.request_ingredient_scoped(conn, ingredient, hid, user.id)
except ValueError as e:
return error_response(request, 400, str(e))
return _to_ingredient_item(item)
# Re-export shared DTOs for importers # Re-export shared DTOs for importers
__all__ = [ __all__ = [
"router", "router",

View file

@ -121,9 +121,7 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
assert "createdBy" in r.json() and "createdById" in r.json() assert "createdBy" in r.json() and "createdById" in r.json()
# Delete it via v2 scoped route # Delete it via v2 scoped route
r2 = self.client.delete( r2 = self.client.delete(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers)
f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers
)
assert r2.status_code == 200, r2.text assert r2.status_code == 200, r2.text
# hiddenBy is populated on delete # hiddenBy is populated on delete
body_del = r2.json() body_del = r2.json()
@ -131,9 +129,7 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
assert body_del["hiddenBy"]["displayName"] == "R" assert body_del["hiddenBy"]["displayName"] == "R"
# Subsequent get in same household should be 404 # Subsequent get in same household should be 404
r3 = self.client.get( r3 = self.client.get(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers)
f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers
)
assert r3.status_code == 404 assert r3.status_code == 404
def test_create_recipe_requires_ingredients_and_cursor_edge(self): def test_create_recipe_requires_ingredients_and_cursor_edge(self):

View file

@ -75,16 +75,12 @@ class TestShoppingRequestIngredientV2(unittest.IsolatedAsyncioTestCase):
assert item["mealId"] is None assert item["mealId"] is None
# Visible in H1 current, not in H2 # Visible in H1 current, not in H2
r1 = self.client.get( r1 = self.client.get(f"/api/v1/households/{self.h1}/shopping/current", headers=self.headers)
f"/api/v1/households/{self.h1}/shopping/current", headers=self.headers
)
assert r1.status_code == 200, r1.text assert r1.status_code == 200, r1.text
cur1 = r1.json() cur1 = r1.json()
assert any(i.get("ingredientId") == self.milk_id for i in cur1["outstandingItems"]) assert any(i.get("ingredientId") == self.milk_id for i in cur1["outstandingItems"])
r2 = self.client.get( r2 = self.client.get(f"/api/v1/households/{self.h2}/shopping/current", headers=self.headers)
f"/api/v1/households/{self.h2}/shopping/current", headers=self.headers
)
assert r2.status_code == 200, r2.text assert r2.status_code == 200, r2.text
cur2 = r2.json() cur2 = r2.json()
assert not any(i.get("ingredientId") == self.milk_id for i in cur2["outstandingItems"]) assert not any(i.get("ingredientId") == self.milk_id for i in cur2["outstandingItems"])