feat: add messaging system database models and core services
- 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>
This commit is contained in:
308
models/schema/message.py
Normal file
308
models/schema/message.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user