"""Activity timeline ORM model and schemas.""" from __future__ import annotations from datetime import datetime from enum import StrEnum from typing import Any from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import DateTime, ForeignKey, Integer, func, text from sqlalchemy import Enum as SqlEnum from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.types import JSON as SA_JSON from sqlalchemy.types import TypeDecorator from app.models.base import Base, enum_values class ActivityType(StrEnum): COMMENT = "comment" STATUS_CHANGED = "status_changed" STAGE_CHANGED = "stage_changed" TASK_CREATED = "task_created" SYSTEM = "system" class JSONBCompat(TypeDecorator): """Uses JSONB on Postgres and plain JSON elsewhere for testability.""" impl = JSONB cache_ok = True def load_dialect_impl(self, dialect): # type: ignore[override] if dialect.name == "sqlite": from sqlalchemy.dialects.sqlite import JSON as SQLITE_JSON # local import return dialect.type_descriptor(SQLITE_JSON()) return dialect.type_descriptor(JSONB()) class Activity(Base): """Represents a timeline event for a deal.""" __tablename__ = "activities" id: Mapped[int] = mapped_column(Integer, primary_key=True) deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id", ondelete="CASCADE")) author_id: Mapped[int | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) type: Mapped[ActivityType] = mapped_column( SqlEnum(ActivityType, name="activity_type", values_callable=enum_values), nullable=False, ) payload: Mapped[dict[str, Any]] = mapped_column( JSONBCompat().with_variant(SA_JSON(), "sqlite"), nullable=False, server_default=text("'{}'"), ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False, ) deal = relationship("Deal", back_populates="activities") author = relationship("User", back_populates="activities") class ActivityBase(BaseModel): deal_id: int author_id: int | None = None type: ActivityType payload: dict[str, Any] = Field(default_factory=dict) class ActivityCreate(ActivityBase): pass class ActivityRead(ActivityBase): id: int created_at: datetime model_config = ConfigDict(from_attributes=True)