Files
budget-tracker/backend/app/services/category_service.py
T
2026-03-17 16:16:08 +00:00

135 lines
4.2 KiB
Python

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()