180 lines
5.5 KiB
Python
180 lines
5.5 KiB
Python
"""Bank account service — CRUD with tenant isolation."""
|
|
|
|
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.bank_account import BankAccount
|
|
from app.core.visibility import apply_visibility_filter, check_single_entity_access
|
|
|
|
|
|
def _account_to_dict(a: BankAccount) -> dict[str, Any]:
|
|
"""Serialize a BankAccount ORM object to dict."""
|
|
return {
|
|
"id": str(a.id),
|
|
"bank_name": a.bank_name,
|
|
"iban": a.iban,
|
|
"bic": a.bic,
|
|
"account_holder": a.account_holder,
|
|
"default_tax": a.default_tax,
|
|
"is_default": a.is_default,
|
|
"created_at": a.created_at.isoformat() if a.created_at else None,
|
|
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
|
}
|
|
|
|
|
|
async def list_bank_accounts(
|
|
db: AsyncSession,
|
|
tenant_id: uuid.UUID,
|
|
user_id: uuid.UUID | None = None,
|
|
is_system_admin: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""List all bank accounts for a tenant."""
|
|
q = (
|
|
select(BankAccount)
|
|
.where(
|
|
BankAccount.tenant_id == tenant_id,
|
|
BankAccount.deleted_at.is_(None),
|
|
)
|
|
.order_by(BankAccount.is_default.desc(), BankAccount.bank_name.asc())
|
|
)
|
|
if user_id and not is_system_admin:
|
|
q = await apply_visibility_filter(
|
|
db, q, "bank_account", BankAccount, user_id, tenant_id, is_system_admin
|
|
)
|
|
result = await db.execute(q)
|
|
accounts = result.scalars().all()
|
|
return {
|
|
"items": [_account_to_dict(a) for a in accounts],
|
|
"total": len(accounts),
|
|
}
|
|
|
|
|
|
async def create_bank_account(
|
|
db: AsyncSession,
|
|
tenant_id: uuid.UUID,
|
|
user_id: uuid.UUID,
|
|
data: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
"""Create a new bank account. If is_default=True, unset other defaults first."""
|
|
if data.get("is_default"):
|
|
await db.execute(
|
|
update(BankAccount)
|
|
.where(
|
|
BankAccount.tenant_id == tenant_id,
|
|
BankAccount.is_default.is_(True),
|
|
BankAccount.deleted_at.is_(None),
|
|
)
|
|
.values(is_default=False)
|
|
)
|
|
|
|
account = BankAccount(
|
|
tenant_id=tenant_id,
|
|
bank_name=data["bank_name"],
|
|
iban=data["iban"],
|
|
bic=data.get("bic"),
|
|
account_holder=data.get("account_holder"),
|
|
default_tax=data.get("default_tax"),
|
|
is_default=data.get("is_default", False),
|
|
owner_id=user_id,
|
|
)
|
|
db.add(account)
|
|
await db.flush()
|
|
await db.refresh(account)
|
|
await log_audit(
|
|
db, tenant_id, user_id, "create", "bank_account", account.id,
|
|
changes={"bank_name": account.bank_name, "iban": account.iban},
|
|
)
|
|
return _account_to_dict(account)
|
|
|
|
|
|
async def update_bank_account(
|
|
db: AsyncSession,
|
|
tenant_id: uuid.UUID,
|
|
user_id: uuid.UUID,
|
|
account_id: uuid.UUID,
|
|
data: dict[str, Any],
|
|
is_system_admin: bool = False,
|
|
) -> dict[str, Any] | None:
|
|
"""Update a bank account. If setting is_default=True, unset other defaults first."""
|
|
q = select(BankAccount).where(
|
|
BankAccount.id == account_id,
|
|
BankAccount.tenant_id == tenant_id,
|
|
BankAccount.deleted_at.is_(None),
|
|
)
|
|
result = await db.execute(q)
|
|
account = result.scalar_one_or_none()
|
|
if account is None:
|
|
return None
|
|
|
|
if not is_system_admin:
|
|
has_access = await check_single_entity_access(
|
|
db, "bank_account", account.id, user_id, tenant_id, "write", is_system_admin
|
|
)
|
|
if not has_access:
|
|
raise PermissionError("No access")
|
|
|
|
if data.get("is_default") is True and not account.is_default:
|
|
await db.execute(
|
|
update(BankAccount)
|
|
.where(
|
|
BankAccount.tenant_id == tenant_id,
|
|
BankAccount.is_default.is_(True),
|
|
BankAccount.id != account_id,
|
|
BankAccount.deleted_at.is_(None),
|
|
)
|
|
.values(is_default=False)
|
|
)
|
|
|
|
changes: dict[str, Any] = {}
|
|
for field in ("bank_name", "iban", "bic", "account_holder", "default_tax", "is_default"):
|
|
if field in data and data[field] is not None:
|
|
old_val = getattr(account, field)
|
|
changes[field] = {"old": old_val, "new": data[field]}
|
|
setattr(account, field, data[field])
|
|
|
|
await db.flush()
|
|
await db.refresh(account)
|
|
await log_audit(db, tenant_id, user_id, "update", "bank_account", account_id, changes=changes)
|
|
return _account_to_dict(account)
|
|
|
|
|
|
async def delete_bank_account(
|
|
db: AsyncSession,
|
|
tenant_id: uuid.UUID,
|
|
user_id: uuid.UUID,
|
|
account_id: uuid.UUID,
|
|
is_system_admin: bool = False,
|
|
) -> bool:
|
|
"""Soft-delete a bank account."""
|
|
q = select(BankAccount).where(
|
|
BankAccount.id == account_id,
|
|
BankAccount.tenant_id == tenant_id,
|
|
BankAccount.deleted_at.is_(None),
|
|
)
|
|
result = await db.execute(q)
|
|
account = result.scalar_one_or_none()
|
|
if account is None:
|
|
return False
|
|
|
|
if not is_system_admin:
|
|
has_access = await check_single_entity_access(
|
|
db, "bank_account", account.id, user_id, tenant_id, "admin", is_system_admin
|
|
)
|
|
if not has_access:
|
|
raise PermissionError("No access")
|
|
|
|
account.deleted_at = datetime.now(UTC)
|
|
await db.flush()
|
|
await log_audit(
|
|
db, tenant_id, user_id, "delete", "bank_account", account_id,
|
|
changes={"bank_name": account.bank_name},
|
|
)
|
|
return True
|