"""API настроек пользователя (валюта бюджетов и язык UI).
Per-user (ТЗ 1.2): у каждого пользователя свои настройки; строка с PK
(user_id, key). Настройки прежней эпохи (app_settings_global) копируются
в per-user при первом логине (claim) — см. services/users.py.
"""
from typing import cast
from fastapi import APIRouter
from app.dependencies import DbDep, UserIdDep
from app.models import AppSetting
from app.realtime import publish
from app.schemas import CURRENCIES, LANGUAGES, AppSettingsOut, AppSettingsUpdate
from app.services import reminders
router = APIRouter(prefix="/api/settings", tags=["settings"])
CURRENCY_KEY = "currency"
LANGUAGE_KEY = "language"
# Тихие часы напоминаний (0.89): окно «HH:MM-HH:MM», пусто — не заданы
QUIET_HOURS_KEY = "quiet_hours"
DEFAULT_CURRENCY = "UAH"
DEFAULT_LANGUAGE = "" # '' = нет переопределения, следуем SSO locale
DEFAULT_QUIET_HOURS = ""
def _read_value(db: DbDep, user_id: str, key: str, valid: tuple[str, ...], default: str) -> str:
row = cast(AppSetting | None, db.get(AppSetting, (user_id, key)))
if row is not None and row.value in valid:
return row.value
return default
def _upsert(db: DbDep, user_id: str, key: str, value: str) -> None:
row = cast(AppSetting | None, db.get(AppSetting, (user_id, key)))
if row is None:
db.add(AppSetting(user_id=user_id, key=key, value=value))
else:
row.value = value
db.flush()
def _read_quiet_hours(db: DbDep, user_id: str) -> str:
"""Тихое окно наружу: только то, что разбирает планировщик.
Значение — пользовательская настройка, и в базе может оказаться что угодно
(правили руками). Неразбираемое отдаём как «не задано»: тем же правилом
руководствуется и планировщик, и расхождение здесь означало бы, что интерфейс
показывает одно, а напоминания молчат по другому.
"""
row = cast(AppSetting | None, db.get(AppSetting, (user_id, QUIET_HOURS_KEY)))
if row is None or reminders.parse_quiet_hours(row.value) is None:
return DEFAULT_QUIET_HOURS
return row.value
@router.get("", response_model=AppSettingsOut)
async def get_settings(db: DbDep, user_id: UserIdDep) -> AppSettingsOut:
return AppSettingsOut(
currency=_read_value(db, user_id, CURRENCY_KEY, CURRENCIES, DEFAULT_CURRENCY),
language=_read_value(db, user_id, LANGUAGE_KEY, LANGUAGES + ("",), DEFAULT_LANGUAGE),
quiet_hours=_read_quiet_hours(db, user_id),
)
@router.put("", response_model=AppSettingsOut)
async def update_settings(
schema: AppSettingsUpdate, db: DbDep, user_id: UserIdDep
) -> AppSettingsOut:
# Частичный PUT: обновляем только переданные поля (model_fields_set)
if schema.currency is not None:
_upsert(db, user_id, CURRENCY_KEY, schema.currency)
if schema.language is not None:
_upsert(db, user_id, LANGUAGE_KEY, schema.language)
if schema.quiet_hours is not None:
_upsert(db, user_id, QUIET_HOURS_KEY, schema.quiet_hours)
db.flush()
db.commit()
publish(user_id, "settings.changed")
return AppSettingsOut(
currency=_read_value(db, user_id, CURRENCY_KEY, CURRENCIES, DEFAULT_CURRENCY),
language=_read_value(db, user_id, LANGUAGE_KEY, LANGUAGES + ("",), DEFAULT_LANGUAGE),
quiet_hours=_read_quiet_hours(db, user_id),
)