216 lines
9.4 KiB
Python
216 lines
9.4 KiB
Python
from sqlalchemy import Boolean, Column, Integer, String, DateTime, ForeignKey, Text, Enum as SQLEnum
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
from database import Base
|
|
import uuid
|
|
import enum
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
email = Column(String, unique=True, nullable=False, index=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
# Relationships
|
|
event_memberships = relationship("EventMember", back_populates="user", cascade="all, delete-orphan")
|
|
guests_added = relationship("Guest", back_populates="added_by_user", foreign_keys="Guest.added_by_user_id")
|
|
|
|
|
|
class Event(Base):
|
|
__tablename__ = "events"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
name = Column(String, nullable=False)
|
|
date = Column(DateTime(timezone=True), nullable=True)
|
|
location = Column(String, nullable=True)
|
|
|
|
# WhatsApp Invitation template fields
|
|
partner1_name = Column(String, nullable=True) # e.g., "Dvir"
|
|
partner2_name = Column(String, nullable=True) # e.g., "Vered"
|
|
venue = Column(String, nullable=True) # Hall name/address
|
|
event_time = Column(String, nullable=True) # HH:mm format, e.g., "19:00"
|
|
guest_link = Column(String, nullable=True) # Custom RSVP link or auto-generated
|
|
invitation_image_url = Column(String, nullable=True) # Background image for invitations
|
|
guest_form_fields = Column(String, nullable=True) # JSON array of visible fields on guest RSVP page
|
|
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
# Relationships
|
|
members = relationship("EventMember", back_populates="event", cascade="all, delete-orphan")
|
|
guests = relationship("Guest", back_populates="event", cascade="all, delete-orphan")
|
|
|
|
|
|
class RoleEnum(str, enum.Enum):
|
|
admin = "admin"
|
|
editor = "editor"
|
|
viewer = "viewer"
|
|
|
|
|
|
class EventMember(Base):
|
|
__tablename__ = "event_members"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
event_id = Column(UUID(as_uuid=True), ForeignKey("events.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
role = Column(SQLEnum(RoleEnum), default=RoleEnum.admin, nullable=False)
|
|
display_name = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
# Relationships
|
|
event = relationship("Event", back_populates="members")
|
|
user = relationship("User", back_populates="event_memberships")
|
|
|
|
__table_args__ = (
|
|
__import__('sqlalchemy').UniqueConstraint('event_id', 'user_id', name='uq_event_user'),
|
|
)
|
|
|
|
|
|
class GuestStatus(str, enum.Enum):
|
|
invited = "invited"
|
|
confirmed = "confirmed"
|
|
declined = "declined"
|
|
|
|
|
|
class Guest(Base):
|
|
__tablename__ = "guests_v2"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
event_id = Column(UUID(as_uuid=True), ForeignKey("events.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
added_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True)
|
|
|
|
# Guest Information
|
|
first_name = Column(String, nullable=False)
|
|
last_name = Column(String, nullable=False)
|
|
email = Column(String, nullable=True)
|
|
phone = Column(String, nullable=True) # Legacy field - use phone_number instead
|
|
phone_number = Column(String, nullable=True)
|
|
|
|
# RSVP & Preferences
|
|
rsvp_status = Column(SQLEnum(GuestStatus), default=GuestStatus.invited, nullable=False)
|
|
meal_preference = Column(String, nullable=True)
|
|
|
|
# Plus One / Companions
|
|
has_plus_one = Column(Boolean, default=False)
|
|
plus_one_name = Column(String, nullable=True)
|
|
companion_count = Column(Integer, default=0, nullable=True) # additional people coming
|
|
|
|
# Event Details
|
|
table_number = Column(String, nullable=True)
|
|
side = Column(String, nullable=True) # e.g. "groom side", "bride side"
|
|
|
|
# Source Information
|
|
owner_email = Column(String, nullable=True) # Email of person who added this guest
|
|
source = Column(String, default="manual", nullable=False) # 'google', 'manual', 'self-service'
|
|
|
|
# Notes & Metadata
|
|
notes = Column(Text, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
# Relationships
|
|
event = relationship("Event", back_populates="guests")
|
|
added_by_user = relationship("User", back_populates="guests_added", foreign_keys=[added_by_user_id])
|
|
|
|
|
|
# ── RSVP tokens ────────────────────────────────────────────────────────────
|
|
|
|
class RsvpToken(Base):
|
|
"""
|
|
One-time token generated per guest per WhatsApp send.
|
|
Encodes event + guest context so the /guest page knows which RSVP
|
|
to update without exposing UUIDs in the URL.
|
|
"""
|
|
__tablename__ = "rsvp_tokens"
|
|
|
|
token = Column(String, primary_key=True, index=True)
|
|
event_id = Column(UUID(as_uuid=True), ForeignKey("events.id", ondelete="CASCADE"), nullable=False)
|
|
guest_id = Column(UUID(as_uuid=True), ForeignKey("guests_v2.id", ondelete="SET NULL"), nullable=True)
|
|
phone = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
|
used_at = Column(DateTime(timezone=True), nullable=True)
|
|
|
|
event = relationship("Event")
|
|
guest = relationship("Guest")
|
|
|
|
|
|
# ── WhatsApp Custom Templates ──────────────────────────────────────────────────
|
|
|
|
class WhatsAppMessage(Base):
|
|
"""
|
|
Tracks WhatsApp messages sent through Meta Cloud API.
|
|
Stores wamid and delivery status from webhooks.
|
|
"""
|
|
__tablename__ = "whatsapp_messages"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
wamid = Column(String, unique=True, nullable=False, index=True) # WhatsApp message ID from Meta
|
|
|
|
# Message context
|
|
event_id = Column(UUID(as_uuid=True), ForeignKey("events.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
guest_id = Column(UUID(as_uuid=True), ForeignKey("guests_v2.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
to_phone = Column(String, nullable=False) # E.164 format
|
|
|
|
# Template info
|
|
template_key = Column(String, nullable=True)
|
|
template_name = Column(String, nullable=True)
|
|
|
|
# Status tracking (sent → delivered → read, or sent → failed)
|
|
status = Column(String, default="sent", nullable=False) # sent, delivered, read, failed
|
|
|
|
# Error details (if failed)
|
|
error_code = Column(String, nullable=True)
|
|
error_title = Column(String, nullable=True)
|
|
error_message = Column(Text, nullable=True)
|
|
|
|
# Timestamps
|
|
sent_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
delivered_at = Column(DateTime(timezone=True), nullable=True)
|
|
read_at = Column(DateTime(timezone=True), nullable=True)
|
|
failed_at = Column(DateTime(timezone=True), nullable=True)
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
# Relationships
|
|
event = relationship("Event")
|
|
guest = relationship("Guest")
|
|
|
|
|
|
class WhatsAppTemplate(Base):
|
|
"""
|
|
Stores custom WhatsApp message templates created by users.
|
|
Built-in templates are defined in code, but custom templates are persisted here.
|
|
"""
|
|
__tablename__ = "whatsapp_templates"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
template_key = Column(String, unique=True, nullable=False, index=True) # e.g., "wedding_invitation_custom_1"
|
|
meta_name = Column(String, nullable=False) # Name as it appears in Meta Business Manager
|
|
friendly_name = Column(String, nullable=False) # Display name in frontend
|
|
language_code = Column(String, default="he", nullable=False) # e.g., "he", "en"
|
|
description = Column(Text, nullable=True)
|
|
|
|
# Template structure
|
|
header_text = Column(Text, nullable=True) # Header content
|
|
body_text = Column(Text, nullable=True) # Body content with {{1}}, {{2}} placeholders
|
|
|
|
# Parameters (stored as JSON string)
|
|
header_params = Column(Text, nullable=False, default="[]") # JSON array
|
|
body_params = Column(Text, nullable=False, default="[]") # JSON array
|
|
fallbacks = Column(Text, nullable=False, default="{}") # JSON object with default values
|
|
|
|
# Optional: Button configuration
|
|
button_type = Column(String, nullable=True) # "URL", "PHONE_NUMBER", "QUICK_REPLY"
|
|
button_text = Column(String, nullable=True)
|
|
button_url = Column(String, nullable=True)
|
|
|
|
# Media support
|
|
header_type = Column(String, default="TEXT", nullable=False) # "TEXT", "IMAGE", "VIDEO", "DOCUMENT"
|
|
header_handle = Column(String, nullable=True) # Media handle for IMAGE/VIDEO/DOCUMENT
|
|
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|