from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.orm import Session from .. import models from ..db import get_session from ..schemas import ( CategoryCreate, CategoryRead, CategoryUpdate, TagCreate, TagRead, TagUpdate, ) router = APIRouter(tags=["taxonomy"]) def _require_household(session: Session, household_id: str) -> None: if not session.get(models.Household, household_id): raise HTTPException(status_code=404, detail="Household not found") def _category_path( session: Session, payload: CategoryCreate | CategoryUpdate, category: models.Category | None = None, ) -> str: if payload.path: return payload.path name = payload.name if payload.name is not None else category.name if category else "" parent_id = payload.parent_id if payload.parent_id is not None else category.parent_id if category else None if parent_id: parent = session.get(models.Category, parent_id) if not parent: raise HTTPException(status_code=404, detail="Parent category not found") return f"{parent.path}/{name}" return name @router.post("/categories", response_model=CategoryRead) def create_category( payload: CategoryCreate, session: Session = Depends(get_session), ) -> models.Category: _require_household(session, payload.household_id) if payload.parent_id: parent = session.get(models.Category, payload.parent_id) if not parent or parent.household_id != payload.household_id: raise HTTPException(status_code=404, detail="Parent category not found") category = models.Category( household_id=payload.household_id, parent_id=payload.parent_id, name=payload.name, path=_category_path(session, payload), is_active=payload.is_active, ) session.add(category) session.commit() session.refresh(category) return category @router.get("/categories", response_model=list[CategoryRead]) def list_categories( household_id: str, include_inactive: bool = False, session: Session = Depends(get_session), ) -> list[models.Category]: query = ( select(models.Category) .where(models.Category.household_id == household_id) .order_by(models.Category.path) ) if not include_inactive: query = query.where(models.Category.is_active.is_(True)) return list(session.scalars(query).all()) @router.patch("/categories/{category_id}", response_model=CategoryRead) def update_category( category_id: str, payload: CategoryUpdate, session: Session = Depends(get_session), ) -> models.Category: category = session.get(models.Category, category_id) if not category: raise HTTPException(status_code=404, detail="Category not found") if payload.parent_id: parent = session.get(models.Category, payload.parent_id) if not parent or parent.household_id != category.household_id: raise HTTPException(status_code=404, detail="Parent category not found") for field, value in payload.model_dump(exclude_unset=True, exclude={"path"}).items(): setattr(category, field, value) if {"name", "parent_id", "path"} & payload.model_fields_set: category.path = _category_path(session, payload, category) session.add(category) session.commit() session.refresh(category) return category @router.delete("/categories/{category_id}") def delete_category( category_id: str, session: Session = Depends(get_session), ) -> dict[str, bool]: category = session.get(models.Category, category_id) if not category: raise HTTPException(status_code=404, detail="Category not found") for event in session.scalars( select(models.FinancialEvent).where(models.FinancialEvent.category_id == category_id) ): event.category_id = None for commitment in session.scalars( select(models.RecurringCommitment).where(models.RecurringCommitment.category_id == category_id) ): commitment.category_id = None for budget_line in session.scalars( select(models.ProjectBudgetLine).where(models.ProjectBudgetLine.category_id == category_id) ): budget_line.category_id = None for child in session.scalars(select(models.Category).where(models.Category.parent_id == category_id)): child.parent_id = None child.path = child.name session.delete(category) session.commit() return {"deleted": True} @router.post("/tags", response_model=TagRead) def create_tag( payload: TagCreate, session: Session = Depends(get_session), ) -> models.Tag: _require_household(session, payload.household_id) tag = models.Tag(household_id=payload.household_id, name=payload.name) session.add(tag) session.commit() session.refresh(tag) return tag @router.get("/tags", response_model=list[TagRead]) def list_tags( household_id: str, session: Session = Depends(get_session), ) -> list[models.Tag]: return list( session.scalars( select(models.Tag).where(models.Tag.household_id == household_id).order_by(models.Tag.name) ).all() ) @router.patch("/tags/{tag_id}", response_model=TagRead) def update_tag( tag_id: str, payload: TagUpdate, session: Session = Depends(get_session), ) -> models.Tag: tag = session.get(models.Tag, tag_id) if not tag: raise HTTPException(status_code=404, detail="Tag not found") for field, value in payload.model_dump(exclude_unset=True).items(): setattr(tag, field, value) session.add(tag) session.commit() session.refresh(tag) return tag @router.delete("/tags/{tag_id}") def delete_tag( tag_id: str, session: Session = Depends(get_session), ) -> dict[str, bool]: tag = session.get(models.Tag, tag_id) if not tag: raise HTTPException(status_code=404, detail="Tag not found") session.query(models.EventTag).filter(models.EventTag.tag_id == tag_id).delete( synchronize_session=False ) session.delete(tag) session.commit() return {"deleted": True}