Files
leocrm-bot 6bf0746b94 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
2026-06-29 00:44:34 +02:00

55 lines
2.2 KiB
Python

"""Company model — with soft-delete, FTS tsvector, and tenant scoping."""
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import String, Text, DateTime, ForeignKey, Index, Computed
from sqlalchemy.dialects.postgresql import UUID as PGUUID, TSVECTOR
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
class Company(Base, TenantMixin):
"""Company entity with full-text search support."""
__tablename__ = "companies"
__table_args__ = (
Index("ix_companies_tenant_deleted", "tenant_id", "deleted_at"),
Index("ix_companies_tenant_name", "tenant_id", "name"),
Index("ix_companies_industry", "tenant_id", "industry"),
Index("ix_companies_search_vec", "search_tsv", postgresql_using="gin"),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
account_number: Mapped[str | None] = mapped_column(String(40), nullable=True)
industry: Mapped[str | None] = mapped_column(String(50), nullable=True)
phone: Mapped[str | None] = mapped_column(String(30), nullable=True)
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
website: Mapped[str | None] = mapped_column(String(500), nullable=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# FTS vector — PostgreSQL generated column, auto-updated on insert/update
search_tsv: Mapped[Any] = mapped_column(
TSVECTOR,
Computed(
"to_tsvector('english', coalesce(name, '') || ' ' || coalesce(description, '') || ' ' || coalesce(industry, ''))",
persisted=True,
),
nullable=True,
)
created_by: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
updated_by: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)