Fixed some tests and added schemas
This commit is contained in:
@@ -10,11 +10,15 @@ from XML generation (xsdata) follows clean architecture principles.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
|
||||
|
||||
from .const import WebhookStatus
|
||||
|
||||
|
||||
# Country name to ISO 3166-1 alpha-2 code mapping
|
||||
COUNTRY_NAME_TO_CODE = {
|
||||
@@ -308,6 +312,148 @@ class CommentsData(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class HotelData(BaseModel):
|
||||
"""Validated hotel configuration data."""
|
||||
|
||||
hotel_id: str = Field(..., min_length=1, max_length=50)
|
||||
hotel_name: str = Field(..., min_length=1, max_length=200)
|
||||
username: str = Field(..., min_length=1, max_length=100)
|
||||
password_hash: str = Field(..., min_length=1, max_length=200)
|
||||
meta_account_id: str | None = Field(None, max_length=50)
|
||||
google_account_id: str | None = Field(None, max_length=50)
|
||||
push_endpoint_url: str | None = Field(None, max_length=500)
|
||||
push_endpoint_token: str | None = Field(None, max_length=200)
|
||||
push_endpoint_username: str | None = Field(None, max_length=100)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now())
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now())
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
@field_validator("hotel_id", "hotel_name", "username")
|
||||
@classmethod
|
||||
def strip_whitespace(cls, v: str) -> str:
|
||||
"""Remove leading/trailing whitespace."""
|
||||
return v.strip()
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class WebhookEndpointData(BaseModel):
|
||||
"""Validated webhook endpoint configuration data."""
|
||||
|
||||
hotel_id: str = Field(..., min_length=1, max_length=50)
|
||||
webhook_secret: str = Field(..., min_length=1, max_length=64)
|
||||
webhook_type: str = Field(..., min_length=1, max_length=50)
|
||||
description: str | None = Field(None, max_length=200)
|
||||
is_enabled: bool = Field(default=True)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now())
|
||||
|
||||
@field_validator("hotel_id", "webhook_secret", "webhook_type")
|
||||
@classmethod
|
||||
def strip_whitespace(cls, v: str) -> str:
|
||||
"""Remove leading/trailing whitespace."""
|
||||
return v.strip()
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class WebhookRequestData(BaseModel):
|
||||
"""Validated webhook request data.
|
||||
|
||||
This model handles the special case where:
|
||||
- payload_json is required for creation (to calculate payload_hash)
|
||||
- payload_json becomes optional after processing (can be purged for privacy/storage)
|
||||
- payload_hash is auto-calculated from payload_json when provided
|
||||
"""
|
||||
|
||||
# Required fields
|
||||
payload_json: dict[str, Any] | None = Field(
|
||||
...,
|
||||
description="Webhook payload (required for creation, nullable after purge)"
|
||||
)
|
||||
|
||||
# Auto-calculated from payload_json
|
||||
payload_hash: str | None = Field(
|
||||
None,
|
||||
min_length=64,
|
||||
max_length=64,
|
||||
description="SHA256 hash of canonical JSON payload (auto-calculated)"
|
||||
)
|
||||
|
||||
# Optional foreign keys
|
||||
webhook_endpoint_id: int | None = Field(None, gt=0)
|
||||
hotel_id: str | None = Field(None, max_length=50)
|
||||
|
||||
# Processing tracking
|
||||
status: WebhookStatus = Field(default=WebhookStatus.PENDING)
|
||||
processing_started_at: datetime | None = None
|
||||
processing_completed_at: datetime | None = None
|
||||
|
||||
# Retry handling
|
||||
retry_count: int = Field(default=0, ge=0)
|
||||
last_error: str | None = Field(None, max_length=2000)
|
||||
|
||||
# Payload metadata
|
||||
purged_at: datetime | None = None
|
||||
|
||||
# Request metadata
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now())
|
||||
source_ip: str | None = Field(None, max_length=45)
|
||||
user_agent: str | None = Field(None, max_length=500)
|
||||
|
||||
# Result tracking
|
||||
created_customer_id: int | None = Field(None, gt=0)
|
||||
created_reservation_id: int | None = Field(None, gt=0)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def calculate_payload_hash(self) -> "WebhookRequestData":
|
||||
"""Auto-calculate payload_hash from payload_json if not provided.
|
||||
|
||||
Uses the same hashing algorithm as api.py:
|
||||
- Canonical JSON with sorted keys
|
||||
- UTF-8 encoding
|
||||
- SHA256 hash
|
||||
|
||||
This runs after all field validation, so we can access the validated payload_json.
|
||||
"""
|
||||
# Only calculate if payload_json is provided and payload_hash is not set
|
||||
if self.payload_json is not None and self.payload_hash is None:
|
||||
# Create canonical JSON string (sorted keys for consistency)
|
||||
payload_json_str = json.dumps(self.payload_json, sort_keys=True)
|
||||
# Calculate SHA256 hash
|
||||
self.payload_hash = hashlib.sha256(
|
||||
payload_json_str.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_payload_hash_requirements(self) -> "WebhookRequestData":
|
||||
"""Ensure payload_hash is present (either provided or calculated).
|
||||
|
||||
This validator runs after calculate_payload_hash, so payload_hash should
|
||||
be set if payload_json was provided.
|
||||
"""
|
||||
if self.payload_hash is None:
|
||||
raise ValueError(
|
||||
"payload_hash is required. It can be auto-calculated from payload_json "
|
||||
"or explicitly provided."
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
@field_validator("status", mode="before")
|
||||
@classmethod
|
||||
def normalize_status(cls, v: str | WebhookStatus) -> WebhookStatus:
|
||||
"""Normalize status to WebhookStatus enum."""
|
||||
if isinstance(v, WebhookStatus):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
return WebhookStatus(v)
|
||||
raise ValueError(f"Invalid webhook status: {v}")
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# Example usage in a service layer
|
||||
class ReservationService:
|
||||
"""Example service showing how to use Pydantic models with SQLAlchemy."""
|
||||
|
||||
Reference in New Issue
Block a user