Files
leocrm/app/services/bank_account_service.py
Agent Zero abbe7a18fc fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner
- P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var
- P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup
- P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns
- P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs)
- P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import
- P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default
- P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed
- P2: 28 frontend TODOs (hardcoded constants, deprecated notification API)
- P3: dead code, duplicates, deprecated imports, private attr, __import__ inline
- P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n)
- ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix)
- F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String)
- Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
2026-08-16 01:17:18 +02:00

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.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