Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete. Co-authored-by: Cursor <cursoragent@cursor.com>
209 lines
7.3 KiB
Python
209 lines
7.3 KiB
Python
"""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"
|
|
),
|
|
)
|