"""Per-person notification preferences — the employee's control surface over the outbound notification firehose. One row per person (PK = person id). Absence of a row means "all defaults" (every channel on, no quiet hours) so the table is sparse: only people who have actually tuned something carry a row. ``NotificationPreferenceService`` treats a missing row identically to an all-default row. Two governed dimensions: * ``muted_channels`` — ``{category_key: ["feishu", "email"]}``. Listing a channel under a category suppresses that *push* channel for notifications in that category. The in-app inbox row is ALWAYS written (muting reduces interruptions; it never loses the record). Critical templates (``is_critical_template``) ignore this entirely. * quiet hours — a daily local-time window (resolved in the recipient's own timezone) during which non-critical push is parked and re-delivered when the window ends. ``start``/``end`` are ``"HH:MM"`` strings; a window where start > end (e.g. 22:00→08:00) crosses midnight. """ from __future__ import annotations import uuid from datetime import datetime from typing import Any from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, func from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base from app.core.sql_types import GUID as Uuid class NotificationPreference(Base): __tablename__ = "notification_preferences" # PK == person id (1:1). No surrogate key — the person owns exactly one # preference row and we always look it up by person id. person_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("identity_persons.id"), primary_key=True, ) # {category_key: [channel, ...]} — channels muted for that category. muted_channels: Mapped[dict[str, Any]] = mapped_column( JSON, nullable=False, default=dict, ) quiet_hours_enabled: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="0", default=False, ) # "HH:MM" local-time bounds. NULL when quiet hours disabled. quiet_hours_start: Mapped[str | None] = mapped_column(String(5)) quiet_hours_end: Mapped[str | None] = mapped_column(String(5)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) __all__ = ["NotificationPreference"]