"""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", ), )