"""Thin compatibility shims so domain code can stay mostly unchanged while its credential source moves under the unified integrations table. Pattern: # before (old path): # from app.domains.platform_config.service import get_feishu_config # after: from app.integrations.compat import get_feishu_config cfg = await get_feishu_config(session=db) The shape of the returned dict is identical to what ``platform_configs`` used to return (public fields + decrypted secrets merged). The backing row now lives in ``integrations`` (one per kind), resolved via the primary-integration rule. """ from __future__ import annotations from typing import Any from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import async_session_factory from app.core.exceptions import AppException from app.domains.cmdb.credential_crypto import CredentialDecryptionError, decrypt_fields from app.integrations.models import Integration from app.integrations.service import get_primary async def _resolve_config( kind: str, *, session: AsyncSession | None = None, ) -> dict[str, Any]: async def _body(s: AsyncSession) -> dict[str, Any]: row: Integration = await get_primary(kind, session=s) merged: dict[str, Any] = dict(row.config_public or {}) if row.secrets_encrypted and row.secrets_nonce: try: merged.update(decrypt_fields( row.secrets_encrypted, row.secrets_nonce, integration_id=str(row.id), )) except CredentialDecryptionError as e: raise AppException( code="CREDENTIAL_DECRYPTION_FAILED", message=str(e), status_code=503, ) from e return merged if session is not None: return await _body(session) async with async_session_factory() as owned: return await _body(owned) async def get_feishu_config( *, session: AsyncSession | None = None, ) -> dict[str, Any]: """Merged plaintext config for the primary ``chat_approval`` integration. Raises ``AppException(INTEGRATION_NOT_CONFIGURED, 503)`` if nothing is configured yet — domain code should surface that as "please configure the Feishu connector" to the UI. """ return await _resolve_config("chat_approval", session=session) async def get_successfactors_config( *, session: AsyncSession | None = None, ) -> dict[str, Any]: """Merged plaintext config for the primary ``hris`` integration.""" return await _resolve_config("hris", session=session) async def list_hris_instances( *, session: AsyncSession | None = None, ) -> list[tuple[Integration, dict[str, Any]]]: """All enabled HRIS integrations with their decrypted config. Returns a list of ``(row, merged_config)`` tuples ordered by name. Used by the scheduled sync worker to iterate over every configured SF instance (e.g. CN + DE) rather than the single "primary" one. Intentionally returns ORM rows so callers can tag downstream records with ``hris_instance_id`` and log the instance slug. """ async def _body(s: AsyncSession) -> list[tuple[Integration, dict[str, Any]]]: rows = ( await s.execute( select(Integration) .where( Integration.kind == "hris", Integration.enabled.is_(True), ) .order_by(Integration.name) ) ).scalars().all() results: list[tuple[Integration, dict[str, Any]]] = [] for row in rows: merged: dict[str, Any] = dict(row.config_public or {}) if row.secrets_encrypted and row.secrets_nonce: try: merged.update(decrypt_fields( row.secrets_encrypted, row.secrets_nonce, integration_id=str(row.id), )) except CredentialDecryptionError as e: raise AppException( code="CREDENTIAL_DECRYPTION_FAILED", message=str(e), status_code=503, ) from e results.append((row, merged)) return results if session is not None: return await _body(session) async with async_session_factory() as owned: return await _body(owned) __all__ = [ "get_feishu_config", "get_successfactors_config", "list_hris_instances", ]