"""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.core.visibility import apply_visibility_filter, check_single_entity_access from app.models.bank_account import BankAccount 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