"""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 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, ) -> 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()) ) 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), ) 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], ) -> 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 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, ) -> 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 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