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