168 lines
4.7 KiB
Python
168 lines
4.7 KiB
Python
|
|
"""Tax rate service — CRUD, default tax 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.tax import TaxRate
|
||
|
|
|
||
|
|
|
||
|
|
def _tax_to_dict(t: TaxRate) -> dict[str, Any]:
|
||
|
|
"""Serialize a TaxRate ORM object to dict."""
|
||
|
|
return {
|
||
|
|
"id": str(t.id),
|
||
|
|
"name": t.name,
|
||
|
|
"rate": float(t.rate),
|
||
|
|
"is_default": t.is_default,
|
||
|
|
"country": t.country,
|
||
|
|
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||
|
|
"updated_at": t.updated_at.isoformat() if t.updated_at else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def list_tax_rates(
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""List all tax rates for a tenant."""
|
||
|
|
q = select(TaxRate).where(
|
||
|
|
TaxRate.tenant_id == tenant_id,
|
||
|
|
TaxRate.deleted_at.is_(None),
|
||
|
|
).order_by(TaxRate.rate.asc())
|
||
|
|
result = await db.execute(q)
|
||
|
|
tax_rates = result.scalars().all()
|
||
|
|
return {
|
||
|
|
"items": [_tax_to_dict(t) for t in tax_rates],
|
||
|
|
"total": len(tax_rates),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def create_tax_rate(
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
user_id: uuid.UUID,
|
||
|
|
data: dict[str, Any],
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""Create a new tax rate. If is_default=True, unset other defaults first."""
|
||
|
|
if data.get("is_default"):
|
||
|
|
await db.execute(
|
||
|
|
update(TaxRate)
|
||
|
|
.where(TaxRate.tenant_id == tenant_id, TaxRate.is_default.is_(True))
|
||
|
|
.values(is_default=False)
|
||
|
|
)
|
||
|
|
|
||
|
|
tax_rate = TaxRate(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
name=data["name"],
|
||
|
|
rate=data["rate"],
|
||
|
|
is_default=data.get("is_default", False),
|
||
|
|
country=data.get("country"),
|
||
|
|
)
|
||
|
|
db.add(tax_rate)
|
||
|
|
await db.flush()
|
||
|
|
await db.refresh(tax_rate)
|
||
|
|
await log_audit(
|
||
|
|
db, tenant_id, user_id, "create", "tax_rate", tax_rate.id,
|
||
|
|
changes={"name": tax_rate.name, "rate": float(tax_rate.rate)},
|
||
|
|
)
|
||
|
|
return _tax_to_dict(tax_rate)
|
||
|
|
|
||
|
|
|
||
|
|
async def update_tax_rate(
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
user_id: uuid.UUID,
|
||
|
|
tax_id: uuid.UUID,
|
||
|
|
data: dict[str, Any],
|
||
|
|
) -> dict[str, Any] | None:
|
||
|
|
"""Update a tax rate. If setting is_default=True, unset other defaults first."""
|
||
|
|
q = select(TaxRate).where(
|
||
|
|
TaxRate.id == tax_id,
|
||
|
|
TaxRate.tenant_id == tenant_id,
|
||
|
|
TaxRate.deleted_at.is_(None),
|
||
|
|
)
|
||
|
|
result = await db.execute(q)
|
||
|
|
tax_rate = result.scalar_one_or_none()
|
||
|
|
if tax_rate is None:
|
||
|
|
return None
|
||
|
|
|
||
|
|
if data.get("is_default") is True and not tax_rate.is_default:
|
||
|
|
await db.execute(
|
||
|
|
update(TaxRate)
|
||
|
|
.where(
|
||
|
|
TaxRate.tenant_id == tenant_id,
|
||
|
|
TaxRate.is_default.is_(True),
|
||
|
|
TaxRate.id != tax_id,
|
||
|
|
)
|
||
|
|
.values(is_default=False)
|
||
|
|
)
|
||
|
|
|
||
|
|
changes: dict[str, Any] = {}
|
||
|
|
for field in ("name", "rate", "is_default", "country"):
|
||
|
|
if field in data and data[field] is not None:
|
||
|
|
old_val = getattr(tax_rate, field)
|
||
|
|
changes[field] = {"old": old_val, "new": data[field]}
|
||
|
|
setattr(tax_rate, field, data[field])
|
||
|
|
|
||
|
|
await db.flush()
|
||
|
|
await db.refresh(tax_rate)
|
||
|
|
await log_audit(db, tenant_id, user_id, "update", "tax_rate", tax_id, changes=changes)
|
||
|
|
return _tax_to_dict(tax_rate)
|
||
|
|
|
||
|
|
|
||
|
|
async def delete_tax_rate(
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
user_id: uuid.UUID,
|
||
|
|
tax_id: uuid.UUID,
|
||
|
|
) -> bool:
|
||
|
|
"""Soft-delete a tax rate."""
|
||
|
|
q = select(TaxRate).where(
|
||
|
|
TaxRate.id == tax_id,
|
||
|
|
TaxRate.tenant_id == tenant_id,
|
||
|
|
TaxRate.deleted_at.is_(None),
|
||
|
|
)
|
||
|
|
result = await db.execute(q)
|
||
|
|
tax_rate = result.scalar_one_or_none()
|
||
|
|
if tax_rate is None:
|
||
|
|
return False
|
||
|
|
|
||
|
|
tax_rate.deleted_at = datetime.now(UTC)
|
||
|
|
await db.flush()
|
||
|
|
await log_audit(
|
||
|
|
db, tenant_id, user_id, "delete", "tax_rate", tax_id,
|
||
|
|
changes={"name": tax_rate.name},
|
||
|
|
)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
async def seed_default_tax_rates(db: AsyncSession, tenant_id: uuid.UUID) -> None:
|
||
|
|
"""Seed default 19% and 7% tax rates for a new tenant if none exist."""
|
||
|
|
q = select(TaxRate).where(TaxRate.tenant_id == tenant_id, TaxRate.deleted_at.is_(None))
|
||
|
|
result = await db.execute(q)
|
||
|
|
if result.scalars().first() is not None:
|
||
|
|
return
|
||
|
|
|
||
|
|
tax_19 = TaxRate(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
name="Mehrwertsteuer 19%",
|
||
|
|
rate=19.00,
|
||
|
|
is_default=True,
|
||
|
|
country="DE",
|
||
|
|
)
|
||
|
|
tax_7 = TaxRate(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
name="Mehrwertsteuer 7%",
|
||
|
|
rate=7.00,
|
||
|
|
is_default=False,
|
||
|
|
country="DE",
|
||
|
|
)
|
||
|
|
db.add_all([tax_19, tax_7])
|
||
|
|
await db.flush()
|