413 lines
12 KiB
Python
413 lines
12 KiB
Python
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)
|