T02: companies + contacts + import/export + N:M + soft-delete + GDPR + FTS

- 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
This commit is contained in:
leocrm-bot
2026-06-29 00:44:34 +02:00
parent 3ab4925783
commit dd16940bb2
21 changed files with 2518 additions and 125 deletions
+61
View File
@@ -0,0 +1,61 @@
"""Contact schemas — create, update, read, list."""
from __future__ import annotations
from pydantic import BaseModel, Field
class ContactCreate(BaseModel):
first_name: str = Field(..., min_length=1, max_length=100)
last_name: str = Field(..., min_length=1, max_length=100)
email: str | None = Field(None, max_length=255)
phone: str | None = Field(None, max_length=30)
mobile: str | None = Field(None, max_length=30)
position: str | None = Field(None, max_length=100)
department: str | None = Field(None, max_length=100)
linkedin_url: str | None = Field(None, max_length=500)
notes: str | None = None
company_ids: list[str] | None = None
class ContactUpdate(BaseModel):
first_name: str | None = Field(None, min_length=1, max_length=100)
last_name: str | None = Field(None, min_length=1, max_length=100)
email: str | None = Field(None, max_length=255)
phone: str | None = Field(None, max_length=30)
mobile: str | None = Field(None, max_length=30)
position: str | None = Field(None, max_length=100)
department: str | None = Field(None, max_length=100)
linkedin_url: str | None = Field(None, max_length=500)
notes: str | None = None
class ContactResponse(BaseModel):
id: str
first_name: str
last_name: str
email: str | None = None
phone: str | None = None
mobile: str | None = None
position: str | None = None
department: str | None = None
linkedin_url: str | None = None
notes: str | None = None
created_at: str | None = None
updated_at: str | None = None
class ContactDetailResponse(ContactResponse):
companies: list[dict] = Field(default_factory=list)
class ContactListResponse(BaseModel):
items: list[ContactResponse]
total: int
page: int
page_size: int
class CompanyLinkRequest(BaseModel):
role_at_company: str | None = Field(None, max_length=100)
is_primary: bool = False