feat(core): add currency service
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user