"""Capability bundles, licensing refs & feature toggles — Phase 11.8. ExperienceBundleDefinition = catalog of commercial capability packs. ExperienceTenantBundle = activated pack for a workspace (optionally site). ExperienceLicenseBinding = Core entitlement refs only (no plan-engine ownership). ExperienceFeatureToggle = per-workspace/site feature flags coordinated with bundles. No widgets/connectors in this phase. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import Boolean, DateTime, 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, BundleDefinitionStatus, BundleKey, FeatureToggleStatus, LicenseBindingStatus, TenantBundleStatus, ) class ExperienceBundleDefinition( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Catalog entry for a licensable Experience capability bundle.""" __tablename__ = "experience_bundle_definitions" workspace_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) code: Mapped[str] = mapped_column(String(50), nullable=False) bundle_key: Mapped[BundleKey] = mapped_column(nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[BundleDefinitionStatus] = mapped_column( default=BundleDefinitionStatus.DRAFT, nullable=False ) feature_keys: Mapped[list | None] = mapped_column(JSON, nullable=True) permission_prefixes: Mapped[list | None] = mapped_column(JSON, nullable=True) api_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_bundle_definitions_tenant_code", ), UniqueConstraint( "tenant_id", "bundle_key", name="uq_experience_bundle_definitions_tenant_key", ), Index("ix_experience_bundle_definitions_status", "tenant_id", "status"), Index( "ix_experience_bundle_definitions_tenant_deleted", "tenant_id", "is_deleted", ), ) class ExperienceTenantBundle( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Activated bundle assignment for a workspace (optional site scope).""" __tablename__ = "experience_tenant_bundles" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) bundle_definition_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) bundle_key: Mapped[BundleKey] = mapped_column(nullable=False) status: Mapped[TenantBundleStatus] = mapped_column( default=TenantBundleStatus.PENDING, nullable=False ) core_entitlement_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) activated_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) expires_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) activated_by: Mapped[str | None] = mapped_column(String(100), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "site_id", "bundle_key", name="uq_experience_tenant_bundles_scope_key", ), Index("ix_experience_tenant_bundles_workspace", "tenant_id", "workspace_id"), Index("ix_experience_tenant_bundles_status", "tenant_id", "status"), Index( "ix_experience_tenant_bundles_tenant_deleted", "tenant_id", "is_deleted" ), ) class ExperienceLicenseBinding( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Licensing shell — stores Core entitlement refs only (no plan engine).""" __tablename__ = "experience_license_bindings" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) tenant_bundle_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) bundle_key: Mapped[BundleKey | None] = mapped_column(nullable=True) core_entitlement_ref: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[LicenseBindingStatus] = mapped_column( default=LicenseBindingStatus.PENDING, nullable=False ) starts_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) ends_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "core_entitlement_ref", name="uq_experience_license_bindings_entitlement", ), Index("ix_experience_license_bindings_workspace", "tenant_id", "workspace_id"), Index("ix_experience_license_bindings_status", "tenant_id", "status"), Index( "ix_experience_license_bindings_tenant_deleted", "tenant_id", "is_deleted" ), ) class ExperienceFeatureToggle( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Per-workspace/site feature toggle for capability discovery.""" __tablename__ = "experience_feature_toggles" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) feature_key: Mapped[str] = mapped_column(String(100), nullable=False) enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) status: Mapped[FeatureToggleStatus] = mapped_column( default=FeatureToggleStatus.ACTIVE, nullable=False ) bundle_key: Mapped[BundleKey | None] = mapped_column(nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "site_id", "feature_key", name="uq_experience_feature_toggles_scope_key", ), Index("ix_experience_feature_toggles_enabled", "tenant_id", "enabled"), Index("ix_experience_feature_toggles_status", "tenant_id", "status"), Index( "ix_experience_feature_toggles_tenant_deleted", "tenant_id", "is_deleted" ), )