from __future__ import annotations from datetime import datetime, timezone from typing import Any from uuid import UUID from src.connectors.base import ConnectorResult from src.connectors.registry import connector_registry from src.connectors.sdk.context import ConnectorContext from src.core.exceptions import ConnectorException from src.core.logging import get_logger from src.domain.entities import ConnectorConfigEntity, ConnectorEntity, ConnectorStatus from src.domain.interfaces import IConnectorConfigRepository, IConnectorRepository from src.infrastructure.messaging.rabbitmq import rabbitmq_client logger = get_logger("connector_service") class ConnectorService: def __init__(self, connector_repo: IConnectorRepository, config_repo: IConnectorConfigRepository) -> None: self._connector_repo = connector_repo self._config_repo = config_repo async def create_connector(self, entity: ConnectorEntity, configs: list[ConnectorConfigEntity] | None = None) -> ConnectorEntity: created = await self._connector_repo.create(entity) if configs: for cfg in configs: cfg.connector_id = created.id await self._config_repo.upsert(cfg) await self._publish_event("connector.created", created.id, {"connector_type": created.connector_type, "pharmacy_id": str(created.pharmacy_id), "erp_system_id": str(created.erp_system_id), "status": created.status}) logger.info("connector_created", extra={"connector_id": str(created.id), "connector_type": created.connector_type}) return created async def get_connector(self, connector_id: UUID) -> ConnectorEntity | None: return await self._connector_repo.get_by_id(connector_id) async def list_connectors(self, *, erp_system_id: UUID | None = None, pharmacy_id: UUID | None = None, status: str | None = None, offset: int = 0, limit: int = 50) -> tuple[list[ConnectorEntity], int]: return await self._connector_repo.list_connectors(erp_system_id=erp_system_id, pharmacy_id=pharmacy_id, status=status, offset=offset, limit=limit) async def update_connector(self, entity: ConnectorEntity) -> ConnectorEntity: updated = await self._connector_repo.update(entity) await self._publish_event("connector.updated", updated.id, {"connector_type": updated.connector_type, "status": updated.status}) return updated async def delete_connector(self, connector_id: UUID) -> bool: connector = await self._connector_repo.get_by_id(connector_id) deleted = await self._connector_repo.delete(connector_id) if deleted: connector_registry.remove_instance(connector_id) if connector: await self._publish_event("connector.deleted", connector_id, {"connector_type": connector.connector_type, "pharmacy_id": str(connector.pharmacy_id)}) return deleted async def test_connection(self, connector_id: UUID) -> ConnectorResult: connector_entity = await self._connector_repo.get_by_id(connector_id) if not connector_entity: raise ConnectorException("unknown", f"Connector '{connector_id}' not found") configs = await self._config_repo.list_by_connector(connector_id) config_dict = {c.key: c.value for c in configs} secrets_dict = {c.key: c.value for c in configs if c.encrypted} context = ConnectorContext( connector_id=connector_entity.id, pharmacy_id=connector_entity.pharmacy_id, erp_system_id=connector_entity.erp_system_id, connector_type=connector_entity.connector_type, config=config_dict, secrets=secrets_dict, ) instance = connector_registry.get_or_create_instance(connector_id, connector_entity.connector_type, config_dict) if hasattr(instance, "set_context"): instance.set_context(context) result = await instance.heartbeat(connector_entity.pharmacy_id) if result.success: connector_entity.status = ConnectorStatus.ACTIVE connector_entity.last_heartbeat_at = datetime.now(timezone.utc) connector_entity.last_error = None await self._connector_repo.update(connector_entity) await self._publish_event("connector.heartbeat", connector_id, {"status": "healthy"}) else: connector_entity.status = ConnectorStatus.ERROR connector_entity.last_error = result.error await self._connector_repo.update(connector_entity) await self._publish_event("connector.error", connector_id, {"error": result.error}) return result async def deploy_connector(self, connector_id: UUID) -> ConnectorEntity: entity = await self._connector_repo.get_by_id(connector_id) if not entity: raise ConnectorException("unknown", f"Connector '{connector_id}' not found") if entity.status == ConnectorStatus.ACTIVE: raise ConnectorException(entity.connector_type, "Connector is already active. Deactivate first to redeploy.") configs = await self._config_repo.list_by_connector(connector_id) config_dict = {c.key: c.value for c in configs} connector_registry.remove_instance(connector_id) connector_registry.get_or_create_instance(connector_id, entity.connector_type, config_dict) entity.status = ConnectorStatus.ACTIVE entity.last_heartbeat_at = datetime.now(timezone.utc) entity.last_error = None updated = await self._connector_repo.update(entity) await self._publish_event("connector.deployed", connector_id, {"connector_type": entity.connector_type, "status": "active"}) logger.info("connector_deployed", extra={"connector_id": str(connector_id)}) return updated async def deactivate_connector(self, connector_id: UUID) -> ConnectorEntity: entity = await self._connector_repo.get_by_id(connector_id) if not entity: raise ConnectorException("unknown", f"Connector '{connector_id}' not found") connector_registry.remove_instance(connector_id) entity.status = ConnectorStatus.INACTIVE updated = await self._connector_repo.update(entity) await self._publish_event("connector.deactivated", connector_id, {"connector_type": entity.connector_type, "status": "inactive"}) return updated async def get_configs(self, connector_id: UUID) -> list[ConnectorConfigEntity]: return await self._config_repo.list_by_connector(connector_id) async def upsert_config(self, entity: ConnectorConfigEntity) -> ConnectorConfigEntity: return await self._config_repo.upsert(entity) async def delete_config(self, config_id: UUID) -> bool: return await self._config_repo.delete(config_id) async def _publish_event(self, event_type: str, connector_id: UUID, payload: dict[str, Any]) -> None: try: routing_key = f"connector.{event_type.split('.', 1)[1]}" if "." in event_type else f"connector.{event_type}" full_payload = {"event_type": event_type, "connector_id": str(connector_id), "timestamp": datetime.now(timezone.utc).isoformat(), **payload} await rabbitmq_client.publish(routing_key, full_payload) except Exception as exc: logger.warning("connector_event_publish_failed", extra={"event_type": event_type, "error": str(exc)})