"""Contacts plugin — Core CRM entity lifecycle management. Registers Contact entity models, permissions, restore config, and history hooks via the same plugin lifecycle as all other business plugins. No Core special case. """ from __future__ import annotations import logging from app.plugins.base import BasePlugin from app.plugins.manifest import ( FieldDefinition, FrontendDashboardWidget, FrontendMenuItem, FrontendPageRoute, MiniAppContribution, PluginManifest, PluginRouteDef, ) logger = logging.getLogger(__name__) class ContactsPlugin(BasePlugin): """Contacts plugin — owns the full Contact domain (Block B1). 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.1.0", display_name="Contacts", description="Core CRM contacts — persons and companies.", dependencies=[], 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=[], miniapps=[ MiniAppContribution( app_id="contacts_stats", name="Kontakt-Zähler", icon="Building2", description="Firmen- und Kontakt-Zähler (persönliche StatCards).", permission="contacts:read", settings_schema={ "fields": [ {"name": "show_companies", "label": "Firmen anzeigen", "type": "boolean", "default": True}, {"name": "show_persons", "label": "Personen anzeigen", "type": "boolean", "default": True}, ] }, col_span=2, row_span=1, hosts=["chat", "dashboard", "window"], component="@/components/dashboard/ContactsStatsWidget", order=5, ), ], dashboard_widgets=[ FrontendDashboardWidget( id="recent_contacts", label_key="dashboard.recentContacts", label="Recent Contacts", component="@/components/dashboard/RecentContactsWidget", icon="Users", order=10, col_span=2, permission="contacts:read", ), ], menu_items=[ FrontendMenuItem(label_key='nav.contacts', label='Kontakte', path='/contacts', icon='Users', order=10, permission='contacts:read'), ], page_routes=[ FrontendPageRoute(path='/contacts', component='@/pages/ContactsList', protected=True, permission='contacts:read'), FrontendPageRoute(path='/contacts/:id', component='@/pages/ContactDetailPage', protected=True, permission='contacts:read'), FrontendPageRoute(path='/contacts/dedup', component='@/pages/DedupMerge', protected=True, permission='contacts:read'), ], permissions=[ "contacts:read", "contacts:write", "contacts:delete", ], # Audit P1/P2: contact field definitions are plugin-owned (moved # from CORE_FIELD_DEFINITIONS) — registered at activation time via # register_field_definitions() and removed on deactivation. field_definitions=[ FieldDefinition(module="contacts", field="firstname", label="First Name", sensitivity="normal"), FieldDefinition(module="contacts", field="surname", label="Last Name", sensitivity="normal"), FieldDefinition(module="contacts", field="displayname", label="Display Name", sensitivity="normal"), FieldDefinition(module="contacts", field="name", label="Name", sensitivity="normal"), FieldDefinition(module="contacts", field="email_1", label="Email 1", sensitivity="normal"), FieldDefinition(module="contacts", field="email_2", label="Email 2", sensitivity="normal"), FieldDefinition(module="contacts", field="phone_1", label="Phone 1", sensitivity="normal"), FieldDefinition(module="contacts", field="phone_2", label="Phone 2", sensitivity="normal"), FieldDefinition(module="contacts", field="mobilephone", label="Mobile", sensitivity="sensitive"), FieldDefinition(module="contacts", field="function", label="Position", sensitivity="normal"), FieldDefinition(module="contacts", field="website", label="Website", sensitivity="normal"), FieldDefinition(module="contacts", field="status", label="Status", sensitivity="normal"), FieldDefinition(module="contacts", field="type", label="Type", sensitivity="normal"), FieldDefinition(module="contacts", field="gender", label="Gender", sensitivity="normal"), FieldDefinition(module="contacts", field="suffix", label="Suffix", sensitivity="normal"), FieldDefinition(module="contacts", field="ext_name_line", label="Extra Name Line", sensitivity="normal"), FieldDefinition(module="contacts", field="country", label="Country", sensitivity="normal"), FieldDefinition(module="contacts", field="code", label="Code", sensitivity="sensitive"), FieldDefinition(module="contacts", field="accounting_code", label="Accounting Code", sensitivity="sensitive"), FieldDefinition(module="contacts", field="vendor_accounting_code", label="Vendor Accounting Code", sensitivity="sensitive"), FieldDefinition(module="contacts", field="vat_code", label="VAT Code", sensitivity="sensitive"), FieldDefinition(module="contacts", field="fiscal_code", label="Fiscal Code", sensitivity="sensitive"), FieldDefinition(module="contacts", field="commerce_code", label="Commerce Code", sensitivity="sensitive"), FieldDefinition(module="contacts", field="purchase_number", label="Purchase Number", sensitivity="sensitive"), FieldDefinition(module="contacts", field="bic", label="BIC", sensitivity="sensitive"), FieldDefinition(module="contacts", field="mailing_street", label="Mailing Street", sensitivity="normal"), FieldDefinition(module="contacts", field="mailing_city", label="Mailing City", sensitivity="normal"), FieldDefinition(module="contacts", field="mailing_postalcode", label="Mailing Postal Code", sensitivity="normal"), FieldDefinition(module="contacts", field="mailing_country", label="Mailing Country", sensitivity="normal"), FieldDefinition(module="contacts", field="visit_street", label="Visit Street", sensitivity="normal"), FieldDefinition(module="contacts", field="visit_city", label="Visit City", sensitivity="normal"), FieldDefinition(module="contacts", field="visit_postalcode", label="Visit Postal Code", sensitivity="normal"), FieldDefinition(module="contacts", field="visit_country", label="Visit Country", sensitivity="normal"), FieldDefinition(module="contacts", field="invoice_street", label="Invoice Street", sensitivity="normal"), FieldDefinition(module="contacts", field="invoice_city", label="Invoice City", sensitivity="normal"), FieldDefinition(module="contacts", field="invoice_postalcode", label="Invoice Postal Code", sensitivity="normal"), FieldDefinition(module="contacts", field="invoice_country", label="Invoice Country", sensitivity="normal"), FieldDefinition(module="contacts", field="notes", label="Notes", sensitivity="sensitive"), FieldDefinition(module="contacts", field="tags", label="Tags", sensitivity="sensitive"), ], is_core=True, author="LeoCRM Team", min_app_version="1.0.0", contract_version="1.0.0", ) def get_job_modules(self) -> list[str]: """Worker discovers the contacts trash-cleanup job here (audit P2).""" return ["app.plugins.builtins.contacts.jobs"] def get_entity_models(self) -> dict[str, type]: from app.models.contact import Contact from app.models.contact_folder import ContactFolder return { "contact": Contact, "contacts": Contact, "company": Contact, # Audit P2: contact_folder is contacts-plugin-owned domain data # (moved from the static core ENTITY_MODELS map). "contact_folder": ContactFolder, } async def on_activate(self, db, service_container, event_bus) -> None: """Activate: register restore config + history hooks for Contact.""" await super().on_activate(db, service_container, event_bus) # Register restore config for Contact entities from app.core.restore_registry import RestoreConfig, get_restore_registry from app.models.contact import Contact get_restore_registry().register(RestoreConfig( entity_type="contact", model_class=Contact, restore_permission="contacts:write", excluded_fields=frozenset({ "search_tsv", "embedding", "default_person_id", "admin_contactperson_id", }), )) # Register history hooks for Contact entities from app.core.history_hooks import register_history_hooks from app.core.hooks import get_hook_registry register_history_hooks( get_hook_registry(), "contact", "contact.after_create", "contact.after_update", "contact.after_delete", owner_tag="contacts", ) logger.info("Contacts plugin activated: restore + history registered") async def on_deactivate(self, db, service_container, event_bus) -> None: """Deactivate: unregister restore config + history hooks for Contact.""" from app.core.restore_registry import get_restore_registry get_restore_registry().unregister("contact") # Unregister history hooks — use owner_tag to remove only this plugin's hooks from app.core.hooks import get_hook_registry hook_reg = get_hook_registry() hook_reg.unregister_actions_by_owner("contact.after_create", "contacts") hook_reg.unregister_actions_by_owner("contact.after_update", "contacts") hook_reg.unregister_actions_by_owner("contact.after_delete", "contacts") from app.plugins.builtins.contracts import get_contract_registry get_contract_registry().unregister(self.manifest.name) await super().on_deactivate(db, service_container, event_bus) logger.info("Contacts plugin deactivated: restore + history unregistered")