from __future__ import annotations import uuid from datetime import datetime from typing import Any from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint, text from sqlalchemy.orm import Mapped, mapped_column, relationship from govoplan_core.db.base import Base, TimestampMixin def new_uuid() -> str: return str(uuid.uuid4()) class Poll(Base, TimestampMixin): __tablename__ = "poll_polls" __table_args__ = ( Index( "uq_poll_polls_active_slug", "tenant_id", "slug", unique=True, sqlite_where=text("deleted_at IS NULL"), postgresql_where=text("deleted_at IS NULL"), ), Index("ix_poll_polls_tenant_status", "tenant_id", "status"), Index("ix_poll_polls_tenant_kind", "tenant_id", "kind"), Index("ix_poll_polls_window", "tenant_id", "opens_at", "closes_at"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) slug: Mapped[str] = mapped_column(String(120), nullable=False) title: Mapped[str] = mapped_column(String(500), nullable=False) description: Mapped[str | None] = mapped_column(Text) kind: Mapped[str] = mapped_column(String(40), nullable=False, index=True) status: Mapped[str] = mapped_column(String(30), default="draft", nullable=False, index=True) visibility: Mapped[str] = mapped_column(String(30), default="private", nullable=False, index=True) result_visibility: Mapped[str] = mapped_column(String(30), default="after_close", nullable=False) context_module: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True) context_resource_type: Mapped[str | None] = mapped_column(String(100), nullable=True) context_resource_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) participation_gateway_: Mapped[dict[str, Any] | None] = mapped_column( "participation_gateway", JSON, nullable=True, ) workflow_state: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True) workflow_steps: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False) allow_anonymous: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) allow_response_update: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) min_choices: Mapped[int] = mapped_column(Integer, default=1, nullable=False) max_choices: Mapped[int | None] = mapped_column(Integer) opens_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) closes_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) decided_option_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) archived_from_status: Mapped[str | None] = mapped_column(String(30), nullable=True) created_by_user_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True) options: Mapped[list["PollOption"]] = relationship(back_populates="poll", cascade="all, delete-orphan", order_by="PollOption.position") responses: Mapped[list["PollResponse"]] = relationship(back_populates="poll", cascade="all, delete-orphan") invitations: Mapped[list["PollInvitation"]] = relationship(back_populates="poll", cascade="all, delete-orphan") lifecycle_transitions: Mapped[list["PollLifecycleTransition"]] = relationship( back_populates="poll", cascade="all, delete-orphan", order_by="PollLifecycleTransition.created_at", ) class PollOption(Base, TimestampMixin): __tablename__ = "poll_options" __table_args__ = ( UniqueConstraint("poll_id", "key", name="uq_poll_options_poll_key"), Index("ix_poll_options_poll_position", "poll_id", "position"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) poll_id: Mapped[str] = mapped_column(ForeignKey("poll_polls.id", ondelete="CASCADE"), nullable=False, index=True) key: Mapped[str] = mapped_column(String(120), nullable=False) label: Mapped[str] = mapped_column(String(500), nullable=False) description: Mapped[str | None] = mapped_column(Text) position: Mapped[int] = mapped_column(Integer, default=0, nullable=False) value: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) poll: Mapped[Poll] = relationship(back_populates="options") class PollResponse(Base, TimestampMixin): __tablename__ = "poll_responses" __table_args__ = ( Index("ix_poll_responses_poll_submitted", "poll_id", "submitted_at"), Index("ix_poll_responses_poll_respondent", "poll_id", "respondent_id"), Index( "uq_poll_responses_active_respondent", "poll_id", "respondent_id", unique=True, sqlite_where=text( "deleted_at IS NULL AND respondent_id IS NOT NULL" ), postgresql_where=text( "deleted_at IS NULL AND respondent_id IS NOT NULL" ), ), Index("ix_poll_responses_tenant_poll", "tenant_id", "poll_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) poll_id: Mapped[str] = mapped_column(ForeignKey("poll_polls.id", ondelete="CASCADE"), nullable=False, index=True) respondent_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) respondent_label: Mapped[str | None] = mapped_column(String(500), nullable=True) answers: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False) submitted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True) poll: Mapped[Poll] = relationship(back_populates="responses") class PollInvitation(Base, TimestampMixin): __tablename__ = "poll_invitations" __table_args__ = ( UniqueConstraint("token_hash", name="uq_poll_invitations_token_hash"), Index("ix_poll_invitations_poll", "tenant_id", "poll_id"), Index("ix_poll_invitations_poll_email", "poll_id", "email"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) poll_id: Mapped[str] = mapped_column(ForeignKey("poll_polls.id", ondelete="CASCADE"), nullable=False, index=True) token_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True) respondent_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) respondent_label: Mapped[str | None] = mapped_column(String(500), nullable=True) email: Mapped[str | None] = mapped_column(String(320), nullable=True, index=True) expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) response_gateway_: Mapped[dict[str, Any] | None] = mapped_column("response_gateway", JSON, nullable=True) participation_policy_: Mapped[dict[str, Any] | None] = mapped_column("participation_policy", JSON, nullable=True) metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True) poll: Mapped[Poll] = relationship(back_populates="invitations") class PollParticipationSubmission(Base, TimestampMixin): __tablename__ = "poll_participation_submissions" __table_args__ = ( UniqueConstraint( "invitation_id", "idempotency_key", name="uq_poll_participation_submission_idempotency", ), Index( "ix_poll_participation_submission_poll_created", "tenant_id", "poll_id", "created_at", ), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) poll_id: Mapped[str] = mapped_column( ForeignKey("poll_polls.id", ondelete="CASCADE"), nullable=False, index=True, ) invitation_id: Mapped[str] = mapped_column( ForeignKey("poll_invitations.id", ondelete="CASCADE"), nullable=False, index=True, ) response_id: Mapped[str] = mapped_column( ForeignKey("poll_responses.id", ondelete="CASCADE"), nullable=False, index=True, ) idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False) request_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False) class PollLifecycleTransition(Base, TimestampMixin): __tablename__ = "poll_lifecycle_transitions" __table_args__ = ( UniqueConstraint("poll_id", "idempotency_key", name="uq_poll_lifecycle_transition_idempotency"), Index("ix_poll_lifecycle_transition_poll_created", "poll_id", "created_at"), Index("ix_poll_lifecycle_transition_tenant_poll", "tenant_id", "poll_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) poll_id: Mapped[str] = mapped_column(ForeignKey("poll_polls.id", ondelete="CASCADE"), nullable=False, index=True) action: Mapped[str] = mapped_column(String(30), nullable=False, index=True) from_status: Mapped[str] = mapped_column(String(30), nullable=False) to_status: Mapped[str] = mapped_column(String(30), nullable=False) decision_option_id: Mapped[str | None] = mapped_column(String(36), nullable=True) previous_decision_option_id: Mapped[str | None] = mapped_column(String(36), nullable=True) idempotency_key: Mapped[str | None] = mapped_column(String(255), nullable=True) actor_user_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) actor_api_key_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True) metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True) poll: Mapped[Poll] = relationship(back_populates="lifecycle_transitions") __all__ = [ "Poll", "PollInvitation", "PollLifecycleTransition", "PollOption", "PollParticipationSubmission", "PollResponse", "new_uuid", ]