Add models for CRM entities: Activity, Contact, Deal, Organization, OrganizationMember, Task; update User model
This commit is contained in:
parent
74330b292f
commit
95a9961549
|
|
@ -1,5 +1,24 @@
|
|||
"""Model exports for Alembic discovery."""
|
||||
from app.models.activity import Activity, ActivityType
|
||||
from app.models.base import Base
|
||||
from app.models.contact import Contact
|
||||
from app.models.deal import Deal, DealStage, DealStatus
|
||||
from app.models.organization import Organization
|
||||
from app.models.organization_member import OrganizationMember, OrganizationRole
|
||||
from app.models.task import Task
|
||||
from app.models.user import User
|
||||
|
||||
__all__ = ["Base", "User"]
|
||||
__all__ = [
|
||||
"Activity",
|
||||
"ActivityType",
|
||||
"Base",
|
||||
"Contact",
|
||||
"Deal",
|
||||
"DealStage",
|
||||
"DealStatus",
|
||||
"Organization",
|
||||
"OrganizationMember",
|
||||
"OrganizationRole",
|
||||
"Task",
|
||||
"User",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
"""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, Enum as SqlEnum, ForeignKey, Integer, func, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class ActivityType(StrEnum):
|
||||
COMMENT = "comment"
|
||||
STATUS_CHANGED = "status_changed"
|
||||
TASK_CREATED = "task_created"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
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"), nullable=False)
|
||||
payload: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSONB,
|
||||
nullable=False,
|
||||
server_default=text("'{}'::jsonb"),
|
||||
)
|
||||
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)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
"""Contact ORM model and schemas."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class Contact(Base):
|
||||
"""Represents a CRM contact belonging to an organization."""
|
||||
|
||||
__tablename__ = "contacts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
organization_id: Mapped[int] = mapped_column(ForeignKey("organizations.id", ondelete="CASCADE"))
|
||||
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="RESTRICT"))
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
email: Mapped[str | None] = mapped_column(String(320), nullable=True)
|
||||
phone: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
organization = relationship("Organization", back_populates="contacts")
|
||||
owner = relationship("User", back_populates="owned_contacts")
|
||||
deals = relationship("Deal", back_populates="contact")
|
||||
|
||||
|
||||
class ContactBase(BaseModel):
|
||||
organization_id: int
|
||||
owner_id: int
|
||||
name: str
|
||||
email: EmailStr | None = None
|
||||
phone: str | None = None
|
||||
|
||||
|
||||
class ContactCreate(ContactBase):
|
||||
pass
|
||||
|
||||
|
||||
class ContactRead(ContactBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
"""Deal ORM model and schemas."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import DateTime, Enum as SqlEnum, ForeignKey, Integer, Numeric, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class DealStatus(StrEnum):
|
||||
NEW = "new"
|
||||
IN_PROGRESS = "in_progress"
|
||||
WON = "won"
|
||||
LOST = "lost"
|
||||
|
||||
|
||||
class DealStage(StrEnum):
|
||||
QUALIFICATION = "qualification"
|
||||
PROPOSAL = "proposal"
|
||||
NEGOTIATION = "negotiation"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class Deal(Base):
|
||||
"""Represents a sales opportunity/deal."""
|
||||
|
||||
__tablename__ = "deals"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
organization_id: Mapped[int] = mapped_column(ForeignKey("organizations.id", ondelete="CASCADE"))
|
||||
contact_id: Mapped[int] = mapped_column(ForeignKey("contacts.id", ondelete="RESTRICT"))
|
||||
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="RESTRICT"))
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
amount: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
|
||||
currency: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
||||
status: Mapped[DealStatus] = mapped_column(
|
||||
SqlEnum(DealStatus, name="deal_status"), nullable=False, default=DealStatus.NEW
|
||||
)
|
||||
stage: Mapped[DealStage] = mapped_column(
|
||||
SqlEnum(DealStage, name="deal_stage"), nullable=False, default=DealStage.QUALIFICATION
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||
)
|
||||
|
||||
organization = relationship("Organization", back_populates="deals")
|
||||
contact = relationship("Contact", back_populates="deals")
|
||||
owner = relationship("User", back_populates="owned_deals")
|
||||
tasks = relationship("Task", back_populates="deal", cascade="all, delete-orphan")
|
||||
activities = relationship("Activity", back_populates="deal", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class DealBase(BaseModel):
|
||||
organization_id: int
|
||||
contact_id: int
|
||||
owner_id: int
|
||||
title: str
|
||||
amount: Decimal | None = None
|
||||
currency: str | None = None
|
||||
status: DealStatus = DealStatus.NEW
|
||||
stage: DealStage = DealStage.QUALIFICATION
|
||||
|
||||
|
||||
class DealCreate(DealBase):
|
||||
pass
|
||||
|
||||
|
||||
class DealRead(DealBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
"""Organization ORM model and schemas."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import DateTime, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class Organization(Base):
|
||||
"""Represents a CRM organization/workspace."""
|
||||
|
||||
__tablename__ = "organizations"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
members = relationship(
|
||||
"OrganizationMember",
|
||||
back_populates="organization",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
contacts = relationship("Contact", back_populates="organization", cascade="all, delete-orphan")
|
||||
deals = relationship("Deal", back_populates="organization", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class OrganizationBase(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class OrganizationCreate(OrganizationBase):
|
||||
pass
|
||||
|
||||
|
||||
class OrganizationRead(OrganizationBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
"""Organization member ORM model."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import DateTime, Enum as SqlEnum, ForeignKey, Integer, UniqueConstraint, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class OrganizationRole(StrEnum):
|
||||
OWNER = "owner"
|
||||
ADMIN = "admin"
|
||||
MANAGER = "manager"
|
||||
MEMBER = "member"
|
||||
|
||||
|
||||
class OrganizationMember(Base):
|
||||
"""Links users to organizations with role-based access."""
|
||||
|
||||
__tablename__ = "organization_members"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("organization_id", "user_id", name="uq_organization_member"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
organization_id: Mapped[int] = mapped_column(ForeignKey("organizations.id", ondelete="CASCADE"))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
role: Mapped[OrganizationRole] = mapped_column(
|
||||
SqlEnum(OrganizationRole, name="organization_role"),
|
||||
nullable=False,
|
||||
default=OrganizationRole.MEMBER,
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
organization = relationship("Organization", back_populates="members")
|
||||
user = relationship("User", back_populates="memberships")
|
||||
|
||||
|
||||
class OrganizationMemberBase(BaseModel):
|
||||
organization_id: int
|
||||
user_id: int
|
||||
role: OrganizationRole
|
||||
|
||||
|
||||
class OrganizationMemberCreate(OrganizationMemberBase):
|
||||
pass
|
||||
|
||||
|
||||
class OrganizationMemberRead(OrganizationMemberBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
"""Task ORM model and schemas."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class Task(Base):
|
||||
"""Represents a task linked to a deal."""
|
||||
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id", ondelete="CASCADE"))
|
||||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
due_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
is_done: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
deal = relationship("Deal", back_populates="tasks")
|
||||
|
||||
|
||||
class TaskBase(BaseModel):
|
||||
deal_id: int
|
||||
title: str
|
||||
description: str | None = None
|
||||
due_date: datetime | None = None
|
||||
is_done: bool = False
|
||||
|
||||
|
||||
class TaskCreate(TaskBase):
|
||||
pass
|
||||
|
||||
|
||||
class TaskRead(TaskBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
|
@ -2,13 +2,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - circular imports only for typing
|
||||
pass
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""SQLAlchemy model for application users."""
|
||||
|
|
@ -18,7 +22,7 @@ class User(Base):
|
|||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
email: Mapped[str] = mapped_column(String(320), unique=True, index=True, nullable=False)
|
||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
full_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
|
|
@ -27,12 +31,17 @@ class User(Base):
|
|||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||
)
|
||||
|
||||
memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan")
|
||||
owned_contacts = relationship("Contact", back_populates="owner")
|
||||
owned_deals = relationship("Deal", back_populates="owner")
|
||||
activities = relationship("Activity", back_populates="author")
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""Shared user fields for Pydantic schemas."""
|
||||
|
||||
email: EmailStr
|
||||
full_name: str | None = None
|
||||
name: str
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue