"""Currency service — CRUD, default currency management, seeding.""" from __future__ import annotations import uuid from datetime import UTC, datetime from typing import Any from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from app.core.audit import log_audit from app.models.currency import Currency def _currency_to_dict(c: Currency) -> dict[str, Any]: """Serialize a Currency ORM object to dict.""" return { "id": str(c.id), "code": c.code, "name": c.name, "symbol": c.symbol, "is_default": c.is_default, "created_at": c.created_at.isoformat() if c.created_at else None, "updated_at": c.updated_at.isoformat() if c.updated_at else None, } async def list_currencies( db: AsyncSession, tenant_id: uuid.UUID, ) -> dict[str, Any]: """List all currencies for a tenant.""" q = select(Currency).where( Currency.tenant_id == tenant_id, Currency.deleted_at.is_(None), ).order_by(Currency.code.asc()) result = await db.execute(q) currencies = result.scalars().all() return { "items": [_currency_to_dict(c) for c in currencies], "total": len(currencies), } async def create_currency( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, data: dict[str, Any], ) -> dict[str, Any]: """Create a new currency. If is_default=True, unset other defaults first.""" if data.get("is_default"): await db.execute( update(Currency) .where(Currency.tenant_id == tenant_id, Currency.is_default.is_(True)) .values(is_default=False) ) currency = Currency( tenant_id=tenant_id, code=data["code"].upper(), name=data["name"], symbol=data["symbol"], is_default=data.get("is_default", False), ) db.add(currency) await db.flush() await db.refresh(currency) await log_audit( db, tenant_id, user_id, "create", "currency", currency.id, changes={"code": currency.code}, ) return _currency_to_dict(currency) async def update_currency( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, currency_id: uuid.UUID, data: dict[str, Any], ) -> dict[str, Any] | None: """Update a currency. If setting is_default=True, unset other defaults first.""" q = select(Currency).where( Currency.id == currency_id, Currency.tenant_id == tenant_id, Currency.deleted_at.is_(None), ) result = await db.execute(q) currency = result.scalar_one_or_none() if currency is None: return None if data.get("is_default") is True and not currency.is_default: await db.execute( update(Currency) .where( Currency.tenant_id == tenant_id, Currency.is_default.is_(True), Currency.id != currency_id, ) .values(is_default=False) ) changes: dict[str, Any] = {} for field in ("code", "name", "symbol", "is_default"): if field in data and data[field] is not None: old_val = getattr(currency, field) changes[field] = {"old": old_val, "new": data[field]} setattr(currency, field, data[field]) await db.flush() await db.refresh(currency) await log_audit(db, tenant_id, user_id, "update", "currency", currency_id, changes=changes) return _currency_to_dict(currency) async def delete_currency( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, currency_id: uuid.UUID, ) -> bool: """Soft-delete a currency.""" q = select(Currency).where( Currency.id == currency_id, Currency.tenant_id == tenant_id, Currency.deleted_at.is_(None), ) result = await db.execute(q) currency = result.scalar_one_or_none() if currency is None: return False currency.deleted_at = datetime.now(UTC) await db.flush() await log_audit( db, tenant_id, user_id, "delete", "currency", currency_id, changes={"code": currency.code}, ) return True async def seed_default_currencies(db: AsyncSession, tenant_id: uuid.UUID) -> None: """Seed default EUR currency for a new tenant if none exist.""" q = select(Currency).where(Currency.tenant_id == tenant_id, Currency.deleted_at.is_(None)) result = await db.execute(q) if result.scalars().first() is not None: return eur = Currency( tenant_id=tenant_id, code="EUR", name="Euro", symbol="€", is_default=True, ) db.add(eur) await db.flush()