From 978c970fb6fcaa16f1b5ddf3e9e9104b68848123 Mon Sep 17 00:00:00 2001 From: jableader Date: Sat, 1 Nov 2025 18:50:40 +1100 Subject: [PATCH] feat(shopping): add household-scoped ingredient request endpoint + tests and spec --- api/shopping.py | 28 ++++++++++++++++++++ tests/test_recipes_household_v2.py | 8 ++---- tests/test_shopping_request_ingredient_v2.py | 8 ++---- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/api/shopping.py b/api/shopping.py index 2f8e96d..fc9fa41 100644 --- a/api/shopping.py +++ b/api/shopping.py @@ -13,6 +13,7 @@ from api.shopping_models import ( ShoppingListOut, PurchasedShoppingList, _to_ingredient_item, + ListIngredientItem, _to_meal_item, _to_shopping_list_out, RequestedMealItem, @@ -202,6 +203,33 @@ class IngredientIdWrapper(_ApiModel): 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 __all__ = [ "router", diff --git a/tests/test_recipes_household_v2.py b/tests/test_recipes_household_v2.py index ed6f5c8..317d0e1 100644 --- a/tests/test_recipes_household_v2.py +++ b/tests/test_recipes_household_v2.py @@ -121,9 +121,7 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase): assert "createdBy" in r.json() and "createdById" in r.json() # Delete it via v2 scoped route - r2 = self.client.delete( - f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers - ) + r2 = self.client.delete(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers) assert r2.status_code == 200, r2.text # hiddenBy is populated on delete body_del = r2.json() @@ -131,9 +129,7 @@ class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase): assert body_del["hiddenBy"]["displayName"] == "R" # Subsequent get in same household should be 404 - r3 = self.client.get( - f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers - ) + r3 = self.client.get(f"/api/v1/households/{self.h1}/recipes/{rid}", headers=self.headers) assert r3.status_code == 404 def test_create_recipe_requires_ingredients_and_cursor_edge(self): diff --git a/tests/test_shopping_request_ingredient_v2.py b/tests/test_shopping_request_ingredient_v2.py index 50b13bf..a87fac1 100644 --- a/tests/test_shopping_request_ingredient_v2.py +++ b/tests/test_shopping_request_ingredient_v2.py @@ -75,16 +75,12 @@ class TestShoppingRequestIngredientV2(unittest.IsolatedAsyncioTestCase): assert item["mealId"] is None # Visible in H1 current, not in H2 - r1 = self.client.get( - f"/api/v1/households/{self.h1}/shopping/current", headers=self.headers - ) + r1 = self.client.get(f"/api/v1/households/{self.h1}/shopping/current", headers=self.headers) assert r1.status_code == 200, r1.text cur1 = r1.json() assert any(i.get("ingredientId") == self.milk_id for i in cur1["outstandingItems"]) - r2 = self.client.get( - f"/api/v1/households/{self.h2}/shopping/current", headers=self.headers - ) + r2 = self.client.get(f"/api/v1/households/{self.h2}/shopping/current", headers=self.headers) assert r2.status_code == 200, r2.text cur2 = r2.json() assert not any(i.get("ingredientId") == self.milk_id for i in cur2["outstandingItems"])