"""Consumer connectors, widgets & notify dispatches — Phase 11.9. Experience stores connector registration shells, widget definitions/instances, and Communication notify client dispatches. No SMS provider ownership (ADR-012/016). """ from __future__ import annotations import uuid from sqlalchemy import Index, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.core.database import Base from app.models.base import ( ActorAuditMixin, OptimisticLockMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( GUID, ConnectorDispatchDirection, ConnectorDispatchStatus, ConsumerConnectorKind, ConsumerConnectorStatus, NotifyDispatchStatus, WidgetDefinitionStatus, WidgetInstanceStatus, WidgetKind, ) class ExperienceConsumerConnectorRegistration( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): __tablename__ = "experience_consumer_connector_registrations" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) kind: Mapped[ConsumerConnectorKind] = mapped_column(nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[ConsumerConnectorStatus] = mapped_column( default=ConsumerConnectorStatus.DRAFT, nullable=False ) external_service_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) entity_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) config_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "code", name="uq_experience_consumer_connector_registrations_tenant_code", ), Index( "ix_experience_consumer_connector_registrations_workspace", "tenant_id", "workspace_id", ), Index( "ix_experience_consumer_connector_registrations_status", "tenant_id", "status", ), Index( "ix_experience_consumer_connector_registrations_tenant_deleted", "tenant_id", "is_deleted", ), ) class ExperienceConsumerConnectorDispatch( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): __tablename__ = "experience_consumer_connector_dispatches" registration_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) direction: Mapped[ConnectorDispatchDirection] = mapped_column(nullable=False) event_type: Mapped[str] = mapped_column(String(100), nullable=False) payload_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) status: Mapped[ConnectorDispatchStatus] = mapped_column( default=ConnectorDispatchStatus.PENDING, nullable=False ) response_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) notes: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( Index( "ix_experience_consumer_connector_dispatches_registration", "tenant_id", "registration_id", ), Index( "ix_experience_consumer_connector_dispatches_status", "tenant_id", "status", ), Index( "ix_experience_consumer_connector_dispatches_tenant_deleted", "tenant_id", "is_deleted", ), ) class ExperienceWidgetDefinition( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): __tablename__ = "experience_widget_definitions" workspace_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) widget_kind: Mapped[WidgetKind] = mapped_column(nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[WidgetDefinitionStatus] = mapped_column( default=WidgetDefinitionStatus.DRAFT, nullable=False ) connector_kind: Mapped[ConsumerConnectorKind | None] = mapped_column(nullable=True) default_config_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) permission_prefixes: Mapped[list | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "code", name="uq_experience_widget_definitions_tenant_code", ), Index("ix_experience_widget_definitions_status", "tenant_id", "status"), Index( "ix_experience_widget_definitions_tenant_deleted", "tenant_id", "is_deleted", ), ) class ExperienceWidgetInstance( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): __tablename__ = "experience_widget_instances" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) widget_definition_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) widget_kind: Mapped[WidgetKind] = mapped_column(nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[WidgetInstanceStatus] = mapped_column( default=WidgetInstanceStatus.DRAFT, nullable=False ) connector_registration_id: Mapped[uuid.UUID | None] = mapped_column( GUID(), nullable=True ) entity_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) embed_ref: Mapped[str] = mapped_column(String(255), nullable=False) config_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "site_id", "code", name="uq_experience_widget_instances_site_code", ), UniqueConstraint( "tenant_id", "embed_ref", name="uq_experience_widget_instances_embed_ref", ), Index("ix_experience_widget_instances_site", "tenant_id", "site_id"), Index("ix_experience_widget_instances_status", "tenant_id", "status"), Index( "ix_experience_widget_instances_tenant_deleted", "tenant_id", "is_deleted", ), ) class ExperienceNotifyDispatch( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): __tablename__ = "experience_notify_dispatches" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) widget_instance_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) connector_registration_id: Mapped[uuid.UUID | None] = mapped_column( GUID(), nullable=True ) channel: Mapped[str] = mapped_column(String(50), nullable=False) template_key: Mapped[str] = mapped_column(String(100), nullable=False) communication_request_ref: Mapped[str | None] = mapped_column( String(255), nullable=True ) to_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) payload_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) status: Mapped[NotifyDispatchStatus] = mapped_column( default=NotifyDispatchStatus.PENDING, nullable=False ) response_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( Index("ix_experience_notify_dispatches_workspace", "tenant_id", "workspace_id"), Index("ix_experience_notify_dispatches_status", "tenant_id", "status"), Index( "ix_experience_notify_dispatches_tenant_deleted", "tenant_id", "is_deleted", ), )