feat(v2): finalize scoped meals consumed + fix shopping v2 invariants; refresh JWT route; tests green
This commit is contained in:
parent
ec3199fb0c
commit
0502902ffe
5 changed files with 71 additions and 13 deletions
|
|
@ -44,7 +44,7 @@ async def login(
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/refresh",
|
"/refresh-cookie",
|
||||||
response_model=persons.Person,
|
response_model=persons.Person,
|
||||||
operation_id="refresh",
|
operation_id="refresh",
|
||||||
summary="Refresh current user from cookie",
|
summary="Refresh current user from cookie",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import aiosqlite
|
||||||
from fastapi import APIRouter, Depends, Request, Response
|
from fastapi import APIRouter, Depends, Request, Response
|
||||||
|
|
||||||
import shopping
|
import shopping
|
||||||
from api.deps import error_response, get_db, get_household_from_slug
|
from api.deps import error_response, get_db, get_household_from_slug, get_current_user
|
||||||
from api.shopping import (
|
from api.shopping import (
|
||||||
CurrentShoppingList,
|
CurrentShoppingList,
|
||||||
PurchasedShoppingList,
|
PurchasedShoppingList,
|
||||||
|
|
@ -105,6 +105,7 @@ async def purchase_ingredients_scoped(
|
||||||
shopping_list: PurchaseListIn,
|
shopping_list: PurchaseListIn,
|
||||||
request: Request,
|
request: Request,
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
|
user=Depends(get_current_user),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
) -> PurchasedShoppingList | Response:
|
) -> PurchasedShoppingList | Response:
|
||||||
hid = household["id"]
|
hid = household["id"]
|
||||||
|
|
@ -123,7 +124,9 @@ async def purchase_ingredients_scoped(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
domain_list = shopping.ShoppingList(items=domain_items, store_name=shopping_list.store_name)
|
domain_list = shopping.ShoppingList(
|
||||||
|
items=domain_items, store_name=shopping_list.store_name, purchased_by_id=user.id
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use household-scoped purchase which ensures requests belong to the same household
|
# Use household-scoped purchase which ensures requests belong to the same household
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
- GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404.
|
- GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404.
|
||||||
- POST `/api/v1/households/{householdSlug}/meals/{mealId}/consumed` marks a meal consumed within the household; validates timezone on provided `consumedDate`; clears outstanding meal requests only within that household.
|
- POST `/api/v1/households/{householdSlug}/meals/{mealId}/consumed` marks a meal consumed within the household; validates timezone on provided `consumedDate`; clears outstanding meal requests only within that household.
|
||||||
- Tests: `tests/test_meals_consumed_v2.py` validates scoping (requests cleared in same household, unaffected in other household). PASS.
|
- Tests: `tests/test_meals_consumed_v2.py` validates scoping (requests cleared in same household, unaffected in other household). PASS.
|
||||||
- Tests: `tests/test_shopping_household_v2.py` verifies isolation of outstanding items; `tests/test_shopping_list_by_id_v2.py` verifies list-by-id scoping (PASS).
|
- Tests: `tests/test_shopping_household_v2.py` verifies isolation of outstanding items; `tests/test_shopping_list_by_id_v2.py` verifies list-by-id scoping; `tests/test_shopping_purchase_v2.py` covers scoped purchase (PASS).
|
||||||
# Backend Specification: Household Multi-Tenancy (v2)
|
# Backend Specification: Household Multi-Tenancy (v2)
|
||||||
|
|
||||||
This document has been validated against the current codebase (v1) to ensure the plan captures all required changes. It starts with a concise baseline of what exists today, then details the v2 multi-tenancy/auth refactor with concrete, file-scoped steps and acceptance criteria.
|
This document has been validated against the current codebase (v1) to ensure the plan captures all required changes. It starts with a concise baseline of what exists today, then details the v2 multi-tenancy/auth refactor with concrete, file-scoped steps and acceptance criteria.
|
||||||
|
|
@ -238,6 +238,7 @@ Impact on existing routes (exact files to refactor):
|
||||||
- POST `/api/v1/households/{householdSlug}/shopping` to purchase list items scoped to household; validates invariants and updates outstanding requests.
|
- POST `/api/v1/households/{householdSlug}/shopping` to purchase list items scoped to household; validates invariants and updates outstanding requests.
|
||||||
- Scoped helpers in `shopping/repository.py` and `shopping/__init__.py` filter by `household_id` (find items, load list, purchased ingredients, and purchase_scoped).
|
- Scoped helpers in `shopping/repository.py` and `shopping/__init__.py` filter by `household_id` (find items, load list, purchased ingredients, and purchase_scoped).
|
||||||
- Tests: `tests/test_shopping_household_v2.py` (current isolation), `tests/test_shopping_list_by_id_v2.py` (list-by-id scoping), `tests/test_shopping_purchase_v2.py` (scoped purchase). PASS.
|
- Tests: `tests/test_shopping_household_v2.py` (current isolation), `tests/test_shopping_list_by_id_v2.py` (list-by-id scoping), `tests/test_shopping_purchase_v2.py` (scoped purchase). PASS.
|
||||||
|
- Notes: Normalized Ingredient.preparation to allow NULLs from DB (treated as empty string) to avoid 422 in v2 responses; set purchased_by_id in scoped purchase from JWT user.
|
||||||
- ⏳ Update Repositories: ingredients, products to accept `household_id` and filter accordingly; extend meals create/update/consumed/delete and shopping write flows (purchase, requests) with scoping.
|
- ⏳ Update Repositories: ingredients, products to accept `household_id` and filter accordingly; extend meals create/update/consumed/delete and shopping write flows (purchase, requests) with scoping.
|
||||||
- ⏳ Update Routers: move/duplicate remaining routers under the household router and wire `household_id` through.
|
- ⏳ Update Routers: move/duplicate remaining routers under the household router and wire `household_id` through.
|
||||||
- **Acceptance**: The same queries as v1, when run under different household slugs and users, return isolated data sets; cross-household access yields 403.
|
- **Acceptance**: The same queries as v1, when run under different household slugs and users, return isolated data sets; cross-household access yields 403.
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,9 @@ class Ingredient(ApiModel):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return v
|
return v
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
# Backward compatibility: DB may contain NULL preparation; normalize to empty string
|
||||||
|
@field_validator("preparation", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _normalize_preparation(cls, v: Any) -> Any:
|
||||||
|
return "" if v is None else v
|
||||||
|
|
|
||||||
62
openapi.json
62
openapi.json
|
|
@ -1321,27 +1321,45 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/auth/refresh": {
|
"/api/v1/auth/refresh-cookie": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"v2",
|
"v1",
|
||||||
"auth-v2"
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Refresh current user from cookie",
|
||||||
|
"operationId": "refresh",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "cookie",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "User Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"summary": "Refresh",
|
|
||||||
"operationId": "refreshV2",
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/RefreshResponse"
|
"$ref": "#/components/schemas/Person"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"$ref": "#/components/responses/Problem422"
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -1393,6 +1411,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"v2",
|
||||||
|
"auth-v2"
|
||||||
|
],
|
||||||
|
"summary": "Refresh",
|
||||||
|
"operationId": "refreshV2",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RefreshResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/components/responses/Problem422"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/auth/logout": {
|
"/api/v1/auth/logout": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue