Independent FastAPI backend for Maskan CRM. Owns contacts, organizations, leads, pipelines, activities, products, quotes, users, permissions, audit records and first-party integration credentials, with Alembic migrations against PostgreSQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
561 lines
22 KiB
Python
561 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy import (
|
|
JSON,
|
|
Boolean,
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
Numeric,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.core.database import Base
|
|
|
|
|
|
def new_id() -> str:
|
|
return str(uuid4())
|
|
|
|
|
|
def now_utc() -> datetime:
|
|
return datetime.now(UTC)
|
|
|
|
|
|
class TimestampMixin:
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=now_utc,
|
|
nullable=False,
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=now_utc,
|
|
onupdate=now_utc,
|
|
nullable=False,
|
|
)
|
|
|
|
|
|
class Tenant(Base, TimestampMixin):
|
|
__tablename__ = "crm_tenants"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
slug: Mapped[str] = mapped_column(String(80), unique=True, nullable=False)
|
|
name: Mapped[str] = mapped_column(String(160), nullable=False)
|
|
mode: Mapped[str] = mapped_column(String(24), default="standalone", nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
|
|
|
|
class User(Base, TimestampMixin):
|
|
__tablename__ = "crm_users"
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "email", name="uq_crm_users_tenant_email"),
|
|
Index("ix_crm_users_tenant_role", "tenant_id", "role"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
email: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
full_name: Mapped[str] = mapped_column(String(160), nullable=False)
|
|
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
role: Mapped[str] = mapped_column(String(24), default="member", nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
tenant: Mapped[Tenant] = relationship()
|
|
|
|
|
|
class Organization(Base, TimestampMixin):
|
|
__tablename__ = "crm_organizations"
|
|
__table_args__ = (
|
|
Index("ix_crm_organizations_tenant_name", "tenant_id", "name"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
legal_name: Mapped[str | None] = mapped_column(String(240))
|
|
website: Mapped[str | None] = mapped_column(String(500))
|
|
primary_email: Mapped[str | None] = mapped_column(String(255))
|
|
phone: Mapped[str | None] = mapped_column(String(80))
|
|
industry: Mapped[str | None] = mapped_column(String(120))
|
|
address: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
attributes: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
owner_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_users.id", ondelete="SET NULL"),
|
|
)
|
|
|
|
owner: Mapped[User | None] = relationship()
|
|
contacts: Mapped[list[Contact]] = relationship(back_populates="organization")
|
|
|
|
|
|
class Contact(Base, TimestampMixin):
|
|
__tablename__ = "crm_contacts"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"primary_email",
|
|
name="uq_crm_contacts_tenant_primary_email",
|
|
),
|
|
Index("ix_crm_contacts_tenant_name", "tenant_id", "last_name", "first_name"),
|
|
Index("ix_crm_contacts_tenant_score", "tenant_id", "score"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
first_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
last_name: Mapped[str] = mapped_column(String(100), default="", nullable=False)
|
|
job_title: Mapped[str | None] = mapped_column(String(160))
|
|
primary_email: Mapped[str | None] = mapped_column(String(255))
|
|
emails: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
|
|
phones: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False)
|
|
lifecycle_stage: Mapped[str] = mapped_column(String(40), default="lead", nullable=False)
|
|
lead_source: Mapped[str | None] = mapped_column(String(120))
|
|
score: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
organization_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_organizations.id", ondelete="SET NULL"),
|
|
index=True,
|
|
)
|
|
owner_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_users.id", ondelete="SET NULL"),
|
|
)
|
|
attributes: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
|
|
organization: Mapped[Organization | None] = relationship(back_populates="contacts")
|
|
owner: Mapped[User | None] = relationship()
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return f"{self.first_name} {self.last_name}".strip()
|
|
|
|
|
|
class Pipeline(Base, TimestampMixin):
|
|
__tablename__ = "crm_pipelines"
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "name", name="uq_crm_pipelines_tenant_name"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
name: Mapped[str] = mapped_column(String(140), nullable=False)
|
|
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
stages: Mapped[list[Stage]] = relationship(
|
|
back_populates="pipeline",
|
|
cascade="all, delete-orphan",
|
|
order_by="Stage.position",
|
|
)
|
|
|
|
|
|
class Stage(Base, TimestampMixin):
|
|
__tablename__ = "crm_stages"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"pipeline_id",
|
|
"name",
|
|
name="uq_crm_stages_pipeline_name",
|
|
),
|
|
Index("ix_crm_stages_pipeline_position", "pipeline_id", "position"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
pipeline_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_pipelines.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
name: Mapped[str] = mapped_column(String(140), nullable=False)
|
|
position: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
probability: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
color: Mapped[str] = mapped_column(String(16), default="#64748b", nullable=False)
|
|
|
|
pipeline: Mapped[Pipeline] = relationship(back_populates="stages")
|
|
|
|
|
|
class LeadSource(Base, TimestampMixin):
|
|
__tablename__ = "crm_lead_sources"
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "name", name="uq_crm_lead_sources_tenant_name"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
|
|
|
|
|
class LeadType(Base, TimestampMixin):
|
|
__tablename__ = "crm_lead_types"
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "name", name="uq_crm_lead_types_tenant_name"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
|
|
|
|
|
class Lead(Base, TimestampMixin):
|
|
__tablename__ = "crm_leads"
|
|
__table_args__ = (
|
|
Index("ix_crm_leads_tenant_stage", "tenant_id", "stage_id", "position"),
|
|
Index("ix_crm_leads_tenant_status", "tenant_id", "status"),
|
|
Index("ix_crm_leads_tenant_owner", "tenant_id", "owner_id"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
title: Mapped[str] = mapped_column(String(240), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
value: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False)
|
|
currency: Mapped[str] = mapped_column(String(3), default="USD", nullable=False)
|
|
status: Mapped[str] = mapped_column(String(24), default="open", nullable=False)
|
|
score: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
position: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
lost_reason: Mapped[str | None] = mapped_column(Text)
|
|
expected_close_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
contact_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_contacts.id", ondelete="SET NULL"),
|
|
index=True,
|
|
)
|
|
organization_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_organizations.id", ondelete="SET NULL"),
|
|
index=True,
|
|
)
|
|
owner_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_users.id", ondelete="SET NULL"),
|
|
)
|
|
pipeline_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_pipelines.id", ondelete="RESTRICT"),
|
|
nullable=False,
|
|
)
|
|
stage_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_stages.id", ondelete="RESTRICT"),
|
|
nullable=False,
|
|
)
|
|
source_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_lead_sources.id", ondelete="SET NULL"),
|
|
)
|
|
type_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_lead_types.id", ondelete="SET NULL"),
|
|
)
|
|
attributes: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
|
|
contact: Mapped[Contact | None] = relationship()
|
|
organization: Mapped[Organization | None] = relationship()
|
|
owner: Mapped[User | None] = relationship()
|
|
pipeline: Mapped[Pipeline] = relationship()
|
|
stage: Mapped[Stage] = relationship()
|
|
source: Mapped[LeadSource | None] = relationship()
|
|
lead_type: Mapped[LeadType | None] = relationship()
|
|
|
|
|
|
class Activity(Base, TimestampMixin):
|
|
__tablename__ = "crm_activities"
|
|
__table_args__ = (
|
|
Index("ix_crm_activities_tenant_due", "tenant_id", "is_done", "due_at"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
activity_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
title: Mapped[str] = mapped_column(String(240), nullable=False)
|
|
details: Mapped[str | None] = mapped_column(Text)
|
|
starts_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
is_done: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
owner_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_users.id", ondelete="SET NULL"),
|
|
)
|
|
contact_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_contacts.id", ondelete="CASCADE"),
|
|
index=True,
|
|
)
|
|
organization_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_organizations.id", ondelete="CASCADE"),
|
|
index=True,
|
|
)
|
|
lead_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_leads.id", ondelete="CASCADE"),
|
|
index=True,
|
|
)
|
|
additional: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
|
|
owner: Mapped[User | None] = relationship()
|
|
contact: Mapped[Contact | None] = relationship()
|
|
organization: Mapped[Organization | None] = relationship()
|
|
lead: Mapped[Lead | None] = relationship()
|
|
|
|
|
|
class Product(Base, TimestampMixin):
|
|
__tablename__ = "crm_products"
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "sku", name="uq_crm_products_tenant_sku"),
|
|
Index("ix_crm_products_tenant_name", "tenant_id", "name"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
sku: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
quantity: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
price: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False)
|
|
currency: Mapped[str] = mapped_column(String(3), default="USD", nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
|
|
class Quote(Base, TimestampMixin):
|
|
__tablename__ = "crm_quotes"
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "number", name="uq_crm_quotes_tenant_number"),
|
|
Index("ix_crm_quotes_tenant_status", "tenant_id", "status"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
number: Mapped[str] = mapped_column(String(80), nullable=False)
|
|
subject: Mapped[str] = mapped_column(String(240), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
status: Mapped[str] = mapped_column(String(32), default="draft", nullable=False)
|
|
currency: Mapped[str] = mapped_column(String(3), default="USD", nullable=False)
|
|
billing_address: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
shipping_address: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
discount_amount: Mapped[Decimal] = mapped_column(
|
|
Numeric(14, 2),
|
|
default=0,
|
|
nullable=False,
|
|
)
|
|
tax_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False)
|
|
adjustment_amount: Mapped[Decimal] = mapped_column(
|
|
Numeric(14, 2),
|
|
default=0,
|
|
nullable=False,
|
|
)
|
|
subtotal: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False)
|
|
grand_total: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False)
|
|
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
contact_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_contacts.id", ondelete="SET NULL"),
|
|
)
|
|
organization_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_organizations.id", ondelete="SET NULL"),
|
|
)
|
|
lead_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_leads.id", ondelete="SET NULL"),
|
|
)
|
|
owner_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_users.id", ondelete="SET NULL"),
|
|
)
|
|
|
|
items: Mapped[list[QuoteItem]] = relationship(
|
|
back_populates="quote",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
|
|
class QuoteItem(Base, TimestampMixin):
|
|
__tablename__ = "crm_quote_items"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
quote_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_quotes.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
product_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_products.id", ondelete="SET NULL"),
|
|
)
|
|
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
quantity: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=1, nullable=False)
|
|
unit_price: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False)
|
|
tax_rate: Mapped[Decimal] = mapped_column(Numeric(6, 3), default=0, nullable=False)
|
|
line_total: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0, nullable=False)
|
|
|
|
quote: Mapped[Quote] = relationship(back_populates="items")
|
|
|
|
|
|
class AuditEvent(Base):
|
|
__tablename__ = "crm_audit_events"
|
|
__table_args__ = (
|
|
Index("ix_crm_audit_tenant_entity", "tenant_id", "entity_type", "entity_id"),
|
|
Index("ix_crm_audit_tenant_created", "tenant_id", "created_at"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
actor_id: Mapped[str | None] = mapped_column(
|
|
ForeignKey("crm_users.id", ondelete="SET NULL"),
|
|
)
|
|
action: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
entity_type: Mapped[str] = mapped_column(String(80), nullable=False)
|
|
entity_id: Mapped[str] = mapped_column(String(80), nullable=False)
|
|
payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=now_utc,
|
|
nullable=False,
|
|
)
|
|
|
|
|
|
class IntegrationEvent(Base):
|
|
__tablename__ = "crm_integration_events"
|
|
__table_args__ = (
|
|
Index("ix_crm_integration_events_delivery", "status", "available_at"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
topic: Mapped[str] = mapped_column(String(140), nullable=False)
|
|
payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
status: Mapped[str] = mapped_column(String(24), default="pending", nullable=False)
|
|
attempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
available_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=now_utc,
|
|
nullable=False,
|
|
)
|
|
delivered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=now_utc,
|
|
nullable=False,
|
|
)
|
|
|
|
|
|
class IntegrationCredential(Base, TimestampMixin):
|
|
__tablename__ = "crm_integration_credentials"
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "name", name="uq_crm_integration_key_name"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
|
key_prefix: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
key_hash: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
|
|
class ExternalLink(Base, TimestampMixin):
|
|
__tablename__ = "crm_external_links"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"provider",
|
|
"entity_type",
|
|
"external_id",
|
|
name="uq_crm_external_links_provider_entity",
|
|
),
|
|
Index("ix_crm_external_links_local", "tenant_id", "entity_type", "entity_id"),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
provider: Mapped[str] = mapped_column(String(80), nullable=False)
|
|
entity_type: Mapped[str] = mapped_column(String(80), nullable=False)
|
|
entity_id: Mapped[str] = mapped_column(String(36), nullable=False)
|
|
external_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
|
|
|
|
class IdempotencyRecord(Base):
|
|
__tablename__ = "crm_idempotency_records"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"scope",
|
|
"idempotency_key",
|
|
name="uq_crm_idempotency_scope_key",
|
|
),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
|
|
tenant_id: Mapped[str] = mapped_column(
|
|
ForeignKey("crm_tenants.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
scope: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
response_code: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
response_body: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=now_utc,
|
|
nullable=False,
|
|
)
|
|
|