feat: backend core — models, auth, CRUD, tests

This commit is contained in:
Nox (OpenClaw)
2026-03-17 16:16:08 +00:00
parent d8c2048a9b
commit 21339d771d
35 changed files with 2161 additions and 1 deletions
@@ -0,0 +1,134 @@
> import uuid
> from fastapi import HTTPException, status
> from sqlalchemy import func, select
> from sqlalchemy.ext.asyncio import AsyncSession
> from app.models.category import Category, CategoryType
> from app.models.transaction import Transaction
> from app.schemas.category import CategoryCreate, CategoryUpdate
> from app.utils import utcnow
# Default categories created at registration
> _DEFAULT_CATEGORIES = [
> {"name": "Alimentation", "type": CategoryType.expense, "color": "#22c55e", "icon": "utensils"},
> {"name": "Transport", "type": CategoryType.expense, "color": "#3b82f6", "icon": "car"},
> {"name": "Logement", "type": CategoryType.expense, "color": "#f59e0b", "icon": "home"},
> {"name": "Santé", "type": CategoryType.expense, "color": "#ef4444", "icon": "heart-pulse"},
> {"name": "Loisirs", "type": CategoryType.expense, "color": "#a855f7", "icon": "gamepad-2"},
> {"name": "Divers", "type": CategoryType.expense, "color": "#6b7280", "icon": "package"},
> {"name": "Salaire", "type": CategoryType.income, "color": "#10b981", "icon": "briefcase"},
> {"name": "Freelance", "type": CategoryType.income, "color": "#06b6d4", "icon": "laptop"},
> {"name": "Remboursement", "type": CategoryType.income, "color": "#8b5cf6", "icon": "refresh-cw"},
> ]
> async def create_default_categories(
> session: AsyncSession, user_id: uuid.UUID
> ) -> list[Category]:
> """Create the default categories for a newly registered user."""
> categories = []
> for data in _DEFAULT_CATEGORIES:
> cat = Category(user_id=user_id, is_default=True, **data)
> session.add(cat)
> categories.append(cat)
> await session.flush()
! return categories
> async def list_categories(
> session: AsyncSession,
> user_id: uuid.UUID,
> *,
> type_filter: CategoryType | None = None,
> ) -> list[Category]:
> query = select(Category).where(
> Category.user_id == user_id,
> Category.deleted_at.is_(None),
> )
> if type_filter is not None:
> query = query.where(Category.type == type_filter)
> query = query.order_by(Category.name)
> result = await session.execute(query)
! return list(result.scalars().all())
> async def get_category(
> session: AsyncSession,
> user_id: uuid.UUID,
> category_id: uuid.UUID,
> ) -> Category:
> result = await session.execute(
> select(Category).where(
> Category.id == category_id,
> Category.user_id == user_id,
> Category.deleted_at.is_(None),
> )
> )
! cat = result.scalar_one_or_none()
! if cat is None:
! raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found")
! return cat
> async def create_category(
> session: AsyncSession,
> user_id: uuid.UUID,
> data: CategoryCreate,
> ) -> Category:
> cat = Category(
> user_id=user_id,
> name=data.name,
> type=data.type,
> color=data.color,
> icon=data.icon,
> is_default=False,
> )
> session.add(cat)
> await session.commit()
! await session.refresh(cat)
! return cat
> async def update_category(
> session: AsyncSession,
> user_id: uuid.UUID,
> category_id: uuid.UUID,
> data: CategoryUpdate,
> ) -> Category:
> cat = await get_category(session, user_id, category_id)
! if data.name is not None:
! cat.name = data.name
! if data.color is not None:
! cat.color = data.color
! if data.icon is not None:
! cat.icon = data.icon
! await session.commit()
! await session.refresh(cat)
! return cat
> async def delete_category(
> session: AsyncSession,
> user_id: uuid.UUID,
> category_id: uuid.UUID,
> ) -> None:
> cat = await get_category(session, user_id, category_id)
# Refuse deletion if active transactions exist
! count_result = await session.execute(
! select(func.count()).where(
! Transaction.category_id == category_id,
! Transaction.user_id == user_id,
! Transaction.deleted_at.is_(None),
! )
! )
! active_count = count_result.scalar_one()
! if active_count > 0:
! raise HTTPException(
! status_code=status.HTTP_409_CONFLICT,
! detail=f"Cannot delete category: {active_count} active transaction(s) are linked to it",
! )
! cat.deleted_at = utcnow()
! await session.commit()