Files
customer-portal/customer_portal/models/booking.py

263 lines
8.0 KiB
Python
Executable File

"""Booking model.
Sprint 14: Import bookings from WordPress kurs-booking plugin.
"""
import contextlib
from datetime import UTC, datetime
from decimal import Decimal
from typing import Any
from sqlalchemy import (
Column,
Date,
DateTime,
ForeignKey,
Index,
Integer,
Numeric,
String,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship
from customer_portal.models import Base
class Booking(Base):
"""Booking synchronized from WordPress kurs-booking.
Each booking is linked to a customer via email address matching.
The wp_booking_id is the unique identifier from WordPress.
"""
__tablename__ = "bookings"
id = Column(Integer, primary_key=True)
customer_id = Column(
Integer, ForeignKey("customers.id", ondelete="CASCADE"), nullable=False
)
# WordPress identifiers
wp_booking_id = Column(Integer, unique=True, nullable=False, index=True)
wp_kurs_id = Column(Integer, index=True)
# Booking data
booking_number = Column(String(50), index=True)
kurs_title = Column(String(255))
kurs_date = Column(Date)
kurs_time = Column(String(10))
kurs_end_time = Column(String(10))
kurs_location = Column(String(255))
# Status & pricing
status = Column(String(50), default="pending", index=True)
total_price = Column(Numeric(10, 2))
ticket_type = Column(String(100))
ticket_count = Column(Integer, default=1)
# Customer data snapshot (at time of booking)
customer_name = Column(String(255))
customer_email = Column(String(255))
customer_phone = Column(String(100))
# sevDesk integration
sevdesk_invoice_id = Column(Integer)
sevdesk_invoice_number = Column(String(50))
# Custom fields from booking (JSON)
custom_fields = Column(JSONB, default=dict)
extra_fields = Column(JSONB, default=dict)
# Timestamps
wp_created_at = Column(DateTime) # Original WordPress creation date
synced_at = Column(DateTime) # Last sync timestamp
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
updated_at = Column(
DateTime,
default=lambda: datetime.now(UTC),
onupdate=lambda: datetime.now(UTC),
)
# Relationship
customer = relationship("Customer", back_populates="bookings")
# Indexes
__table_args__ = (
Index("ix_bookings_customer_status", customer_id, status),
Index("ix_bookings_kurs_date", kurs_date),
)
def __repr__(self) -> str:
return f"<Booking {self.booking_number} ({self.status})>"
@classmethod
def get_by_wp_id(cls, db, wp_booking_id: int):
"""Get booking by WordPress ID.
Args:
db: Database session
wp_booking_id: WordPress booking post ID
Returns:
Booking instance or None
"""
return db.query(cls).filter(cls.wp_booking_id == wp_booking_id).first()
@classmethod
def get_by_customer(cls, db, customer_id: int, status: str | None = None):
"""Get all bookings for a customer.
Args:
db: Database session
customer_id: Customer ID
status: Optional status filter
Returns:
List of Booking instances
"""
query = db.query(cls).filter(cls.customer_id == customer_id)
if status:
query = query.filter(cls.status == status)
return query.order_by(cls.kurs_date.desc()).all()
@classmethod
def create_or_update_from_wp(
cls, db, customer_id: int, wp_data: dict
) -> tuple["Booking", bool]:
"""Create or update booking from WordPress API data.
Args:
db: Database session
customer_id: Customer ID to link booking to
wp_data: Data from WordPress REST API
Returns:
Tuple of (Booking instance, is_new)
"""
wp_booking_id = wp_data.get("id")
if not wp_booking_id:
raise ValueError("WordPress booking ID required")
existing = cls.get_by_wp_id(db, wp_booking_id)
is_new = existing is None
if is_new:
booking = cls(
customer_id=customer_id,
wp_booking_id=wp_booking_id,
created_at=datetime.now(UTC),
)
db.add(booking)
else:
booking = existing
# Update fields from WordPress data
booking.wp_kurs_id = wp_data.get("kurs_id")
booking.booking_number = wp_data.get("number", "")
booking.kurs_title = wp_data.get("kurs_title", "")
booking.kurs_location = wp_data.get("kurs_location", "")
booking.status = wp_data.get("status", "pending")
booking.ticket_type = wp_data.get("ticket_type", "")
booking.ticket_count = wp_data.get("ticket_count", 1)
# Parse date
kurs_date = wp_data.get("kurs_date")
if kurs_date:
with contextlib.suppress(ValueError):
booking.kurs_date = datetime.strptime(kurs_date, "%Y-%m-%d").date()
# Parse times
booking.kurs_time = wp_data.get("kurs_time", "")
booking.kurs_end_time = wp_data.get("kurs_end_time", "")
# Parse price
price = wp_data.get("price")
if price is not None:
booking.total_price = Decimal(str(price))
# Customer snapshot
customer_data = wp_data.get("customer", {})
if customer_data:
booking.customer_name = customer_data.get("name", "")
booking.customer_email = customer_data.get("email", "")
booking.customer_phone = customer_data.get("phone", "")
# sevDesk
sevdesk = wp_data.get("sevdesk", {})
if sevdesk:
if sevdesk.get("invoice_id"):
booking.sevdesk_invoice_id = int(sevdesk["invoice_id"])
booking.sevdesk_invoice_number = sevdesk.get("invoice_number", "")
# Custom fields
if wp_data.get("custom_fields"):
booking.custom_fields = wp_data["custom_fields"]
if wp_data.get("extra_fields"):
booking.extra_fields = wp_data["extra_fields"]
# Parse WordPress created date
wp_created = wp_data.get("created_at")
if wp_created:
with contextlib.suppress(ValueError):
booking.wp_created_at = datetime.strptime(
wp_created, "%Y-%m-%d %H:%M:%S"
)
# Update sync timestamp
booking.synced_at = datetime.now(UTC)
booking.updated_at = datetime.now(UTC)
return booking, is_new
@property
def status_display(self) -> str:
"""Get human-readable status."""
status_map = {
"pending": "Ausstehend",
"confirmed": "Bestaetigt",
"cancelled": "Storniert",
"cancel_requested": "Stornierung angefragt",
}
return status_map.get(self.status, self.status or "Unbekannt")
@property
def status_color(self) -> str:
"""Get Bootstrap color class for status."""
color_map = {
"pending": "warning",
"confirmed": "success",
"cancelled": "danger",
"cancel_requested": "info",
}
return color_map.get(self.status, "secondary")
@property
def formatted_price(self) -> str:
"""Get formatted price string."""
if self.total_price is None:
return "-"
return f"{self.total_price:.2f} EUR"
@property
def formatted_date(self) -> str:
"""Get formatted date string."""
if not self.kurs_date:
return "-"
return self.kurs_date.strftime("%d.%m.%Y")
@property
def formatted_time(self) -> str:
"""Get formatted time range."""
if not self.kurs_time:
return "-"
if self.kurs_end_time:
return f"{self.kurs_time} - {self.kurs_end_time}"
return self.kurs_time
def get_custom_field(self, key: str, default: Any = None) -> Any:
"""Get a custom field value."""
if not self.custom_fields:
return default
return self.custom_fields.get(key, default)