invy/backend/models.py

177 lines
7.8 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 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())