Skip to main content
Glama
data-model.md17.3 kB
# Data Model: Guest Communication & Enhanced Error Handling (v1.1) **Date**: 2025-10-13 **Branch**: `002-enhance-the-hostaway` **Input**: Research findings from `research.md`, functional requirements FR-001 to FR-018 --- ## Overview v1.1 introduces 6 new entities to support guest communication, partial failure handling, and message search. All models use Pydantic for type safety and validation, following v1.0 patterns. ### New Entities 1. **Message** - Individual communication between guest and property manager 2. **ConversationThread** - Collection of messages for a booking 3. **PartialFailureResponse[T]** - Generic batch operation response 4. **BatchFailure** - Individual failed item in batch operation 5. **BatchSummary** - Metrics for batch operation 6. **MessageSearchCriteria** - Filter parameters for message queries --- ## Entity Definitions ### 1. Message **Purpose**: Represents a single message in a guest conversation **Source**: Hostaway Conversations API (`GET /v1/conversations/{id}/messages`) **Requirements**: FR-001 to FR-006, FR-016, FR-018 ```python from enum import Enum from datetime import datetime from pydantic import BaseModel, Field class MessageChannel(str, Enum): """Communication channel types""" EMAIL = "email" SMS = "sms" IN_APP = "in_app" PLATFORM = "platform" # Airbnb, Booking.com, VRBO, etc. class SenderType(str, Enum): """Message sender classification""" GUEST = "guest" SYSTEM = "system" PROPERTY_MANAGER = "property_manager" class DeliveryStatus(str, Enum): """Message delivery status""" SENT = "sent" DELIVERED = "delivered" FAILED = "failed" READ = "read" class Message(BaseModel): """Individual message in a conversation""" id: str = Field(..., description="Unique message identifier from Hostaway") booking_id: int = Field(..., gt=0, description="Associated booking/reservation ID") content: str = Field(..., min_length=1, description="Message text content") timestamp: datetime = Field(..., description="When message was sent (UTC)") channel: MessageChannel = Field(..., description="Communication channel") sender_type: SenderType = Field(..., description="Who sent the message") sender_id: str = Field(..., description="Sender identifier (guest_id, pm_id, system)") recipient_id: str = Field(..., description="Recipient identifier") delivery_status: DeliveryStatus = Field( default=DeliveryStatus.SENT, description="Current delivery status" ) class Config: json_schema_extra = { "example": { "id": "msg_abc123", "booking_id": 12345, "content": "What time is check-in?", "timestamp": "2025-10-13T14:30:00Z", "channel": "platform", "sender_type": "guest", "sender_id": "guest_xyz", "recipient_id": "pm_789", "delivery_status": "delivered" } } ``` **Validation Rules**: - `content`: Must be non-empty string (min_length=1) - `booking_id`: Must be positive integer (gt=0) - `timestamp`: Required, must be valid datetime (UTC assumed) - `channel`: Must be one of enum values (email, sms, in_app, platform) - `sender_type`: Must be one of enum values (guest, system, property_manager) **Mapping from Hostaway API**: ```python # Hostaway response → Message model { "id": "msg_123", → id "reservationId": 12345, → booking_id "content": "Message text", → content "createdAt": "2025-10-13T...", → timestamp "channelType": "airbnb", → channel (map to MessageChannel.PLATFORM) "direction": "in", → sender_type (in=GUEST, out=PROPERTY_MANAGER/SYSTEM) "sender": {"id": "..."}, → sender_id "recipient": {"id": "..."} → recipient_id } ``` --- ### 2. ConversationThread **Purpose**: Aggregates all messages for a booking into chronological timeline **Requirements**: FR-004, FR-006 ```python from typing import List, Set from pydantic import BaseModel, Field, field_validator class ConversationThread(BaseModel): """Complete conversation history for a booking""" booking_id: int = Field(..., gt=0, description="Booking identifier") messages: List[Message] = Field( default_factory=list, description="Messages ordered chronologically (oldest first)" ) total_count: int = Field(..., ge=0, description="Total messages in thread") channels_used: Set[MessageChannel] = Field( default_factory=set, description="Unique channels present in conversation" ) @field_validator("messages") @classmethod def messages_ordered_chronologically(cls, messages: List[Message]) -> List[Message]: """Ensure messages are sorted by timestamp""" return sorted(messages, key=lambda m: m.timestamp) @field_validator("total_count") @classmethod def total_matches_messages(cls, total_count: int, info) -> int: """Validate total_count matches messages list length""" messages = info.data.get("messages", []) if len(messages) != total_count: raise ValueError(f"total_count ({total_count}) must match messages length ({len(messages)})") return total_count class Config: json_schema_extra = { "example": { "booking_id": 12345, "messages": [ {"id": "msg_1", "timestamp": "2025-10-10T10:00:00Z", "...": "..."}, {"id": "msg_2", "timestamp": "2025-10-11T15:30:00Z", "...": "..."} ], "total_count": 2, "channels_used": ["email", "platform"] } } ``` **Validation Rules**: - `booking_id`: Must be positive integer - `messages`: Automatically sorted by timestamp (validator enforces) - `total_count`: Must equal `len(messages)` (validator enforces) - `channels_used`: Derived from message list (set of unique channels) **Construction Logic**: ```python # Pseudocode for building ConversationThread def build_conversation(booking_id: int, messages: List[Message]) -> ConversationThread: channels = {m.channel for m in messages} return ConversationThread( booking_id=booking_id, messages=messages, # Will be auto-sorted by validator total_count=len(messages), channels_used=channels ) ``` --- ### 3. PartialFailureResponse[T] **Purpose**: Generic response for batch operations supporting partial failures **Requirements**: FR-007 to FR-011 ```python from typing import Generic, TypeVar, List from pydantic import BaseModel, Field, field_validator T = TypeVar('T') class PartialFailureResponse(BaseModel, Generic[T]): """Response for batch operations with partial success/failure support""" successful_results: List[T] = Field( default_factory=list, description="Successfully processed items" ) failed_items: List["BatchFailure"] = Field( default_factory=list, description="Failed items with error details" ) summary: "BatchSummary" = Field(..., description="Batch operation metrics") @field_validator("summary") @classmethod def validate_summary_totals(cls, summary: "BatchSummary", info) -> "BatchSummary": """Ensure summary totals match actual results""" successful = len(info.data.get("successful_results", [])) failed = len(info.data.get("failed_items", [])) if summary.succeeded != successful: raise ValueError(f"summary.succeeded ({summary.succeeded}) must match successful_results length ({successful})") if summary.failed != failed: raise ValueError(f"summary.failed ({summary.failed}) must match failed_items length ({failed})") if summary.total_attempted != successful + failed: raise ValueError(f"summary.total_attempted must equal succeeded + failed") return summary class Config: json_schema_extra = { "example": { "successful_results": [ {"id": 101, "name": "Property A"}, {"id": 102, "name": "Property B"} ], "failed_items": [ { "item_id": "999", "error_type": "not_found", "error_message": "Property 999 not found", "remediation": "Verify property ID exists in Hostaway" } ], "summary": { "total_attempted": 3, "succeeded": 2, "failed": 1, "success_rate": 0.6667 } } } ``` **Usage Examples**: ```python # Batch property retrieval PartialFailureResponse[List[Property]] # Batch booking retrieval PartialFailureResponse[List[Booking]] # Batch message search PartialFailureResponse[List[Message]] ``` --- ### 4. BatchFailure **Purpose**: Represents a single failed item in a batch operation **Requirements**: FR-008, FR-011 ```python class ErrorType(str, Enum): """Categorized error types for batch failures""" NOT_FOUND = "not_found" # 404: Resource doesn't exist UNAUTHORIZED = "unauthorized" # 403: Permission denied VALIDATION_ERROR = "validation_error" # 422: Invalid input RATE_LIMIT = "rate_limit" # 429: Too many requests TIMEOUT = "timeout" # 504: Upstream timeout INTERNAL_ERROR = "internal_error" # 500: Unexpected failure class BatchFailure(BaseModel): """Individual failed item in batch operation""" item_id: str = Field(..., description="Identifier of failed item") error_type: ErrorType = Field(..., description="Categorized error type") error_message: str = Field(..., min_length=1, description="Human-readable error description") remediation: str = Field(..., min_length=1, description="Actionable guidance for resolution") class Config: json_schema_extra = { "example": { "item_id": "property_999", "error_type": "not_found", "error_message": "Property with ID 999 does not exist in Hostaway", "remediation": "Verify the property ID exists. Use GET /api/listings to see available properties." } } ``` **Remediation Templates**: ```python REMEDIATION_TEMPLATES = { ErrorType.NOT_FOUND: "Verify {item_type} ID '{item_id}' exists in Hostaway. Use GET /api/{endpoint} to see available items.", ErrorType.UNAUTHORIZED: "Check API permissions for accessing {item_type} '{item_id}'. Verify API key has required scopes.", ErrorType.VALIDATION_ERROR: "Fix validation error for {item_type} '{item_id}': {validation_detail}", ErrorType.RATE_LIMIT: "Retry after {retry_after} seconds or reduce request rate. Current limit: {limit} req/{period}.", ErrorType.TIMEOUT: "Upstream service timeout for {item_type} '{item_id}'. Retry with exponential backoff.", ErrorType.INTERNAL_ERROR: "Unexpected error processing {item_type} '{item_id}'. Contact support if issue persists." } ``` --- ### 5. BatchSummary **Purpose**: Provides metrics for batch operation results **Requirements**: FR-010 ```python from pydantic import BaseModel, Field, field_validator class BatchSummary(BaseModel): """Summary metrics for batch operation""" total_attempted: int = Field(..., ge=0, description="Total items in batch") succeeded: int = Field(..., ge=0, description="Successfully processed items") failed: int = Field(..., ge=0, description="Failed items") success_rate: float = Field(..., ge=0.0, le=1.0, description="Succeeded / Total (0.0 to 1.0)") @field_validator("success_rate") @classmethod def validate_success_rate(cls, success_rate: float, info) -> float: """Ensure success_rate matches succeeded/total calculation""" total = info.data.get("total_attempted", 0) succeeded = info.data.get("succeeded", 0) if total == 0: expected_rate = 0.0 else: expected_rate = succeeded / total if abs(success_rate - expected_rate) > 0.001: # Float precision tolerance raise ValueError(f"success_rate ({success_rate}) must equal succeeded/total ({expected_rate})") return success_rate @field_validator("total_attempted") @classmethod def total_equals_sum(cls, total: int, info) -> int: """Validate total = succeeded + failed""" succeeded = info.data.get("succeeded", 0) failed = info.data.get("failed", 0) if total != succeeded + failed: raise ValueError(f"total_attempted ({total}) must equal succeeded ({succeeded}) + failed ({failed})") return total class Config: json_schema_extra = { "example": { "total_attempted": 10, "succeeded": 8, "failed": 2, "success_rate": 0.8 } } ``` --- ### 6. MessageSearchCriteria **Purpose**: Filter parameters for message search queries **Requirements**: FR-001 to FR-003, FR-017 ```python from typing import Optional, List from datetime import date from pydantic import BaseModel, Field, field_validator class MessageSearchCriteria(BaseModel): """Filter and pagination parameters for message search""" booking_id: Optional[int] = Field( default=None, gt=0, description="Filter messages by booking ID" ) guest_name: Optional[str] = Field( default=None, min_length=1, max_length=255, description="Partial match search on guest name" ) start_date: Optional[date] = Field( default=None, description="Filter messages from this date (inclusive)" ) end_date: Optional[date] = Field( default=None, description="Filter messages until this date (inclusive)" ) channels: Optional[List[MessageChannel]] = Field( default=None, description="Filter by specific channels (empty = all channels)" ) limit: int = Field( default=50, ge=1, le=1000, description="Page size (1-1000, default 50)" ) offset: int = Field( default=0, ge=0, description="Pagination offset (default 0)" ) @field_validator("end_date") @classmethod def end_date_after_start_date(cls, end_date: Optional[date], info) -> Optional[date]: """Validate end_date >= start_date""" start_date = info.data.get("start_date") if start_date and end_date and end_date < start_date: raise ValueError(f"end_date ({end_date}) must be >= start_date ({start_date})") return end_date class Config: json_schema_extra = { "example": { "booking_id": 12345, "start_date": "2025-10-01", "end_date": "2025-10-15", "channels": ["email", "platform"], "limit": 50, "offset": 0 } } ``` **Validation Rules**: - `booking_id`: If provided, must be positive integer - `guest_name`: If provided, 1-255 characters (prevent excessive length) - `start_date` / `end_date`: end_date must be >= start_date - `channels`: If provided, must be list of valid MessageChannel enum values - `limit`: 1-1000 (enforced by Field constraint) - `offset`: Non-negative integer --- ## Relationships ``` ConversationThread 1 ─┬─ * Message │ (booking_id foreign key) │ └─> booking_id references Booking.id (from v1.0) PartialFailureResponse[T] 1 ─┬─ * T (successful_results) ├─ * BatchFailure (failed_items) └─ 1 BatchSummary MessageSearchCriteria ──> used by GET /api/messages endpoint ``` --- ## File Organization ### New Model Files ``` src/models/ ├── messages.py # Message, ConversationThread, MessageChannel, SenderType, DeliveryStatus ├── batch.py # PartialFailureResponse, BatchFailure, BatchSummary, ErrorType └── search.py # MessageSearchCriteria ``` ### Import Structure ```python # In src/models/messages.py from pydantic import BaseModel, Field from enum import Enum from datetime import datetime from typing import List, Set # In src/models/batch.py from pydantic import BaseModel, Field, field_validator from enum import Enum from typing import Generic, TypeVar, List # In src/models/search.py from pydantic import BaseModel, Field, field_validator from typing import Optional, List from datetime import date from src.models.messages import MessageChannel ``` --- ## Data Model Complete All entities defined with: - ✅ Pydantic models with type safety - ✅ Field validation (constraints, enums) - ✅ Custom validators for business logic - ✅ Example schemas for documentation - ✅ Relationship mapping **Next**: Generate API contracts (OpenAPI schemas)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/darrentmorgan/hostaway-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server