fix(pantry): make public ingredient endpoint idempotent
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled

Backend:
- POST /api/ingredients now checks name_lower and aliases before inserting
- Returns existing ingredient on 409 instead of throwing error

Frontend:
- Removed fragile 409-recovery logic from Pantry.tsx handleAdd
- Added aliases field to Ingredient type for case-insensitive matching

Fixes pantry add for ingredients like 'Carrots' whose canonical name is 'Carrot'
This commit is contained in:
2026-05-18 17:16:57 -07:00
parent 54b3e785b3
commit 69acd70188
4 changed files with 39 additions and 8 deletions
+29 -1
View File
@@ -57,9 +57,24 @@ def get_ingredient(ingredient_id: UUID, db: Session = Depends(get_db)) -> Ingred
@public_router.post("", response_model=IngredientRead, status_code=status.HTTP_201_CREATED)
def create_ingredient_public(payload: IngredientCreate, db: Session = Depends(get_db)) -> Ingredient:
name_lower = payload.name.lower()
# idempotent: return existing if name or alias already known
existing = (
db.query(Ingredient)
.filter(
or_(
Ingredient.name_lower == name_lower,
Ingredient.aliases.any(payload.name.lower()),
)
)
.first()
)
if existing:
return existing
row = Ingredient(
name=payload.name,
name_lower=payload.name.lower(),
name_lower=name_lower,
aliases=payload.aliases,
aisle=payload.aisle,
unit=payload.unit,
@@ -70,6 +85,19 @@ def create_ingredient_public(payload: IngredientCreate, db: Session = Depends(ge
db.commit()
except IntegrityError:
db.rollback()
# race-window safety: re-check after rollback
existing = (
db.query(Ingredient)
.filter(
or_(
Ingredient.name_lower == name_lower,
Ingredient.aliases.any(payload.name.lower()),
)
)
.first()
)
if existing:
return existing
raise HTTPException(status_code=409, detail="ingredient name already exists")
db.refresh(row)
return row