From 725c4413f512f24407b5ff04e7b4025f3fa32894 Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Sat, 4 Jul 2026 00:21:42 +0000 Subject: [PATCH] feat(core): add currency service --- app/services/currency_service.py | 160 +++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 app/services/currency_service.py diff --git a/app/services/currency_service.py b/app/services/currency_service.py new file mode 100644 index 0000000..8392dc3 --- /dev/null +++ b/app/services/currency_service.py @@ -0,0 +1,160 @@ +"""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()