diff --git a/PROGRESS.md b/PROGRESS.md index df67f15..c5b2e51 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -32,6 +32,27 @@ --- +## Phase 1B: Backend Plugins — entity_type='company' → 'contact' + +| Task | Status | Datum | Notiz | +|---|---|---|---| +| 1.9 | ✅ done | 2026-07-23 | entity_links Plugin: entity_type pattern ^(company|contact)$ → ^contact$, company_router entfernt, on_company_deleted → on_contact_deleted, company.deleted → contact.deleted | +| 1.10 | ✅ done | 2026-07-23 | unified_search Plugin: CompanySearchProvider → ContactSearchProvider, index_company → index_contact, events company.created/updated → contact.created/updated, search_engine mapping aktualisiert | +| 1.11 | ✅ done | 2026-07-23 | calendar Plugin: entity_type pattern ^(company|contact)$ → ^contact$ (CalendarType='company' bleibt) | +| 1.12 | ✅ done | 2026-07-23 | tags Plugin: entity_type pattern ^(company|contact|file|folder)$ → ^(contact|file|folder)$ | +| 1.13 | ✅ done | 2026-07-23 | mail Plugin: company_id → contact_id in model, schemas, routes, services | +| 1.14 | ✅ done | 2026-07-23 | test_sample Plugin: company.created → contact.created | +| 1.15 | ✅ done | 2026-07-23 | Event Names Unify: Alle company.created/updated/deleted → contact.created/updated/deleted | +| 1.16 | ✅ done | 2026-07-23 | DB Migration 0027: entity_type 'company' → 'contact' in entity_links, tag_assignments, calendar_entry_links, addresses; mails company_id → contact_id | +| 1.17 | ✅ done | 2026-07-23 | Backend Tests Update: test_companies.py, test_unified_search.py, test_entity_links.py, test_calendar.py, test_tags.py, test_ai_proactive.py, test_tenant.py — entity_type='company' → 'contact' | +| 1.18 | ✅ done | 2026-07-23 | Permission-Registry-Cleanup: companies:read/write/delete aus CORE_PERMISSIONS entfernt (bereits in 1A) | +| 1.19 | ✅ done | 2026-07-23 | Addresses entity_type='company' → 'contact' in address_service.py (bereits in 1A) | +| 1.20 | ✅ done | 2026-07-23 | conftest.py Update: Company → Contact, CompanyContact → ContactPerson, TRUNCATE ohne companies/company_contacts | + +**Phase 1B Gesamt: ✅ Complete** + +--- + ## Phase 1C: Frontend — Unified Contact UI | Task | Status | Datum | Notiz | diff --git a/app/plugins/builtins/ai_proactive/context_tools.py b/app/plugins/builtins/ai_proactive/context_tools.py index 66aa0b9..2e9e90b 100644 --- a/app/plugins/builtins/ai_proactive/context_tools.py +++ b/app/plugins/builtins/ai_proactive/context_tools.py @@ -263,7 +263,7 @@ def register_context_tools(registry) -> None: parameters={ "type": "object", "properties": { - "entity_type": {"type": "string", "description": "contact, company, mail, file, event"}, + "entity_type": {"type": "string", "description": "contact, mail, file, event"}, "entity_id": {"type": "string", "description": "UUID der Entity"}, "limit": {"type": "integer", "default": 5, "description": "Max results per type"}, }, @@ -300,7 +300,7 @@ def register_context_tools(registry) -> None: parameters={ "type": "object", "properties": { - "entity_type": {"type": "string", "description": "contact or company"}, + "entity_type": {"type": "string", "description": "contact"}, "entity_id": {"type": "string", "description": "UUID der Entity"}, }, "required": ["entity_type", "entity_id"], diff --git a/app/plugins/builtins/ai_proactive/jobs.py b/app/plugins/builtins/ai_proactive/jobs.py index add3ab9..d1cd6e2 100644 --- a/app/plugins/builtins/ai_proactive/jobs.py +++ b/app/plugins/builtins/ai_proactive/jobs.py @@ -127,7 +127,7 @@ async def deep_analysis( contact = contact_result.scalar_one_or_none() extended_context["contact"] = _serialize_row(contact) if contact else None - elif entity_type == "company": + elif entity_type == "contact": from app.models.contact import Contact as Company comp_result = await db.execute( @@ -136,10 +136,10 @@ async def deep_analysis( .where(Company.tenant_id == tid) .limit(1) ) - company = comp_result.scalar_one_or_none() - extended_context["company"] = _serialize_row(company) if company else None + contact = comp_result.scalar_one_or_none() + extended_context["contact"] = _serialize_row(contact) if contact else None - # All mails for company + # All mails for contact mail_result = await db.execute( select(Mail) .where(Mail.contact_id == eid) diff --git a/app/plugins/builtins/ai_proactive/services.py b/app/plugins/builtins/ai_proactive/services.py index 0b44772..a375dad 100644 --- a/app/plugins/builtins/ai_proactive/services.py +++ b/app/plugins/builtins/ai_proactive/services.py @@ -22,7 +22,7 @@ from app.core.cache import get_cache from app.core.db import create_db_session, get_session_factory from app.core.notifications import create_notification from app.models.audit import AuditLog -from app.models.contact import Contact as Company +from app.models.contact import Contact, ContactPerson from app.models.contact import Contact, ContactPerson from app.plugins.builtins.ai_proactive.models import ( ContextLog, @@ -178,28 +178,28 @@ async def gather_context( ) context["mails"] = [_serialize_row(m) for m in mail_result.scalars().all()] - # Company via company_contacts + # Company via contact_persons cc_result = await db.execute( select(ContactPerson) .where(ContactPerson.contact_id == entity_id) .where(ContactPerson.tenant_id == tenant_id) .limit(5) ) - companies: list[dict[str, Any]] = [] + contacts_list: list[dict[str, Any]] = [] for cc in cc_result.scalars().all(): comp_result = await db.execute( - select(Company) - .where(Company.id == cc.company_id) - .where(Company.tenant_id == tenant_id) + select(Contact) + .where(Contact.id == cc.contact_id) + .where(Contact.tenant_id == tenant_id) .limit(1) ) comp = comp_result.scalar_one_or_none() if comp: comp_data = _serialize_row(comp) - comp_data["role_at_company"] = cc.role_at_company + comp_data["role"] = cc.role comp_data["is_primary"] = cc.is_primary - companies.append(comp_data) - context["company"] = companies[0] if companies else None + contacts_list.append(comp_data) + context["contact"] = contacts_list[0] if contacts_list else None context["companies"] = companies # Upcoming calendar events @@ -262,25 +262,25 @@ async def gather_context( if mail and mail.contact_id: comp_result = await db.execute( - select(Company) - .where(Company.id == mail.contact_id) - .where(Company.tenant_id == tenant_id) + select(Contact) + .where(Contact.id == mail.contact_id) + .where(Contact.tenant_id == tenant_id) .limit(1) ) comp = comp_result.scalar_one_or_none() - context["company"] = _serialize_row(comp) if comp else None + context["contact"] = _serialize_row(comp) if comp else None - elif entity_type == "company": + elif entity_type == "contact": result = await db.execute( - select(Company) - .where(Company.id == entity_id) - .where(Company.tenant_id == tenant_id) + select(Contact) + .where(Contact.id == entity_id) + .where(Contact.tenant_id == tenant_id) .limit(1) ) company = result.scalar_one_or_none() - context["company"] = _serialize_row(company) if company else None + context["contact"] = _serialize_row(company) if company else None - # Contacts via company_contacts + # Contacts via contact_persons cc_result = await db.execute( select(ContactPerson) .where(ContactPerson.contact_id == entity_id) @@ -297,12 +297,12 @@ async def gather_context( contact = contact_result.scalar_one_or_none() if contact: contact_data = _serialize_row(contact) - contact_data["role_at_company"] = cc.role_at_company + contact_data["role"] = cc.role contact_data["is_primary"] = cc.is_primary contacts.append(contact_data) context["contacts"] = contacts - # Mails for this company + # Mails for this contact from app.plugins.builtins.mail.models import Mail mail_result = await db.execute( diff --git a/app/plugins/builtins/calendar/models.py b/app/plugins/builtins/calendar/models.py index 0338166..c45cff2 100644 --- a/app/plugins/builtins/calendar/models.py +++ b/app/plugins/builtins/calendar/models.py @@ -76,7 +76,7 @@ class CalendarEntry(Base, TenantMixin): class CalendarEntryLink(Base, TenantMixin): - """Link between a calendar entry and an entity (company/contact).""" + """Link between a calendar entry and an entity (contact).""" __tablename__ = "calendar_entry_links" __table_args__ = (Index("ix_entry_links_entry", "entry_id"),) diff --git a/app/plugins/builtins/unified_search/jobs.py b/app/plugins/builtins/unified_search/jobs.py index 3dbc55f..d2e7fdc 100644 --- a/app/plugins/builtins/unified_search/jobs.py +++ b/app/plugins/builtins/unified_search/jobs.py @@ -121,27 +121,6 @@ async def index_contact(ctx: dict[str, Any], contact_id: str) -> None: logger.exception("Failed to index contact %s", contact_id) -async def index_company(ctx: dict[str, Any], company_id: str) -> None: - """Index a company (contact with type='company'): generate and store embedding.""" - from sqlalchemy import text - from app.plugins.builtins.unified_search.embedding import index_entity - - factory = get_session_factory() - async with factory() as db: - try: - eid = _parse_id(company_id) - result = await db.execute( - text("SELECT tenant_id FROM contacts WHERE id = :cid"), - {"cid": eid}, - ) - row = result.mappings().first() - if not row: - return - await index_entity("contact", eid, row["tenant_id"], db) - except Exception: - logger.exception("Failed to index company %s", company_id) - - async def index_event(ctx: dict[str, Any], event_id: str) -> None: """Index a calendar event: generate and store embedding.""" from sqlalchemy import text diff --git a/app/plugins/builtins/unified_search/query_understanding.py b/app/plugins/builtins/unified_search/query_understanding.py index 9971835..1aa44a6 100644 --- a/app/plugins/builtins/unified_search/query_understanding.py +++ b/app/plugins/builtins/unified_search/query_understanding.py @@ -18,7 +18,7 @@ DEFAULT_LLM_MODEL = os.environ.get('SEARCH_LLM_MODEL', 'ollama/deepseek-v4-flash QUERY_ANALYZE_SYSTEM = ( "Du bist ein Query-Analyzer fuer ein CRM. " "Analysiere die Suchanfrage und gib JSON zurueck: " - '{"normalized_query": str, "entities": {"person": str|null, "company": str|null, "topic": str|null}, ' + '{"normalized_query": str, "entities": {"person": str|null, "contact": str|null, "topic": str|null}, ' '"intent": str, "semantic_terms": [str], "suggested_filters": {}}' ) diff --git a/app/routes/addresses.py b/app/routes/addresses.py index d9bfb75..ffd7884 100644 --- a/app/routes/addresses.py +++ b/app/routes/addresses.py @@ -17,7 +17,7 @@ router = APIRouter(prefix="/api/v1/addresses", tags=["addresses"]) @router.get("") async def list_addresses( - entity_type: str = Query(..., pattern="^(company|contact)$"), + entity_type: str = Query(..., pattern="^contact$"), entity_id: str = Query(...), db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("addresses:read")), diff --git a/app/services/ai_copilot_service.py b/app/services/ai_copilot_service.py index 5c879c5..1caffef 100644 --- a/app/services/ai_copilot_service.py +++ b/app/services/ai_copilot_service.py @@ -321,7 +321,7 @@ async def _execute_api_action( return {"error": f"Unsupported entity: {entity}", "status_code": 400, "success": False} -async def _exec_contacts\( +async def _exec_contacts( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, diff --git a/tests/test_unified_search.py b/tests/test_unified_search.py index e39eeb9..b00df42 100644 --- a/tests/test_unified_search.py +++ b/tests/test_unified_search.py @@ -49,7 +49,6 @@ from app.plugins.builtins.unified_search.search_engine import ( from app.plugins.builtins.unified_search.jobs import ( index_mails, index_contact, - index_company, index_event, reindex, embedding_batch, @@ -993,8 +992,8 @@ async def test_index_contact(mock_index_entity, mock_factory, db_session: AsyncS @pytest.mark.asyncio @patch("app.plugins.builtins.unified_search.jobs.get_session_factory") @patch("app.plugins.builtins.unified_search.embedding.index_entity", new_callable=AsyncMock, return_value=True) -async def test_index_company(mock_index_entity, mock_factory, db_session: AsyncSession): - """index_company calls index_entity.""" +async def test_index_contact_company_type(mock_index_entity, mock_factory, db_session: AsyncSession): + """index_contact calls index_entity for company-type contact.""" from app.models.contact import Contact as Company from app.models.tenant import Tenant from app.models.user import User @@ -1027,7 +1026,7 @@ async def test_index_company(mock_index_entity, mock_factory, db_session: AsyncS sf = async_sessionmaker(bind=db_session.bind, expire_on_commit=False, class_=AsyncSession) mock_factory.return_value = sf - await index_company({}, str(company.id)) + await index_contact({}, str(company.id)) mock_index_entity.assert_called()