6bf0746b94
- Company CRUD with soft-delete, FTS search (tsvector + GIN), filter, pagination - Contact CRUD with N:M company linking via company_contacts - CSV/XLSX export, CSV import with dry-run preview - GDPR hard-delete with deletion_log - Audit log on all mutations - 27 new tests (24 ACs), 56 total tests pass - Migration 0002: contacts, company_contacts, FTS search_tsv - Fixed T01 tests: POST→201, PATCH→PUT compatibility
5.8 KiB
5.8 KiB
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 modelsapp/routes/companies.py(210 lines) — Company CRUD skeleton, needs expansion + Contact routesapp/schemas/company.py(23 lines) — Company schema, needs Contact schemasapp/core/db/__init__.py— Engine, Session, Base, TenantMixin, set_tenant_contextapp/deps.py— get_current_user, require_admin, get_tenant_idapp/core/audit.py— log_audit functionapp/core/notifications.py— create_notificationtests/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)
- GET /api/v1/companies → 200 + paginated (total/page/page_size)
- GET /api/v1/companies?search=Tech → 200 + FTS results (tsvector)
- GET /api/v1/companies?industry=IT&sort_by=name&sort_order=asc → 200 + filtered+sorted
- POST /api/v1/companies valid → 201 + company object
- POST /api/v1/companies missing name → 422
- GET /api/v1/companies/{id} → 200 + detail inkl. contacts array
- PUT /api/v1/companies/{id} → 200 + updated
- DELETE /api/v1/companies/{id} → 204, deleted_at gesetzt (soft-delete)
- DELETE /api/v1/companies/{id}?cascade=true → 204, company + links geloescht
- POST /api/v1/companies/{id}/contacts/{cid} → 200, N:M link
- DELETE /api/v1/companies/{id}/contacts/{cid} → 204, N:M unlink
- GET /api/v1/companies/export?format=csv → 200 + text/csv
- GET /api/v1/companies/export?format=xlsx → 200 + openxmlformats
- GET /api/v1/contacts → 200 + paginated
- POST /api/v1/contacts mit company_ids array → 201 + N:M links
- GET /api/v1/contacts/{id} → 200 + detail inkl. companies array
- PUT /api/v1/contacts/{id} → 200
- DELETE /api/v1/contacts/{id} → 204, soft-delete
- DELETE /api/v1/contacts/{id}?gdpr=true → 204, hard-delete + deletion_log
- POST /api/v1/import CSV + entity_type=companies → 200 + result
- POST /api/v1/import/preview CSV → 200 + dry-run (no DB changes)
- GET /api/v1/companies/{id}/emails → 200 (empty array, mail plugin inactive)
- Audit log entry on every company/contact mutation
- 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 + paginationapp/services/contact_service.py— Contact CRUD + N:M linkingapp/services/import_export_service.py— CSV import/export, XLSX export, dry-run previewapp/routes/contacts.py— Contact CRUD + N:M endpointsapp/routes/import_export.py— Import/export endpointstests/test_companies.py— Company CRUD + search + filter + export teststests/test_contacts.py— Contact CRUD + N:M + GDPR delete teststests/test_import_export.py— CSV import + preview + export tests
Modify:
app/models/company.py— Add soft-delete (deleted_at), FTS tsvector, ensure TenantMixinapp/routes/companies.py— Expand to full CRUD + search + filter + export + N:M endpointsapp/schemas/company.py— Add pagination, search, filter schemasapp/models/__init__.py— Register Contact, CompanyContactapp/routes/__init__.py— Register contacts + import_export routersapp/schemas/__init__.py— Register contact schemasapp/services/__init__.py— Register new servicestests/conftest.py— Add contacts, company_contacts to TRUNCATE listalembic/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 LOCALwith bound params (useSELECT 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:readfor MODIFY,text_editor:writefor NEW,code_execution_tool:terminalfor 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:
{"tool_name":"text_editor","tool_args":{"action":"write","path":"/a0/usr/workdir/dev-projects/leocrm/app/models/contact.py","content":"..."}}
{"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"}}