invy/backend/models.py
dvirlabs 7262ba4718
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Add order by rsvp status and
by how many
add refresh button
choose column to display to the guest page
2026-04-03 11:04:09 +03:00

139 lines
5.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")