From 5ad107ff83a641b94d07e00db75905a2ac211710 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Sun, 23 Aug 2026 20:20:02 +0200 Subject: [PATCH] refactor(b1): contacts domain fully plugin-owned - routes moved from core to contacts plugin with require_active_plugin guard --- app/main.py | 11 ++---- .../builtins/contacts/company_routes.py} | 0 .../contacts/folder_permission_routes.py} | 0 .../builtins/contacts/folder_routes.py} | 0 app/plugins/builtins/contacts/plugin.py | 36 +++++++++++++++---- .../builtins/contacts/routes.py} | 0 app/routes/__init__.py | 1 - tests/conftest.py | 15 ++++++++ 8 files changed, 48 insertions(+), 15 deletions(-) rename app/{routes/companies.py => plugins/builtins/contacts/company_routes.py} (100%) rename app/{routes/contact_folder_permissions.py => plugins/builtins/contacts/folder_permission_routes.py} (100%) rename app/{routes/contact_folders.py => plugins/builtins/contacts/folder_routes.py} (100%) rename app/{routes/contacts.py => plugins/builtins/contacts/routes.py} (100%) diff --git a/app/main.py b/app/main.py index d875b20..1ec9f6c 100644 --- a/app/main.py +++ b/app/main.py @@ -39,9 +39,6 @@ from app.routes import ( # noqa: E402 compliance, backups, bank_accounts, - contact_folder_permissions, - contact_folders, - contacts, currencies, custom_field_definitions, custom_fields, @@ -545,11 +542,9 @@ def create_app() -> FastAPI: app.include_router(groups.router) app.include_router(tenants.router) app.include_router(notifications.router) - from app.routes.companies import router as companies_router - app.include_router(companies_router) - app.include_router(contacts.router) - app.include_router(contact_folders.router) - app.include_router(contact_folder_permissions.router) + # NOTE: contacts/companies/contact-folders routes are plugin-owned now + # (Block B1) and mounted via the manifest.routes mechanism below with + # require_active_plugin("contacts") protection. app.include_router(entity_permissions.router) app.include_router(dashboard.router) app.include_router(entity_history.router) diff --git a/app/routes/companies.py b/app/plugins/builtins/contacts/company_routes.py similarity index 100% rename from app/routes/companies.py rename to app/plugins/builtins/contacts/company_routes.py diff --git a/app/routes/contact_folder_permissions.py b/app/plugins/builtins/contacts/folder_permission_routes.py similarity index 100% rename from app/routes/contact_folder_permissions.py rename to app/plugins/builtins/contacts/folder_permission_routes.py diff --git a/app/routes/contact_folders.py b/app/plugins/builtins/contacts/folder_routes.py similarity index 100% rename from app/routes/contact_folders.py rename to app/plugins/builtins/contacts/folder_routes.py diff --git a/app/plugins/builtins/contacts/plugin.py b/app/plugins/builtins/contacts/plugin.py index a77746a..4aec111 100644 --- a/app/plugins/builtins/contacts/plugin.py +++ b/app/plugins/builtins/contacts/plugin.py @@ -9,25 +9,49 @@ from __future__ import annotations import logging from app.plugins.base import BasePlugin -from app.plugins.manifest import PluginManifest +from app.plugins.manifest import PluginManifest, PluginRouteDef logger = logging.getLogger(__name__) class ContactsPlugin(BasePlugin): - """Contacts plugin — manages Contact entity lifecycle (models, permissions, restore, history). + """Contacts plugin — owns the full Contact domain (Block B1). - Routes remain in app/routes/contacts.py as core routes, but entity lifecycle - (permissions, entity models, restore, history) is managed through on_activate/on_deactivate. + Routes (contacts, companies, contact folders, folder permissions) live in + this plugin and are mounted via manifest.routes with + require_active_plugin("contacts") protection. Entity lifecycle + (permissions, entity models, restore, history) is managed through + on_activate/on_deactivate like every other business plugin. """ manifest = PluginManifest( name="contacts", - version="1.0.0", + version="1.1.0", display_name="Contacts", description="Core CRM contacts — persons and companies.", dependencies=[], - routes=[], # Routes are registered as core routes in main.py + routes=[ + PluginRouteDef( + path="/api/v1/contacts", + module="app.plugins.builtins.contacts.routes", + router_attr="router", + ), + PluginRouteDef( + path="/api/v1/companies", + module="app.plugins.builtins.contacts.company_routes", + router_attr="router", + ), + PluginRouteDef( + path="/api/v1/contact-folders", + module="app.plugins.builtins.contacts.folder_routes", + router_attr="router", + ), + PluginRouteDef( + path="/api/v1/contact-folders", + module="app.plugins.builtins.contacts.folder_permission_routes", + router_attr="router", + ), + ], events=[], migrations=[], permissions=[ diff --git a/app/routes/contacts.py b/app/plugins/builtins/contacts/routes.py similarity index 100% rename from app/routes/contacts.py rename to app/plugins/builtins/contacts/routes.py diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 36f6c34..032f36c 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -7,7 +7,6 @@ from app.routes import ( compliance, # noqa: F401 auth, # noqa: F401 bank_accounts, # noqa: F401 - contacts, # noqa: F401 currencies, # noqa: F401 dashboard, # noqa: F401 entity_history, # noqa: F401 diff --git a/tests/conftest.py b/tests/conftest.py index d263e3b..a35a2e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -318,6 +318,21 @@ async def app(engine: AsyncEngine, redis_client: aioredis.Redis): """FastAPI app with test engine injected (session-scoped for speed).""" reset_engine_for_testing(engine) app = create_app() + # Block B1: contacts routes are plugin-owned and guarded by + # require_active_plugin("contacts"). The test DB is empty, so create_app() + # leaves active plugins empty — activate the core contacts plugin for the + # generic app/client fixtures (same pattern as the specialized ai_app). + from app.core.permission_registry import ( + init_permission_registry, + register_plugin_permissions, + ) + + init_permission_registry(active_plugin_names={"contacts"}) + from app.plugins.builtins.contacts.plugin import ContactsPlugin + + contacts_manifest = ContactsPlugin().manifest + if contacts_manifest.permissions: + register_plugin_permissions("contacts", contacts_manifest.permissions) yield app await close_engine()