Add the Sales CRM service through collaboration, sync architecture/registry docs, expose crm.torbatyar.ir, and include a production deploy script. Co-authored-by: Cursor <cursoragent@cursor.com>
498 lines
21 KiB
Python
498 lines
21 KiB
Python
"""Phase 6.3 — Enterprise Sales Collaboration Platform models."""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
DateTime,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
from sqlalchemy.types import JSON
|
|
|
|
from app.core.database import Base
|
|
from app.models.base import (
|
|
ActorAuditMixin,
|
|
SoftDeleteMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
UUIDPrimaryKeyMixin,
|
|
)
|
|
from app.models.types import (
|
|
CallDirection,
|
|
CollaborationEntityType,
|
|
EmailDeliveryStatus,
|
|
GUID,
|
|
RecurrenceRule,
|
|
TeamRoleCode,
|
|
TimelineEventType,
|
|
)
|
|
|
|
|
|
class ActivityTypeDefinition(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "activity_types"
|
|
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(150), nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "code", name="uq_activity_types_tenant_code"),
|
|
)
|
|
|
|
|
|
class ActivityStatusDefinition(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "activity_statuses"
|
|
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(150), nullable=False)
|
|
is_terminal: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "code", name="uq_activity_statuses_tenant_code"),
|
|
)
|
|
|
|
|
|
class ActivityPriorityDefinition(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "activity_priorities"
|
|
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(150), nullable=False)
|
|
rank: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "code", name="uq_activity_priorities_tenant_code"
|
|
),
|
|
)
|
|
|
|
|
|
class ActivityParticipant(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "activity_participants"
|
|
|
|
activity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
contact_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
role_label: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
is_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
|
|
__table_args__ = (
|
|
Index("ix_activity_participants_activity", "tenant_id", "activity_id"),
|
|
)
|
|
|
|
|
|
class ActivityReminder(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "activity_reminders"
|
|
|
|
activity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
remind_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
channel: Mapped[str] = mapped_column(String(50), default="in_app", nullable=False)
|
|
is_sent: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
|
|
__table_args__ = (
|
|
Index("ix_activity_reminders_activity", "tenant_id", "activity_id"),
|
|
)
|
|
|
|
|
|
class SalesTask(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin):
|
|
__tablename__ = "tasks"
|
|
|
|
activity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
owner_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
assignee_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
status: Mapped[str] = mapped_column(String(30), default="open", nullable=False)
|
|
priority: Mapped[str] = mapped_column(String(20), default="medium", nullable=False)
|
|
due_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
completed_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
recurrence: Mapped[RecurrenceRule] = mapped_column(
|
|
default=RecurrenceRule.NONE, nullable=False
|
|
)
|
|
opportunity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
lead_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
contact_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
organization_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
completion_percent: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
|
|
__table_args__ = (
|
|
Index("ix_tasks_tenant_status", "tenant_id", "status"),
|
|
Index("ix_tasks_assignee", "tenant_id", "assignee_user_id"),
|
|
)
|
|
|
|
|
|
class TaskChecklistItem(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "task_checklists"
|
|
|
|
task_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
position: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
is_completed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
completed_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
|
|
__table_args__ = (Index("ix_task_checklists_task", "tenant_id", "task_id"),)
|
|
|
|
|
|
class Meeting(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "meetings"
|
|
|
|
activity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
subject: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
start_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
end_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
location: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
online_meeting_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
agenda: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
outcome: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
owner_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
opportunity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
lead_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
contact_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
organization_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
is_finished: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
finished_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
|
|
__table_args__ = (
|
|
Index("ix_meetings_tenant_start", "tenant_id", "start_at"),
|
|
Index("ix_meetings_opportunity", "tenant_id", "opportunity_id"),
|
|
)
|
|
|
|
|
|
class MeetingAttendee(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "meeting_attendees"
|
|
|
|
meeting_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
contact_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
display_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
response_status: Mapped[str] = mapped_column(
|
|
String(30), default="pending", nullable=False
|
|
)
|
|
|
|
__table_args__ = (Index("ix_meeting_attendees_meeting", "tenant_id", "meeting_id"),)
|
|
|
|
|
|
class CallOutcome(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "call_outcomes"
|
|
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(150), nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "code", name="uq_call_outcomes_tenant_code"),
|
|
)
|
|
|
|
|
|
class CallLog(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "call_logs"
|
|
|
|
activity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
direction: Mapped[CallDirection] = mapped_column(nullable=False)
|
|
subject: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
duration_seconds: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
outcome_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
outcome_label: Mapped[str | None] = mapped_column(String(150), nullable=True)
|
|
recording_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
owner_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
opportunity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
contact_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
lead_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
organization_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
called_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
|
|
__table_args__ = (
|
|
Index("ix_call_logs_opportunity", "tenant_id", "opportunity_id"),
|
|
Index("ix_call_logs_called_at", "tenant_id", "called_at"),
|
|
)
|
|
|
|
|
|
class EmailThread(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "email_threads"
|
|
|
|
subject: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
opportunity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
contact_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
organization_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
lead_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
last_message_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
|
|
__table_args__ = (
|
|
Index("ix_email_threads_opportunity", "tenant_id", "opportunity_id"),
|
|
)
|
|
|
|
|
|
class EmailMessage(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
"""CRM email metadata only — delivery owned by Notification Service."""
|
|
|
|
__tablename__ = "emails"
|
|
|
|
thread_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
subject: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
status: Mapped[EmailDeliveryStatus] = mapped_column(
|
|
default=EmailDeliveryStatus.DRAFT, nullable=False
|
|
)
|
|
opened: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
clicked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
opportunity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
notification_ref: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
sent_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
|
|
__table_args__ = (Index("ix_emails_thread", "tenant_id", "thread_id"),)
|
|
|
|
|
|
class SalesTimelineEvent(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
"""Immutable sales timeline entries."""
|
|
|
|
__tablename__ = "sales_timeline"
|
|
|
|
event_type: Mapped[TimelineEventType] = mapped_column(nullable=False)
|
|
entity_type: Mapped[CollaborationEntityType] = mapped_column(nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
related_opportunity_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
related_lead_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
related_contact_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
related_organization_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
GUID(), nullable=True
|
|
)
|
|
actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
payload: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
occurred_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False
|
|
)
|
|
is_immutable: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
__table_args__ = (
|
|
Index("ix_sales_timeline_entity", "tenant_id", "entity_type", "entity_id"),
|
|
Index("ix_sales_timeline_occurred", "tenant_id", "occurred_at"),
|
|
Index("ix_sales_timeline_opportunity", "tenant_id", "related_opportunity_id"),
|
|
)
|
|
|
|
|
|
class TimelineEventLink(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
"""Optional secondary index rows for timeline fan-out (same immutable event)."""
|
|
|
|
__tablename__ = "timeline_events"
|
|
|
|
timeline_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
link_entity_type: Mapped[CollaborationEntityType] = mapped_column(nullable=False)
|
|
link_entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
|
|
__table_args__ = (
|
|
Index("ix_timeline_events_link", "tenant_id", "link_entity_type", "link_entity_id"),
|
|
Index("ix_timeline_events_timeline", "tenant_id", "timeline_id"),
|
|
)
|
|
|
|
|
|
class Comment(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
):
|
|
__tablename__ = "comments"
|
|
|
|
entity_type: Mapped[CollaborationEntityType] = mapped_column(nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
parent_comment_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
body: Mapped[str] = mapped_column(Text, nullable=False)
|
|
body_format: Mapped[str] = mapped_column(String(30), default="richtext", nullable=False)
|
|
version: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
is_pinned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
|
|
__table_args__ = (
|
|
Index("ix_comments_entity", "tenant_id", "entity_type", "entity_id"),
|
|
Index("ix_comments_parent", "tenant_id", "parent_comment_id"),
|
|
)
|
|
|
|
|
|
class CommentVersion(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "comment_versions"
|
|
|
|
comment_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
body: Mapped[str] = mapped_column(Text, nullable=False)
|
|
edited_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "comment_id", "version", name="uq_comment_versions"
|
|
),
|
|
)
|
|
|
|
|
|
class Mention(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "mentions"
|
|
|
|
comment_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
entity_type: Mapped[CollaborationEntityType] = mapped_column(nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
mentioned_user_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
mentioned_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
is_read: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
notification_event_emitted: Mapped[bool] = mapped_column(
|
|
Boolean, default=False, nullable=False
|
|
)
|
|
|
|
__table_args__ = (
|
|
Index("ix_mentions_user", "tenant_id", "mentioned_user_id", "is_read"),
|
|
Index("ix_mentions_entity", "tenant_id", "entity_type", "entity_id"),
|
|
)
|
|
|
|
|
|
class Bookmark(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "bookmarks"
|
|
|
|
user_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
entity_type: Mapped[CollaborationEntityType] = mapped_column(nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
label: Mapped[str | None] = mapped_column(String(150), nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"user_id",
|
|
"entity_type",
|
|
"entity_id",
|
|
name="uq_bookmarks_user_entity",
|
|
),
|
|
)
|
|
|
|
|
|
class Favorite(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "favorites"
|
|
|
|
user_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
entity_type: Mapped[CollaborationEntityType] = mapped_column(nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"user_id",
|
|
"entity_type",
|
|
"entity_id",
|
|
name="uq_favorites_user_entity",
|
|
),
|
|
)
|
|
|
|
|
|
class TeamRole(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "team_roles"
|
|
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(150), nullable=False)
|
|
role_kind: Mapped[TeamRoleCode] = mapped_column(
|
|
default=TeamRoleCode.CUSTOM, nullable=False
|
|
)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("tenant_id", "code", name="uq_team_roles_tenant_code"),
|
|
)
|
|
|
|
|
|
class SalesTeamMember(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "sales_team_members"
|
|
|
|
user_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
display_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
team_key: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
role_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True)
|
|
role_code: Mapped[TeamRoleCode] = mapped_column(
|
|
default=TeamRoleCode.SALESPERSON, nullable=False
|
|
)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "team_key", "user_id", name="uq_sales_team_members"
|
|
),
|
|
Index("ix_sales_team_members_team", "tenant_id", "team_key"),
|
|
)
|
|
|
|
|
|
class TeamAssignment(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "team_assignments"
|
|
|
|
team_key: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
member_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
entity_type: Mapped[CollaborationEntityType] = mapped_column(nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
assigned_by: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index("ix_team_assignments_entity", "tenant_id", "entity_type", "entity_id"),
|
|
Index("ix_team_assignments_team", "tenant_id", "team_key"),
|
|
)
|
|
|
|
|
|
class CrmNotificationPreference(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
"""In-CRM preference flags only — delivery owned by Notification Platform."""
|
|
|
|
__tablename__ = "crm_notifications_preferences"
|
|
|
|
user_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
mentions_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
task_reminders_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, default=True, nullable=False
|
|
)
|
|
meeting_reminders_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, default=True, nullable=False
|
|
)
|
|
timeline_digest_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, default=False, nullable=False
|
|
)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "user_id", name="uq_crm_notification_prefs_user"
|
|
),
|
|
)
|
|
|
|
|
|
class SalesCollaborationAudit(Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin):
|
|
__tablename__ = "sales_collaboration_audit"
|
|
|
|
entity_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
entity_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
action: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
actor_user_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
changes: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index(
|
|
"ix_sales_collab_audit_entity",
|
|
"tenant_id",
|
|
"entity_type",
|
|
"entity_id",
|
|
),
|
|
)
|