feat: implement three-tier module classification and framework layer

Module Classification:
- Core (4): core, tenancy, cms, customers - always enabled
- Optional (7): payments, billing, inventory, orders, marketplace, analytics, messaging
- Internal (2): dev-tools, monitoring - admin-only

Key Changes:
- Rename platform-admin module to tenancy
- Promote CMS and Customers to core modules
- Create new payments module (gateway abstractions)
- Add billing→payments and orders→payments dependencies
- Mark dev-tools and monitoring as internal modules

New Infrastructure:
- app/modules/events.py: Module event bus (ENABLED, DISABLED, STARTUP, SHUTDOWN)
- app/modules/migrations.py: Module-specific migration discovery
- app/core/observability.py: Health checks, Prometheus metrics, Sentry integration

Enhanced ModuleDefinition:
- version, is_internal, permissions
- config_schema, default_config
- migrations_path
- Lifecycle hooks: on_enable, on_disable, on_startup, health_check

New Registry Functions:
- get_optional_module_codes(), get_internal_module_codes()
- is_core_module(), is_internal_module()
- get_modules_by_tier(), get_module_tier()

Migrations:
- zc*: Rename platform-admin to tenancy
- zd*: Ensure CMS and Customers enabled for all platforms

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 22:02:39 +01:00
parent 9a828999fe
commit 1a52611438
26 changed files with 3084 additions and 67 deletions

View File

@@ -213,6 +213,41 @@ if config.config_file_name is not None:
target_metadata = Base.metadata
# ============================================================================
# MODULE MIGRATIONS DISCOVERY
# ============================================================================
# Discover migration paths from self-contained modules.
# Each module can have its own migrations/ directory.
# ============================================================================
def get_version_locations() -> list[str]:
"""
Get all version locations including module migrations.
Returns:
List of paths to migration version directories
"""
try:
from app.modules.migrations import get_all_migration_paths
paths = get_all_migration_paths()
locations = [str(p) for p in paths if p.exists()]
if len(locations) > 1:
print(f"[ALEMBIC] Found {len(locations)} migration locations:")
for loc in locations:
print(f" - {loc}")
return locations
except ImportError:
# Fallback if module migrations not available
return ["alembic/versions"]
# Get version locations for multi-directory support
version_locations = get_version_locations()
def run_migrations_offline() -> None:
"""
Run migrations in 'offline' mode.
@@ -229,6 +264,7 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
version_locations=version_locations,
)
with context.begin_transaction():
@@ -249,7 +285,11 @@ def run_migrations_online() -> None:
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection,
target_metadata=target_metadata,
version_locations=version_locations,
)
with context.begin_transaction():
context.run_migrations()

View File

@@ -0,0 +1,95 @@
# alembic/versions/zc2m3n4o5p6q7_rename_platform_admin_to_tenancy.py
"""Rename platform-admin module to tenancy.
Revision ID: zc2m3n4o5p6q7
Revises: zb1l2m3n4o5p6
Create Date: 2026-01-27 10:00:00.000000
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "zc2m3n4o5p6q7"
down_revision = "zb1l2m3n4o5p6"
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Rename platform-admin to tenancy in platform_modules table."""
# Update module_code in platform_modules junction table
op.execute(
"""
UPDATE platform_modules
SET module_code = 'tenancy'
WHERE module_code = 'platform-admin'
"""
)
# Also update any JSON settings that might reference the old module code
# This handles Platform.settings["enabled_modules"] for legacy data
op.execute(
"""
UPDATE platforms
SET settings = jsonb_set(
settings,
'{enabled_modules}',
(
SELECT COALESCE(
jsonb_agg(
CASE
WHEN elem = 'platform-admin' THEN 'tenancy'
ELSE elem
END
),
'[]'::jsonb
)
FROM jsonb_array_elements_text(
COALESCE(settings->'enabled_modules', '[]'::jsonb)
) AS elem
)
)
WHERE settings ? 'enabled_modules'
AND settings->'enabled_modules' @> '"platform-admin"'
"""
)
def downgrade() -> None:
"""Revert tenancy back to platform-admin."""
# Revert module_code in platform_modules junction table
op.execute(
"""
UPDATE platform_modules
SET module_code = 'platform-admin'
WHERE module_code = 'tenancy'
"""
)
# Revert JSON settings
op.execute(
"""
UPDATE platforms
SET settings = jsonb_set(
settings,
'{enabled_modules}',
(
SELECT COALESCE(
jsonb_agg(
CASE
WHEN elem = 'tenancy' THEN 'platform-admin'
ELSE elem
END
),
'[]'::jsonb
)
FROM jsonb_array_elements_text(
COALESCE(settings->'enabled_modules', '[]'::jsonb)
) AS elem
)
)
WHERE settings ? 'enabled_modules'
AND settings->'enabled_modules' @> '"tenancy"'
"""
)

View File

@@ -0,0 +1,95 @@
# alembic/versions/zd3n4o5p6q7r8_promote_cms_customers_to_core.py
"""Promote CMS and Customers modules to core.
Revision ID: zd3n4o5p6q7r8
Revises: zc2m3n4o5p6q7
Create Date: 2026-01-27 10:10:00.000000
This migration ensures that CMS and Customers modules are enabled for all platforms,
since they are now core modules that cannot be disabled.
"""
from datetime import datetime, timezone
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "zd3n4o5p6q7r8"
down_revision = "zc2m3n4o5p6q7"
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Enable CMS and Customers modules for all platforms."""
connection = op.get_bind()
# Get all platform IDs
platforms = connection.execute(
sa.text("SELECT id FROM platforms")
).fetchall()
now = datetime.now(timezone.utc)
core_modules = ["cms", "customers"]
for (platform_id,) in platforms:
for module_code in core_modules:
# Check if record exists
existing = connection.execute(
sa.text(
"""
SELECT id FROM platform_modules
WHERE platform_id = :platform_id AND module_code = :module_code
"""
),
{"platform_id": platform_id, "module_code": module_code},
).fetchone()
if existing:
# Update to enabled
connection.execute(
sa.text(
"""
UPDATE platform_modules
SET is_enabled = true, enabled_at = :now
WHERE platform_id = :platform_id AND module_code = :module_code
"""
),
{"platform_id": platform_id, "module_code": module_code, "now": now},
)
else:
# Insert new enabled record
connection.execute(
sa.text(
"""
INSERT INTO platform_modules (platform_id, module_code, is_enabled, enabled_at, config)
VALUES (:platform_id, :module_code, true, :now, '{}')
"""
),
{"platform_id": platform_id, "module_code": module_code, "now": now},
)
# Also update JSON settings to include CMS and Customers if not present
for module_code in core_modules:
op.execute(
f"""
UPDATE platforms
SET settings = jsonb_set(
COALESCE(settings, '{{}}'::jsonb),
'{{enabled_modules}}',
COALESCE(settings->'enabled_modules', '[]'::jsonb) || '"{module_code}"'::jsonb
)
WHERE settings ? 'enabled_modules'
AND NOT (settings->'enabled_modules' @> '"{module_code}"')
"""
)
def downgrade() -> None:
"""
Note: This doesn't actually disable CMS and Customers since that would
break functionality. It just removes the explicit enabling done by upgrade.
"""
# No-op: We don't want to disable core modules
pass