""" SQLAlchemy ORM Models Clean, focused data models for parking management """ import enum from sqlalchemy import Column, String, Integer, Text, ForeignKey, Index, Enum, Date, DateTime, Boolean from sqlalchemy.orm import relationship, declarative_base from datetime import datetime, date Base = declarative_base() class UserRole(str, enum.Enum): ADMIN = "admin" MANAGER = "manager" EMPLOYEE = "employee" class PresenceStatus(str, enum.Enum): PRESENT = "present" REMOTE = "remote" ABSENT = "absent" class NotificationType(str, enum.Enum): PRESENCE_REMINDER = "presence_reminder" WEEKLY_PARKING = "weekly_parking" DAILY_PARKING = "daily_parking" PARKING_CHANGE = "parking_change" class WeekDay(enum.IntEnum): # Matches Python's calendar (0=Monday)? No! # The current DB convention in ManagerWeeklyClosingDay seems to be 0=Sunday based on comment: # "0=Sunday, 1=Monday, ..., 6=Saturday" # To keep consistency with existing logic comments, we'll stick to that, # OR we can switch to standard Python (0=Monday). # Plan said: "IntEnum matching DB convention (0=Sunday, 1=Monday, ...)" MONDAY = 0 TUESDAY = 1 WEDNESDAY = 2 THURSDAY = 3 FRIDAY = 4 SATURDAY = 5 SUNDAY = 6 class Office(Base): """Organization units that have parking spots""" __tablename__ = "offices" id = Column(Text, primary_key=True) name = Column(Text, nullable=False) parking_quota = Column(Integer, default=0) spot_prefix = Column(Text) # Letter prefix: A, B, C # Booking Window Settings (Batch Assignment) booking_window_enabled = Column(Boolean, default=False) booking_window_end_hour = Column(Integer, default=18) # 0-23 booking_window_end_minute = Column(Integer, default=0) # 0-59 created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships users = relationship("User", back_populates="office") closing_days = relationship("OfficeClosingDay", back_populates="office", cascade="all, delete-orphan") weekly_closing_days = relationship("OfficeWeeklyClosingDay", back_populates="office", cascade="all, delete-orphan") class User(Base): """Application users""" __tablename__ = "users" id = Column(Text, primary_key=True) email = Column(Text, unique=True, nullable=False) password_hash = Column(Text) name = Column(Text) role = Column(Enum(UserRole, values_callable=lambda obj: [e.value for e in obj]), nullable=False, default=UserRole.EMPLOYEE) office_id = Column(Text, ForeignKey("offices.id")) # Which office this user belongs to # User preferences week_start_day = Column(Integer, default=0) # 0=Sunday, 1=Monday, ... (Matches WeekDay logic) # Notification preferences notify_weekly_parking = Column(Boolean, default=True) # Weekly parking summary (Friday at 12) notify_daily_parking = Column(Boolean, default=True) # Daily parking reminder notify_daily_parking_hour = Column(Integer, default=8) # Hour for daily reminder (0-23) notify_daily_parking_minute = Column(Integer, default=0) # Minute for daily reminder (0-59) notify_parking_changes = Column(Boolean, default=True) # Immediate notification on assignment changes created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships office = relationship("Office", back_populates="users") presences = relationship("UserPresence", back_populates="user", cascade="all, delete-orphan") assignments = relationship("DailyParkingAssignment", back_populates="user", foreign_keys="DailyParkingAssignment.user_id") __table_args__ = ( Index('idx_user_email', 'email'), Index('idx_user_office', 'office_id'), ) class UserPresence(Base): """Daily presence records""" __tablename__ = "user_presences" id = Column(Text, primary_key=True) user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) date = Column(Date, nullable=False) status = Column(Enum(PresenceStatus, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # present, remote, absent created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships user = relationship("User", back_populates="presences") __table_args__ = ( Index('idx_presence_user_date', 'user_id', 'date', unique=True), Index('idx_presence_date', 'date'), ) class DailyParkingAssignment(Base): """Parking spot assignments per day - spots belong to offices""" __tablename__ = "daily_parking_assignments" id = Column(Text, primary_key=True) date = Column(Date, nullable=False) spot_id = Column(Text, nullable=False) # A1, A2, B1, B2, etc. (prefix from office) user_id = Column(Text, ForeignKey("users.id", ondelete="SET NULL")) office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False) # Office that owns the spot created_at = Column(DateTime, default=datetime.utcnow) # Relationships user = relationship("User", back_populates="assignments", foreign_keys=[user_id]) office = relationship("Office") __table_args__ = ( Index('idx_assignment_office_date', 'office_id', 'date'), Index('idx_assignment_user', 'user_id'), Index('idx_assignment_date_spot', 'date', 'spot_id'), ) class OfficeClosingDay(Base): """Specific date closing days for an office's parking pool (holidays, special closures)""" __tablename__ = "office_closing_days" id = Column(Text, primary_key=True) office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False) date = Column(Date, nullable=False) end_date = Column(Date) reason = Column(Text) # Relationships office = relationship("Office", back_populates="closing_days") __table_args__ = ( Index('idx_closing_office_date', 'office_id', 'date', unique=True), ) class OfficeWeeklyClosingDay(Base): """Weekly recurring closing days for an office's parking pool (e.g., Saturday and Sunday)""" __tablename__ = "office_weekly_closing_days" id = Column(Text, primary_key=True) office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False) weekday = Column(Integer, nullable=False) # 0=Sunday, 1=Monday, ..., 6=Saturday (Matches WeekDay Enum logic) # Relationships office = relationship("Office", back_populates="weekly_closing_days") __table_args__ = ( Index('idx_weekly_closing_office_day', 'office_id', 'weekday', unique=True), ) class ParkingGuarantee(Base): """Users guaranteed a parking spot when present (set by office manager)""" __tablename__ = "parking_guarantees" id = Column(Text, primary_key=True) office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False) user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) start_date = Column(Date) # Optional (null = no start limit) end_date = Column(Date) # Optional (null = no end limit) notes = Column(Text, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) # Relationships office = relationship("Office") user = relationship("User", foreign_keys=[user_id]) __table_args__ = ( Index('idx_guarantee_office_user', 'office_id', 'user_id', unique=True), ) class ParkingExclusion(Base): """Users excluded from parking assignment (set by office manager)""" __tablename__ = "parking_exclusions" id = Column(Text, primary_key=True) office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False) user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) start_date = Column(Date) # Optional end_date = Column(Date) # Optional notes = Column(Text, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) # Relationships office = relationship("Office") user = relationship("User", foreign_keys=[user_id]) __table_args__ = ( Index('idx_exclusion_office_user', 'office_id', 'user_id', unique=True), ) class NotificationLog(Base): """Log of sent notifications to prevent duplicates""" __tablename__ = "notification_logs" id = Column(Text, primary_key=True) user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) notification_type = Column(Enum(NotificationType, values_callable=lambda obj: [e.value for e in obj]), nullable=False) reference_date = Column(Text) # The date/week this notification refers to (YYYY-MM-DD or YYYY-Www) - keeping as Text for flexibility sent_at = Column(DateTime, default=datetime.utcnow) __table_args__ = ( Index('idx_notification_user_type_date', 'user_id', 'notification_type', 'reference_date'), ) class NotificationQueue(Base): """Queue for pending notifications (for immediate parking change notifications)""" __tablename__ = "notification_queue" id = Column(Text, primary_key=True) user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) notification_type = Column(Enum(NotificationType, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # parking_change subject = Column(Text, nullable=False) body = Column(Text, nullable=False) created_at = Column(DateTime, default=datetime.utcnow) sent_at = Column(DateTime) # null = not sent yet __table_args__ = ( Index('idx_queue_pending', 'sent_at'), )