- Add Conversation, ConversationParticipant, Message, MessageAttachment models - Add ConversationType enum (admin_vendor, vendor_customer, admin_customer) - Add ParticipantType enum (admin, vendor, customer) - Add Alembic migration for messaging tables - Add MessagingService for conversation/message operations - Add MessageAttachmentService for file upload handling - Add message-related exceptions (ConversationNotFoundException, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
309 lines
7.9 KiB
Python
309 lines
7.9 KiB
Python
# models/schema/message.py
|
|
"""
|
|
Pydantic schemas for the messaging system.
|
|
|
|
Supports three communication channels:
|
|
- Admin <-> Vendor
|
|
- Vendor <-> Customer
|
|
- Admin <-> Customer
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from models.database.message import ConversationType, ParticipantType
|
|
|
|
|
|
# ============================================================================
|
|
# Attachment Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class AttachmentResponse(BaseModel):
|
|
"""Schema for message attachment in responses."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
filename: str
|
|
original_filename: str
|
|
file_size: int
|
|
mime_type: str
|
|
is_image: bool
|
|
image_width: int | None = None
|
|
image_height: int | None = None
|
|
download_url: str | None = None
|
|
thumbnail_url: str | None = None
|
|
|
|
@property
|
|
def file_size_display(self) -> str:
|
|
"""Human-readable file size."""
|
|
if self.file_size < 1024:
|
|
return f"{self.file_size} B"
|
|
elif self.file_size < 1024 * 1024:
|
|
return f"{self.file_size / 1024:.1f} KB"
|
|
else:
|
|
return f"{self.file_size / 1024 / 1024:.1f} MB"
|
|
|
|
|
|
# ============================================================================
|
|
# Message Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class MessageCreate(BaseModel):
|
|
"""Schema for sending a new message."""
|
|
|
|
content: str = Field(..., min_length=1, max_length=10000)
|
|
|
|
|
|
class MessageResponse(BaseModel):
|
|
"""Schema for a single message in responses."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
conversation_id: int
|
|
sender_type: ParticipantType
|
|
sender_id: int
|
|
content: str
|
|
is_system_message: bool
|
|
is_deleted: bool
|
|
created_at: datetime
|
|
|
|
# Enriched sender info (populated by API)
|
|
sender_name: str | None = None
|
|
sender_email: str | None = None
|
|
|
|
# Attachments
|
|
attachments: list[AttachmentResponse] = []
|
|
|
|
|
|
# ============================================================================
|
|
# Participant Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class ParticipantInfo(BaseModel):
|
|
"""Schema for participant information."""
|
|
|
|
id: int
|
|
type: ParticipantType
|
|
name: str
|
|
email: str | None = None
|
|
avatar_url: str | None = None
|
|
|
|
|
|
class ParticipantResponse(BaseModel):
|
|
"""Schema for conversation participant in responses."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
participant_type: ParticipantType
|
|
participant_id: int
|
|
unread_count: int
|
|
last_read_at: datetime | None
|
|
email_notifications: bool
|
|
muted: bool
|
|
|
|
# Enriched info (populated by API)
|
|
participant_info: ParticipantInfo | None = None
|
|
|
|
|
|
# ============================================================================
|
|
# Conversation Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class ConversationCreate(BaseModel):
|
|
"""Schema for creating a new conversation."""
|
|
|
|
conversation_type: ConversationType
|
|
subject: str = Field(..., min_length=1, max_length=500)
|
|
recipient_type: ParticipantType
|
|
recipient_id: int
|
|
vendor_id: int | None = None
|
|
initial_message: str | None = Field(None, min_length=1, max_length=10000)
|
|
|
|
|
|
class ConversationSummary(BaseModel):
|
|
"""Schema for conversation in list views."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
conversation_type: ConversationType
|
|
subject: str
|
|
vendor_id: int | None = None
|
|
is_closed: bool
|
|
closed_at: datetime | None
|
|
last_message_at: datetime | None
|
|
message_count: int
|
|
created_at: datetime
|
|
|
|
# Unread count for current user (from participant)
|
|
unread_count: int = 0
|
|
|
|
# Other participant info (enriched by API)
|
|
other_participant: ParticipantInfo | None = None
|
|
|
|
# Last message preview
|
|
last_message_preview: str | None = None
|
|
|
|
|
|
class ConversationDetailResponse(BaseModel):
|
|
"""Schema for full conversation detail with messages."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
conversation_type: ConversationType
|
|
subject: str
|
|
vendor_id: int | None = None
|
|
is_closed: bool
|
|
closed_at: datetime | None
|
|
closed_by_type: ParticipantType | None = None
|
|
closed_by_id: int | None = None
|
|
last_message_at: datetime | None
|
|
message_count: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
# Participants with enriched info
|
|
participants: list[ParticipantResponse] = []
|
|
|
|
# Messages ordered by created_at
|
|
messages: list[MessageResponse] = []
|
|
|
|
# Current user's unread count
|
|
unread_count: int = 0
|
|
|
|
# Vendor info if applicable
|
|
vendor_name: str | None = None
|
|
|
|
|
|
class ConversationListResponse(BaseModel):
|
|
"""Schema for paginated conversation list."""
|
|
|
|
conversations: list[ConversationSummary]
|
|
total: int
|
|
total_unread: int
|
|
skip: int
|
|
limit: int
|
|
|
|
|
|
# ============================================================================
|
|
# Unread Count Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class UnreadCountResponse(BaseModel):
|
|
"""Schema for unread message count (for header badge)."""
|
|
|
|
total_unread: int
|
|
|
|
|
|
# ============================================================================
|
|
# Notification Preferences Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class NotificationPreferencesUpdate(BaseModel):
|
|
"""Schema for updating notification preferences."""
|
|
|
|
email_notifications: bool | None = None
|
|
muted: bool | None = None
|
|
|
|
|
|
# ============================================================================
|
|
# Conversation Action Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class CloseConversationResponse(BaseModel):
|
|
"""Response after closing a conversation."""
|
|
|
|
success: bool
|
|
message: str
|
|
conversation_id: int
|
|
|
|
|
|
class ReopenConversationResponse(BaseModel):
|
|
"""Response after reopening a conversation."""
|
|
|
|
success: bool
|
|
message: str
|
|
conversation_id: int
|
|
|
|
|
|
class MarkReadResponse(BaseModel):
|
|
"""Response after marking conversation as read."""
|
|
|
|
success: bool
|
|
conversation_id: int
|
|
unread_count: int
|
|
|
|
|
|
# ============================================================================
|
|
# Recipient Selection Schemas (for compose modal)
|
|
# ============================================================================
|
|
|
|
|
|
class RecipientOption(BaseModel):
|
|
"""Schema for a selectable recipient in compose modal."""
|
|
|
|
id: int
|
|
type: ParticipantType
|
|
name: str
|
|
email: str | None = None
|
|
vendor_id: int | None = None # For vendor users
|
|
vendor_name: str | None = None
|
|
|
|
|
|
class RecipientListResponse(BaseModel):
|
|
"""Schema for list of available recipients."""
|
|
|
|
recipients: list[RecipientOption]
|
|
total: int
|
|
|
|
|
|
# ============================================================================
|
|
# Admin-specific Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class AdminConversationSummary(ConversationSummary):
|
|
"""Extended conversation summary with vendor info for admin views."""
|
|
|
|
vendor_name: str | None = None
|
|
vendor_code: str | None = None
|
|
|
|
|
|
class AdminConversationListResponse(BaseModel):
|
|
"""Schema for admin conversation list with vendor info."""
|
|
|
|
conversations: list[AdminConversationSummary]
|
|
total: int
|
|
total_unread: int
|
|
skip: int
|
|
limit: int
|
|
|
|
|
|
class AdminMessageStats(BaseModel):
|
|
"""Messaging statistics for admin dashboard."""
|
|
|
|
total_conversations: int = 0
|
|
open_conversations: int = 0
|
|
closed_conversations: int = 0
|
|
total_messages: int = 0
|
|
|
|
# By type
|
|
admin_vendor_conversations: int = 0
|
|
vendor_customer_conversations: int = 0
|
|
admin_customer_conversations: int = 0
|
|
|
|
# Unread
|
|
unread_admin: int = 0
|