refactor: Clean up imports in currencies and users routes fix: Update theme settings saving logic and clean up test imports
194 lines
5.3 KiB
Python
194 lines
5.3 KiB
Python
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from models.currency import Currency
|
|
from routes.dependencies import get_db
|
|
|
|
router = APIRouter(prefix="/api/currencies", tags=["Currencies"])
|
|
|
|
|
|
DEFAULT_CURRENCY_CODE = "USD"
|
|
DEFAULT_CURRENCY_NAME = "US Dollar"
|
|
DEFAULT_CURRENCY_SYMBOL = "$"
|
|
|
|
|
|
class CurrencyBase(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=128)
|
|
symbol: Optional[str] = Field(default=None, max_length=8)
|
|
|
|
@staticmethod
|
|
def _normalize_symbol(value: Optional[str]) -> Optional[str]:
|
|
if value is None:
|
|
return None
|
|
value = value.strip()
|
|
return value or None
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def _strip_name(cls, value: str) -> str:
|
|
return value.strip()
|
|
|
|
@field_validator("symbol")
|
|
@classmethod
|
|
def _strip_symbol(cls, value: Optional[str]) -> Optional[str]:
|
|
return cls._normalize_symbol(value)
|
|
|
|
|
|
class CurrencyCreate(CurrencyBase):
|
|
code: str = Field(..., min_length=3, max_length=3)
|
|
is_active: bool = True
|
|
|
|
@field_validator("code")
|
|
@classmethod
|
|
def _normalize_code(cls, value: str) -> str:
|
|
return value.strip().upper()
|
|
|
|
|
|
class CurrencyUpdate(CurrencyBase):
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class CurrencyActivation(BaseModel):
|
|
is_active: bool
|
|
|
|
|
|
class CurrencyRead(CurrencyBase):
|
|
id: int
|
|
code: str
|
|
is_active: bool
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
def _ensure_default_currency(db: Session) -> Currency:
|
|
existing = (
|
|
db.query(Currency)
|
|
.filter(Currency.code == DEFAULT_CURRENCY_CODE)
|
|
.one_or_none()
|
|
)
|
|
if existing:
|
|
return existing
|
|
|
|
default_currency = Currency(
|
|
code=DEFAULT_CURRENCY_CODE,
|
|
name=DEFAULT_CURRENCY_NAME,
|
|
symbol=DEFAULT_CURRENCY_SYMBOL,
|
|
is_active=True,
|
|
)
|
|
db.add(default_currency)
|
|
try:
|
|
db.commit()
|
|
except IntegrityError:
|
|
db.rollback()
|
|
existing = (
|
|
db.query(Currency)
|
|
.filter(Currency.code == DEFAULT_CURRENCY_CODE)
|
|
.one()
|
|
)
|
|
return existing
|
|
db.refresh(default_currency)
|
|
return default_currency
|
|
|
|
|
|
def _get_currency_or_404(db: Session, code: str) -> Currency:
|
|
normalized = code.strip().upper()
|
|
currency = (
|
|
db.query(Currency).filter(Currency.code == normalized).one_or_none()
|
|
)
|
|
if currency is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Currency not found"
|
|
)
|
|
return currency
|
|
|
|
|
|
@router.get("/", response_model=List[CurrencyRead])
|
|
def list_currencies(
|
|
include_inactive: bool = Query(
|
|
False, description="Include inactive currencies"
|
|
),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
_ensure_default_currency(db)
|
|
query = db.query(Currency)
|
|
if not include_inactive:
|
|
query = query.filter(Currency.is_active.is_(True))
|
|
currencies = query.order_by(Currency.code).all()
|
|
return currencies
|
|
|
|
|
|
@router.post(
|
|
"/", response_model=CurrencyRead, status_code=status.HTTP_201_CREATED
|
|
)
|
|
def create_currency(payload: CurrencyCreate, db: Session = Depends(get_db)):
|
|
code = payload.code
|
|
existing = db.query(Currency).filter(Currency.code == code).one_or_none()
|
|
if existing is not None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=f"Currency '{code}' already exists",
|
|
)
|
|
|
|
currency = Currency(
|
|
code=code,
|
|
name=payload.name,
|
|
symbol=CurrencyBase._normalize_symbol(payload.symbol),
|
|
is_active=payload.is_active,
|
|
)
|
|
db.add(currency)
|
|
db.commit()
|
|
db.refresh(currency)
|
|
return currency
|
|
|
|
|
|
@router.put("/{code}", response_model=CurrencyRead)
|
|
def update_currency(
|
|
code: str, payload: CurrencyUpdate, db: Session = Depends(get_db)
|
|
):
|
|
currency = _get_currency_or_404(db, code)
|
|
|
|
if payload.name is not None:
|
|
setattr(currency, "name", payload.name)
|
|
if payload.symbol is not None or payload.symbol == "":
|
|
setattr(
|
|
currency,
|
|
"symbol",
|
|
CurrencyBase._normalize_symbol(payload.symbol),
|
|
)
|
|
if payload.is_active is not None:
|
|
code_value = getattr(currency, "code")
|
|
if code_value == DEFAULT_CURRENCY_CODE and payload.is_active is False:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="The default currency cannot be deactivated.",
|
|
)
|
|
setattr(currency, "is_active", payload.is_active)
|
|
|
|
db.add(currency)
|
|
db.commit()
|
|
db.refresh(currency)
|
|
return currency
|
|
|
|
|
|
@router.patch("/{code}/activation", response_model=CurrencyRead)
|
|
def toggle_currency_activation(
|
|
code: str, body: CurrencyActivation, db: Session = Depends(get_db)
|
|
):
|
|
currency = _get_currency_or_404(db, code)
|
|
code_value = getattr(currency, "code")
|
|
if code_value == DEFAULT_CURRENCY_CODE and body.is_active is False:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="The default currency cannot be deactivated.",
|
|
)
|
|
|
|
setattr(currency, "is_active", body.is_active)
|
|
db.add(currency)
|
|
db.commit()
|
|
db.refresh(currency)
|
|
return currency
|