feat: Add hotel and webhook endpoint management

- Introduced Hotel and WebhookEndpoint models to manage hotel configurations and webhook settings.
- Implemented sync_config_to_database function to synchronize hotel data from configuration to the database.
- Added HotelService for accessing hotel configurations and managing customer data.
- Created WebhookProcessor interface and specific processors for handling different webhook types (Wix form and generic).
- Enhanced webhook processing logic to handle incoming requests and create/update reservations and customers.
- Added logging for better traceability of operations related to hotels and webhooks.
This commit is contained in:
Jonas Linter
2025-11-25 12:05:48 +01:00
parent da85098d8d
commit 8d144a761c
8 changed files with 1706 additions and 262 deletions

View File

@@ -13,6 +13,7 @@ from sqlalchemy import (
Double,
ForeignKey,
ForeignKeyConstraint,
Index,
Integer,
String,
)
@@ -674,3 +675,114 @@ class ConversionRoom(Base):
# Relationships
conversion = relationship("Conversion", back_populates="conversion_rooms")
class Hotel(Base):
"""Hotel configuration (migrated from alpine_bits_auth in config.yaml)."""
__tablename__ = "hotels"
id = Column(Integer, primary_key=True)
# Core identification
hotel_id = Column(String(50), unique=True, nullable=False, index=True)
hotel_name = Column(String(200), nullable=False)
# AlpineBits authentication
username = Column(String(100), unique=True, nullable=False, index=True)
password_hash = Column(String(200), nullable=False) # bcrypt
# Advertising accounts
meta_account_id = Column(String(50), nullable=True)
google_account_id = Column(String(50), nullable=True)
# Push endpoint (optional)
push_endpoint_url = Column(String(500), nullable=True)
push_endpoint_token = Column(String(200), nullable=True)
push_endpoint_username = Column(String(100), nullable=True)
# Metadata
created_at = Column(DateTime(timezone=True), nullable=False)
updated_at = Column(DateTime(timezone=True), nullable=False)
is_active = Column(Boolean, default=True, nullable=False, index=True)
# Relationships
webhook_endpoints = relationship("WebhookEndpoint", back_populates="hotel")
class WebhookEndpoint(Base):
"""Webhook configurations per hotel (supports multiple webhook types per hotel)."""
__tablename__ = "webhook_endpoints"
id = Column(Integer, primary_key=True)
# Hotel association
hotel_id = Column(String(50), ForeignKey("hotels.hotel_id"), nullable=False, index=True)
# Webhook configuration
webhook_secret = Column(String(64), unique=True, nullable=False, index=True)
webhook_type = Column(String(50), nullable=False) # 'wix_form', 'generic', etc.
# Metadata
description = Column(String(200), nullable=True) # Human-readable label
is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime(timezone=True), nullable=False)
# Relationships
hotel = relationship("Hotel", back_populates="webhook_endpoints")
webhook_requests = relationship("WebhookRequest", back_populates="webhook_endpoint")
__table_args__ = (
Index('idx_webhook_endpoint_hotel_type', 'hotel_id', 'webhook_type'),
)
class WebhookRequest(Base):
"""Tracks incoming webhooks for deduplication and retry handling."""
__tablename__ = "webhook_requests"
id = Column(Integer, primary_key=True)
# Request identification
payload_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA256
webhook_endpoint_id = Column(Integer, ForeignKey("webhook_endpoints.id"), nullable=True, index=True)
hotel_id = Column(String(50), ForeignKey("hotels.hotel_id"), nullable=True, index=True)
# Processing tracking
status = Column(String(20), nullable=False, default='pending', index=True)
# Status values: 'pending', 'processing', 'completed', 'failed'
processing_started_at = Column(DateTime(timezone=True), nullable=True)
processing_completed_at = Column(DateTime(timezone=True), nullable=True)
# Retry handling
retry_count = Column(Integer, default=0)
last_error = Column(String(2000), nullable=True)
# Payload storage
payload_json = Column(JSON, nullable=True) # NULL after purge, kept for retries
payload_size_bytes = Column(Integer, nullable=True) # Track original size
purged_at = Column(DateTime(timezone=True), nullable=True) # When JSON was purged
# Metadata
created_at = Column(DateTime(timezone=True), nullable=False, index=True)
source_ip = Column(String(45), nullable=True)
user_agent = Column(String(500), nullable=True)
# Result tracking
created_customer_id = Column(Integer, ForeignKey("customers.id"), nullable=True)
created_reservation_id = Column(Integer, ForeignKey("reservations.id"), nullable=True)
# Relationships
webhook_endpoint = relationship("WebhookEndpoint", back_populates="webhook_requests")
hotel = relationship("Hotel")
customer = relationship("Customer")
reservation = relationship("Reservation")
__table_args__ = (
Index('idx_webhook_status_created', 'status', 'created_at'),
Index('idx_webhook_hotel_created', 'hotel_id', 'created_at'),
Index('idx_webhook_purge_candidate', 'status', 'purged_at', 'created_at'),
)