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())