"""专线 (dedicated / leased line) register — service layer. Pure manual-CRUD over ``cmdb_dedicated_lines``. There is no upstream provider to reconcile against, so unlike the cloud-resource services this module is a thin, idempotent persistence layer: list / get / create / update (partial) / soft-delete, with modifier stamping for audit. """ from __future__ import annotations import uuid from datetime import UTC, datetime from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.models import CmdbDedicatedLine from app.domains.cmdb.schemas import DedicatedLineCreate, DedicatedLineUpdate def _esc(s: str) -> str: """Escape LIKE metacharacters to prevent wildcard injection.""" return s.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") def _stamp_modifier(row: CmdbDedicatedLine, actor_person_id: str | None) -> None: if actor_person_id: row.last_modified_by_person_id = actor_person_id row.last_modified_at = datetime.now(UTC) async def list_dedicated_lines( db: AsyncSession, *, search: str | None = None, carrier: str | None = None, page: int = 1, page_size: int = 50, ) -> tuple[list[CmdbDedicatedLine], int]: """Paginated list of live (non-deleted) leased-line rows.""" base = select(CmdbDedicatedLine).where(CmdbDedicatedLine.deleted_at.is_(None)) if carrier: base = base.where(CmdbDedicatedLine.carrier == carrier) if search: like = f"%{_esc(search)}%" base = base.where( or_( CmdbDedicatedLine.location.ilike(like), CmdbDedicatedLine.contracting_entity.ilike(like), CmdbDedicatedLine.carrier.ilike(like), CmdbDedicatedLine.purpose.ilike(like), CmdbDedicatedLine.remark.ilike(like), ) ) total = await db.scalar( select(func.count()).select_from(base.order_by(None).subquery()) ) rows = await db.execute( base.order_by(CmdbDedicatedLine.location.asc(), CmdbDedicatedLine.created_at.asc()) .offset((page - 1) * page_size) .limit(page_size) ) return list(rows.scalars().all()), int(total or 0) async def get_dedicated_line( db: AsyncSession, line_id: str, ) -> CmdbDedicatedLine | None: try: uid = uuid.UUID(str(line_id)) except (ValueError, TypeError): return None return await db.scalar( select(CmdbDedicatedLine).where( CmdbDedicatedLine.id == uid, CmdbDedicatedLine.deleted_at.is_(None), ) ) async def create_dedicated_line( db: AsyncSession, data: DedicatedLineCreate, *, actor_person_id: str | None = None, ) -> CmdbDedicatedLine: """Create + flush (caller commits).""" row = CmdbDedicatedLine( location=data.location, contracting_entity=data.contracting_entity, carrier=data.carrier, payment_method=data.payment_method, monthly_cost=data.monthly_cost, annual_cost=data.annual_cost, currency=data.currency, renewal_date=data.renewal_date, purpose=data.purpose, remark=data.remark, ) _stamp_modifier(row, actor_person_id) db.add(row) await db.flush() await db.refresh(row) return row async def update_dedicated_line( db: AsyncSession, line_id: str, data: DedicatedLineUpdate, *, actor_person_id: str | None = None, ) -> CmdbDedicatedLine | None: """Apply a partial update under a row lock. Returns None if missing.""" try: uid = uuid.UUID(str(line_id)) except (ValueError, TypeError): return None row = await db.scalar( select(CmdbDedicatedLine) .where( CmdbDedicatedLine.id == uid, CmdbDedicatedLine.deleted_at.is_(None), ) .with_for_update() ) if row is None: return None fields = data.model_fields_set changed = False for attr in ( "location", "contracting_entity", "carrier", "payment_method", "monthly_cost", "annual_cost", "currency", "renewal_date", "purpose", "remark", ): if attr in fields: setattr(row, attr, getattr(data, attr)) changed = True if changed: _stamp_modifier(row, actor_person_id) await db.flush() await db.refresh(row) return row async def delete_dedicated_line( db: AsyncSession, line_id: str, *, actor_person_id: str | None = None, ) -> CmdbDedicatedLine | None: """Soft-delete. Returns the row (for audit) or None if missing.""" try: uid = uuid.UUID(str(line_id)) except (ValueError, TypeError): return None row = await db.scalar( select(CmdbDedicatedLine) .where( CmdbDedicatedLine.id == uid, CmdbDedicatedLine.deleted_at.is_(None), ) .with_for_update() ) if row is None: return None row.deleted_at = datetime.now(UTC) _stamp_modifier(row, actor_person_id) await db.flush() return row