All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
by how many add refresh button choose column to display to the guest page
362 lines
10 KiB
Python
362 lines
10 KiB
Python
from pydantic import BaseModel, Field
|
|
from typing import Optional, List, Dict
|
|
from datetime import datetime
|
|
from uuid import UUID
|
|
|
|
|
|
# ============================================
|
|
# User Schemas
|
|
# ============================================
|
|
class UserBase(BaseModel):
|
|
email: str
|
|
|
|
|
|
class UserCreate(UserBase):
|
|
pass
|
|
|
|
|
|
class User(UserBase):
|
|
id: UUID
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================
|
|
# Event Schemas
|
|
# ============================================
|
|
class EventBase(BaseModel):
|
|
name: str
|
|
date: Optional[datetime] = None
|
|
location: Optional[str] = None
|
|
partner1_name: Optional[str] = None
|
|
partner2_name: Optional[str] = None
|
|
venue: Optional[str] = None
|
|
event_time: Optional[str] = None
|
|
guest_link: Optional[str] = None
|
|
invitation_image_url: Optional[str] = None
|
|
guest_form_fields: Optional[str] = None
|
|
|
|
|
|
class EventCreate(EventBase):
|
|
pass
|
|
|
|
|
|
class EventUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
date: Optional[datetime] = None
|
|
location: Optional[str] = None
|
|
partner1_name: Optional[str] = None
|
|
partner2_name: Optional[str] = None
|
|
venue: Optional[str] = None
|
|
event_time: Optional[str] = None
|
|
guest_link: Optional[str] = None
|
|
invitation_image_url: Optional[str] = None
|
|
guest_form_fields: Optional[str] = None
|
|
|
|
|
|
class Event(EventBase):
|
|
id: UUID
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class EventWithMembers(Event):
|
|
members: List["EventMember"] = []
|
|
|
|
|
|
# ============================================
|
|
# Event Member Schemas
|
|
# ============================================
|
|
class EventMemberBase(BaseModel):
|
|
role: str = "admin" # admin, editor, viewer
|
|
display_name: Optional[str] = None
|
|
|
|
|
|
class EventMemberCreate(BaseModel):
|
|
user_email: str = Field(..., description="Email address of the user to invite")
|
|
role: str = "admin"
|
|
display_name: Optional[str] = None
|
|
|
|
|
|
class EventMember(EventMemberBase):
|
|
id: UUID
|
|
event_id: UUID
|
|
user_id: UUID
|
|
created_at: datetime
|
|
user: Optional[User] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================
|
|
# Guest Schemas
|
|
# ============================================
|
|
class GuestBase(BaseModel):
|
|
first_name: str
|
|
last_name: str
|
|
email: Optional[str] = None
|
|
phone_number: Optional[str] = None
|
|
rsvp_status: str = "invited" # invited, confirmed, declined
|
|
meal_preference: Optional[str] = None
|
|
has_plus_one: bool = False
|
|
plus_one_name: Optional[str] = None
|
|
companion_count: Optional[int] = 0
|
|
table_number: Optional[str] = None
|
|
side: Optional[str] = None # e.g., "groom side", "bride side"
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class GuestCreate(GuestBase):
|
|
pass
|
|
|
|
|
|
class GuestUpdate(BaseModel):
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
email: Optional[str] = None
|
|
phone_number: Optional[str] = None
|
|
rsvp_status: Optional[str] = None
|
|
meal_preference: Optional[str] = None
|
|
has_plus_one: Optional[bool] = None
|
|
plus_one_name: Optional[str] = None
|
|
companion_count: Optional[int] = None
|
|
table_number: Optional[str] = None
|
|
side: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class Guest(GuestBase):
|
|
id: UUID
|
|
event_id: UUID
|
|
added_by_user_id: UUID
|
|
owner_email: Optional[str] = None
|
|
source: str = "manual"
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================
|
|
# Bulk Import Schemas
|
|
# ============================================
|
|
class GuestImportItem(BaseModel):
|
|
first_name: str
|
|
last_name: str
|
|
email: Optional[str] = None
|
|
phone_number: Optional[str] = None
|
|
side: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class GuestBulkImport(BaseModel):
|
|
guests: List[GuestImportItem]
|
|
|
|
|
|
class GuestBulkDelete(BaseModel):
|
|
guest_ids: List[UUID]
|
|
|
|
|
|
# ============================================
|
|
# Filter/Search Schemas
|
|
# ============================================
|
|
class GuestFilter(BaseModel):
|
|
search: Optional[str] = None
|
|
side: Optional[str] = None
|
|
status: Optional[str] = None
|
|
added_by: Optional[str] = None # "me" for current user
|
|
|
|
|
|
# ============================================
|
|
# WhatsApp Schemas
|
|
# ============================================
|
|
class WhatsAppMessage(BaseModel):
|
|
message: str
|
|
phone: Optional[str] = None # Optional: override guest's phone
|
|
|
|
|
|
class WhatsAppStatus(BaseModel):
|
|
message_id: str
|
|
status: str
|
|
timestamp: datetime
|
|
|
|
|
|
class WhatsAppWeddingInviteRequest(BaseModel):
|
|
"""Request to send wedding invitation template to guest(s)"""
|
|
guest_ids: Optional[List[str]] = None # For bulk sending
|
|
phone_override: Optional[str] = None # Optional: override phone number
|
|
template_key: Optional[str] = "wedding_invitation" # Registry key for template selection
|
|
# Optional form data overrides (frontend form values take priority over DB)
|
|
partner1_name: Optional[str] = None # First partner / groom name
|
|
partner2_name: Optional[str] = None # Second partner / bride name
|
|
venue: Optional[str] = None # Hall / venue name
|
|
event_date: Optional[str] = None # YYYY-MM-DD or DD/MM
|
|
event_time: Optional[str] = None # HH:mm
|
|
guest_link: Optional[str] = None # RSVP link
|
|
extra_params: Optional[Dict[str, str]] = None # Custom/extra param values keyed by param name
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class WhatsAppSendResult(BaseModel):
|
|
"""Result of sending WhatsApp message to a guest"""
|
|
guest_id: str
|
|
guest_name: Optional[str] = None
|
|
phone: str
|
|
status: str # "sent", "failed"
|
|
message_id: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class WhatsAppBulkResult(BaseModel):
|
|
"""Result of bulk WhatsApp sending"""
|
|
total: int
|
|
succeeded: int
|
|
failed: int
|
|
results: List[WhatsAppSendResult]
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================
|
|
# Google Contacts Import Schema
|
|
# ============================================
|
|
class GoogleContactsImport(BaseModel):
|
|
access_token: str
|
|
owner: Optional[str] = "Google Import"
|
|
|
|
|
|
# ============================================
|
|
# Public Guest Self-Service Schema
|
|
# ============================================
|
|
class GuestPublicUpdate(BaseModel):
|
|
"""Schema for public guest self-service updates (phone-based lookup)"""
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
rsvp_status: Optional[str] = None
|
|
meal_preference: Optional[str] = None
|
|
has_plus_one: Optional[bool] = None
|
|
plus_one_name: Optional[str] = None
|
|
|
|
|
|
# ============================================
|
|
# Event-Scoped RSVP Schemas (/public/events/:id)
|
|
# ============================================
|
|
|
|
class EventPublicInfo(BaseModel):
|
|
"""Public event details returned on the RSVP landing page"""
|
|
event_id: str
|
|
name: str
|
|
date: Optional[str] = None
|
|
venue: Optional[str] = None
|
|
partner1_name: Optional[str] = None
|
|
partner2_name: Optional[str] = None
|
|
event_time: Optional[str] = None
|
|
|
|
|
|
class EventScopedRsvpUpdate(BaseModel):
|
|
"""
|
|
Guest submits RSVP for a specific event.
|
|
Identified by phone; update is scoped exclusively to that (event, phone) pair.
|
|
"""
|
|
phone: str
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
rsvp_status: Optional[str] = None
|
|
meal_preference: Optional[str] = None
|
|
companion_count: Optional[int] = None
|
|
|
|
|
|
# ============================================
|
|
# RSVP Token Schemas
|
|
# ============================================
|
|
|
|
class RsvpResolveResponse(BaseModel):
|
|
"""Returned when a guest opens their personal RSVP link via token"""
|
|
valid: bool
|
|
token: str
|
|
event_id: Optional[str] = None
|
|
event_name: Optional[str] = None
|
|
event_date: Optional[str] = None
|
|
venue: Optional[str] = None
|
|
partner1_name: Optional[str] = None
|
|
partner2_name: Optional[str] = None
|
|
guest_id: Optional[str] = None
|
|
guest_first_name: Optional[str] = None
|
|
guest_last_name: Optional[str] = None
|
|
current_rsvp_status: Optional[str] = None
|
|
current_meal_preference: Optional[str] = None
|
|
current_has_plus_one: Optional[bool] = None
|
|
current_plus_one_name: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
class RsvpSubmit(BaseModel):
|
|
"""Guest submits their RSVP via token"""
|
|
token: str
|
|
rsvp_status: str # "attending", "not_attending", "maybe"
|
|
meal_preference: Optional[str] = None
|
|
has_plus_one: Optional[bool] = None
|
|
plus_one_name: Optional[str] = None
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
|
|
|
|
class RsvpSubmitResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
guest_id: Optional[str] = None
|
|
|
|
|
|
# ============================================
|
|
# Contact Import Schemas
|
|
# ============================================
|
|
class ImportContactRow(BaseModel):
|
|
"""Represents a single row from an uploaded CSV / JSON import file."""
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
full_name: Optional[str] = None # alternative: "Full Name" column
|
|
phone: Optional[str] = None
|
|
phone_number: Optional[str] = None
|
|
email: Optional[str] = None
|
|
rsvp_status: Optional[str] = None
|
|
meal_preference: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
side: Optional[str] = None
|
|
table_number: Optional[str] = None
|
|
has_plus_one: Optional[bool] = None
|
|
plus_one_name: Optional[str] = None
|
|
|
|
|
|
class ImportRowResult(BaseModel):
|
|
"""Per-row result returned in the import response."""
|
|
row: int
|
|
action: str # "created" | "updated" | "skipped" | "error"
|
|
name: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
reason: Optional[str] = None # for errors / skips
|
|
|
|
|
|
class ImportContactsResponse(BaseModel):
|
|
"""Full response from POST /admin/import/contacts."""
|
|
dry_run: bool
|
|
total: int
|
|
created: int
|
|
updated: int
|
|
skipped: int
|
|
errors: int
|
|
rows: List[ImportRowResult]
|
|
|