from __future__ import annotations from datetime import date from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from api.config import settings from api.deps import AuthUser, require_api_token, require_user from api.schemas import ( AuthSessionOut, AuthUserOut, BudgetActiveIn, BudgetCreateIn, BudgetOut, BudgetStatusOut, BudgetUpdateIn, BudgetUpsertIn, BudgetsListOut, ExpenseCreateIn, ExpenseOut, ExpensesPageOut, InternalAuthIn, TelegramLoginIn, UndoExpenseOut, ) from api.telegram_auth import TelegramAuthError, verify_telegram_login from api.tokens import create_access_token from bot.services.budget import BudgetService, BudgetStatus, PeriodExpensesPage router = APIRouter(prefix="/api") def _status_out(status_data: BudgetStatus) -> BudgetStatusOut: b = status_data.budget return BudgetStatusOut( budget=BudgetOut( id=b.id, user_id=b.user_id, name=b.name, total_amount=b.total_amount, start_date=b.start_date, end_date=b.end_date, currency=b.currency, is_active=b.is_active, ), today=status_data.today, days_left=status_data.days_left, total_spent=status_data.total_spent, remaining=status_data.remaining, daily_limit=status_data.daily_limit, spent_today=status_data.spent_today, remaining_today=status_data.remaining_today, is_over_daily=status_data.is_over_daily, is_over_budget=status_data.is_over_budget, is_expired=status_data.is_expired, selected=status_data.selected, ) def _page_out(page: PeriodExpensesPage) -> ExpensesPageOut: return ExpensesPageOut( page=page.page, total_pages=page.total_pages, total_count=page.total_count, total_sum=page.total_sum, page_size=page.page_size, budget_id=page.budget.id, items=[ ExpenseOut( id=item.id, budget_id=item.budget_id, amount=item.amount, note=item.note, spent_at=item.spent_at, ) for item in page.items ], ) def _service(request: Request) -> BudgetService: return request.app.state.budget_service @router.post("/auth/telegram", response_model=AuthSessionOut) async def auth_telegram(body: TelegramLoginIn) -> AuthSessionOut: try: profile = verify_telegram_login( body.model_dump(), settings.bot_token, max_age_seconds=settings.telegram_auth_max_age_seconds, ) except TelegramAuthError as exc: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc), ) from exc token = create_access_token(user_id=profile["id"], profile=profile) return AuthSessionOut( access_token=token, user=AuthUserOut( user_id=profile["id"], first_name=profile.get("first_name"), last_name=profile.get("last_name"), username=profile.get("username"), photo_url=profile.get("photo_url"), ), ) @router.post( "/auth/internal", response_model=AuthSessionOut, dependencies=[Depends(require_api_token)], ) async def auth_internal(body: InternalAuthIn) -> AuthSessionOut: profile = { "id": body.user_id, "first_name": body.first_name, "last_name": body.last_name, "username": body.username, } token = create_access_token(user_id=body.user_id, profile=profile) return AuthSessionOut( access_token=token, user=AuthUserOut( user_id=body.user_id, first_name=body.first_name, last_name=body.last_name, username=body.username, ), ) @router.get("/me", response_model=AuthUserOut) async def me(user: AuthUser = Depends(require_user)) -> AuthUserOut: return AuthUserOut( user_id=user.user_id, first_name=user.first_name, last_name=user.last_name, username=user.username, ) @router.get("/me/budgets", response_model=BudgetsListOut) async def my_budgets( request: Request, user: AuthUser = Depends(require_user), ) -> BudgetsListOut: service = _service(request) items = await service.list_user_statuses(user.user_id) return BudgetsListOut(items=[_status_out(item) for item in items]) @router.post("/me/budgets", response_model=BudgetStatusOut) async def create_my_budget( body: BudgetCreateIn, request: Request, user: AuthUser = Depends(require_user), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.create_budget( user_id=user.user_id, total_amount=body.total_amount, end_date=body.end_date, name=body.name, is_active=body.is_active, select=body.select, ) except ValueError as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc), ) from exc return _status_out(status_data) @router.put("/me/budgets/{budget_id}", response_model=BudgetStatusOut) async def update_my_budget( budget_id: int, body: BudgetUpdateIn, request: Request, user: AuthUser = Depends(require_user), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.update_budget( user.user_id, budget_id, name=body.name, total_amount=body.total_amount, end_date=body.end_date, reset_expenses=body.reset_expenses, ) except ValueError as exc: code = ( status.HTTP_404_NOT_FOUND if "не найден" in str(exc).lower() else status.HTTP_400_BAD_REQUEST ) raise HTTPException(status_code=code, detail=str(exc)) from exc return _status_out(status_data) @router.patch("/me/budgets/{budget_id}/active", response_model=BudgetStatusOut) async def set_my_budget_active( budget_id: int, body: BudgetActiveIn, request: Request, user: AuthUser = Depends(require_user), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.set_budget_active( user.user_id, budget_id, body.is_active, ) except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc return _status_out(status_data) @router.post("/me/budgets/{budget_id}/select", response_model=BudgetStatusOut) async def select_my_budget( budget_id: int, request: Request, user: AuthUser = Depends(require_user), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.select_budget(user.user_id, budget_id) except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc return _status_out(status_data) @router.get("/me/budget", response_model=BudgetStatusOut) async def my_budget( request: Request, user: AuthUser = Depends(require_user), budget_id: int | None = Query(default=None), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.get_status(user.user_id, budget_id=budget_id) except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc return _status_out(status_data) @router.get("/me/expenses", response_model=ExpensesPageOut) async def my_expenses( request: Request, user: AuthUser = Depends(require_user), page: int = Query(default=0, ge=0), page_size: int = Query(default=20, ge=1, le=100), spent_at: date | None = Query(default=None), budget_id: int | None = Query(default=None), ) -> ExpensesPageOut: service = _service(request) try: if spent_at is not None: page_data = await service.get_expenses_on_date_page( user.user_id, spent_at, page=page, page_size=page_size, budget_id=budget_id, ) else: page_data = await service.get_period_expenses_page( user.user_id, page=page, page_size=page_size, budget_id=budget_id, ) except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc return _page_out(page_data) @router.post("/me/expenses", response_model=BudgetStatusOut) async def create_my_expense( body: ExpenseCreateIn, request: Request, user: AuthUser = Depends(require_user), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.add_expense( user_id=user.user_id, amount=body.amount, note=body.note, spent_at=body.spent_at, budget_id=body.budget_id, ) except ValueError as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc), ) from exc return _status_out(status_data) @router.delete("/me/expenses/last", response_model=UndoExpenseOut) async def undo_my_last_expense( request: Request, user: AuthUser = Depends(require_user), budget_id: int | None = Query(default=None), ) -> UndoExpenseOut: service = _service(request) status_data, amount = await service.undo_last_expense( user.user_id, budget_id=budget_id, ) if amount is None or status_data is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Нечего отменять", ) return UndoExpenseOut(deleted_amount=amount, status=_status_out(status_data)) @router.put("/me/budget", response_model=BudgetStatusOut) async def upsert_my_budget( body: BudgetUpsertIn, request: Request, user: AuthUser = Depends(require_user), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.set_budget( user_id=user.user_id, total_amount=body.total_amount, end_date=body.end_date, reset_expenses=body.reset_expenses, name=body.name, budget_id=body.budget_id, ) except ValueError as exc: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc), ) from exc return _status_out(status_data) @router.get( "/budgets", response_model=BudgetsListOut, dependencies=[Depends(require_api_token)], ) async def list_budgets(request: Request) -> BudgetsListOut: service = _service(request) items = await service.list_budget_summaries() return BudgetsListOut(items=[_status_out(item) for item in items]) @router.get( "/budgets/{user_id}", response_model=BudgetStatusOut, dependencies=[Depends(require_api_token)], ) async def get_budget( user_id: int, request: Request, budget_id: int | None = Query(default=None), ) -> BudgetStatusOut: service = _service(request) try: status_data = await service.get_status(user_id, budget_id=budget_id) except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc return _status_out(status_data) @router.get( "/budgets/{user_id}/expenses", response_model=ExpensesPageOut, dependencies=[Depends(require_api_token)], ) async def list_expenses( user_id: int, request: Request, page: int = Query(default=0, ge=0), page_size: int = Query(default=20, ge=1, le=100), spent_at: date | None = Query(default=None), budget_id: int | None = Query(default=None), ) -> ExpensesPageOut: service = _service(request) try: if spent_at is not None: page_data = await service.get_expenses_on_date_page( user_id, spent_at, page=page, page_size=page_size, budget_id=budget_id, ) else: page_data = await service.get_period_expenses_page( user_id, page=page, page_size=page_size, budget_id=budget_id, ) except ValueError as exc: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc return _page_out(page_data)