# T02: Company + Contact + Import/Export System ## Context - Project: LeoCRM (greenfield rewrite, Option C) - Repo: /a0/usr/workdir/dev-projects/leocrm - T01 COMPLETE: auth, multi-tenant, RBAC, sessions, audit, notifications all working - T01 commit: 7a7daf8 (pushed to Forgejo) - Tech: FastAPI + SQLAlchemy 2.0 async + PostgreSQL + Redis + Pydantic v2 ## Existing T01 Code to Build On - `app/models/company.py` (40 lines) — Company model skeleton, needs Contact + CompanyContact models - `app/routes/companies.py` (210 lines) — Company CRUD skeleton, needs expansion + Contact routes - `app/schemas/company.py` (23 lines) — Company schema, needs Contact schemas - `app/core/db/__init__.py` — Engine, Session, Base, TenantMixin, set_tenant_context - `app/deps.py` — get_current_user, require_admin, get_tenant_id - `app/core/audit.py` — log_audit function - `app/core/notifications.py` — create_notification - `tests/conftest.py` — Test fixtures with TRUNCATE CASCADE (DO NOT modify truncate list without checking table names) ## Requirements (25) F-COMP-01..08, F-CONT-01..07, F-DATA-01..04, F-MIG-01, F-CORE-06, F-CORE-11, F-CORE-13, F-SEARCH-01, F-TEST-01 ## Acceptance Criteria (24) 1. GET /api/v1/companies → 200 + paginated (total/page/page_size) 2. GET /api/v1/companies?search=Tech → 200 + FTS results (tsvector) 3. GET /api/v1/companies?industry=IT&sort_by=name&sort_order=asc → 200 + filtered+sorted 4. POST /api/v1/companies valid → 201 + company object 5. POST /api/v1/companies missing name → 422 6. GET /api/v1/companies/{id} → 200 + detail inkl. contacts array 7. PUT /api/v1/companies/{id} → 200 + updated 8. DELETE /api/v1/companies/{id} → 204, deleted_at gesetzt (soft-delete) 9. DELETE /api/v1/companies/{id}?cascade=true → 204, company + links geloescht 10. POST /api/v1/companies/{id}/contacts/{cid} → 200, N:M link 11. DELETE /api/v1/companies/{id}/contacts/{cid} → 204, N:M unlink 12. GET /api/v1/companies/export?format=csv → 200 + text/csv 13. GET /api/v1/companies/export?format=xlsx → 200 + openxmlformats 14. GET /api/v1/contacts → 200 + paginated 15. POST /api/v1/contacts mit company_ids array → 201 + N:M links 16. GET /api/v1/contacts/{id} → 200 + detail inkl. companies array 17. PUT /api/v1/contacts/{id} → 200 18. DELETE /api/v1/contacts/{id} → 204, soft-delete 19. DELETE /api/v1/contacts/{id}?gdpr=true → 204, hard-delete + deletion_log 20. POST /api/v1/import CSV + entity_type=companies → 200 + result 21. POST /api/v1/import/preview CSV → 200 + dry-run (no DB changes) 22. GET /api/v1/companies/{id}/emails → 200 (empty array, mail plugin inactive) 23. Audit log entry on every company/contact mutation 24. Soft-deleted company not in GET list (deleted_at IS NULL filter) ## Files to Create/Modify ### New Files: - `app/models/contact.py` — Contact model + CompanyContact (N:M join table) - `app/schemas/contact.py` — Contact schemas (create/update/read/list) - `app/services/company_service.py` — Company CRUD + search + filter + pagination - `app/services/contact_service.py` — Contact CRUD + N:M linking - `app/services/import_export_service.py` — CSV import/export, XLSX export, dry-run preview - `app/routes/contacts.py` — Contact CRUD + N:M endpoints - `app/routes/import_export.py` — Import/export endpoints - `tests/test_companies.py` — Company CRUD + search + filter + export tests - `tests/test_contacts.py` — Contact CRUD + N:M + GDPR delete tests - `tests/test_import_export.py` — CSV import + preview + export tests ### Modify: - `app/models/company.py` — Add soft-delete (deleted_at), FTS tsvector, ensure TenantMixin - `app/routes/companies.py` — Expand to full CRUD + search + filter + export + N:M endpoints - `app/schemas/company.py` — Add pagination, search, filter schemas - `app/models/__init__.py` — Register Contact, CompanyContact - `app/routes/__init__.py` — Register contacts + import_export routers - `app/schemas/__init__.py` — Register contact schemas - `app/services/__init__.py` — Register new services - `tests/conftest.py` — Add contacts, company_contacts to TRUNCATE list - `alembic/versions/` — New migration for contacts + company_contacts + FTS indexes ## Dependencies to Install - `openpyxl>=3.1` — XLSX export (add to requirements.txt) ## Forbidden Patterns - NO JWT tokens (session-based auth from T01) - NO SQLite (PostgreSQL only) - NO wildcard CORS - NO raw SQL without tenant context (use set_config or ORM filtering) - NO hardcoded secrets - NO legacy code reuse - NO .test TLD emails (Pydantic v2 rejects — use .com) - NO `SET LOCAL` with bound params (use `SELECT set_config()` instead) - NO raising HTTPException in middleware (return JSONResponse) - NO POST without status_code=201 ## Test Spec - Commands: `cd /a0/usr/workdir/dev-projects/leocrm && source venv/bin/activate && python -m pytest tests/test_companies.py tests/test_contacts.py tests/test_import_export.py -v --tb=short` - Coverage: `python -m pytest tests/test_companies.py tests/test_contacts.py tests/test_import_export.py --cov=app/routes/companies --cov=app/routes/contacts --cov=app/services --cov-report=term-missing` - Target: 85% for new modules - All 24 ACs must pass ## Token Rule - Use `text_editor:read` for MODIFY, `text_editor:write` for NEW, `code_execution_tool:terminal` for test runs - Reference files by path, not inline - Multi-file output: separate files, not one big file ## JSON Tool Examples Use this format for all tool calls: ```json {"tool_name":"text_editor","tool_args":{"action":"write","path":"/a0/usr/workdir/dev-projects/leocrm/app/models/contact.py","content":"..."}} ``` ```json {"tool_name":"code_execution_tool","tool_args":{"runtime":"terminal","session":0,"code":"cd /a0/usr/workdir/dev-projects/leocrm && source venv/bin/activate && python -m pytest tests/test_companies.py -v --tb=short"}} ```