from datetime import datetime from typing import Any from sqlalchemy import ( JSON, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base from app.core.utils.time import utc_now from app.modules.subscriptions.constants import ( PushDeliveryStatus, PushSubscriptionStatus, ) class PushSubscription(Base): __tablename__ = "push_subscriptions" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) code: Mapped[str] = mapped_column(String(64), unique=True, index=True) owner_id: Mapped[int] = mapped_column( ForeignKey("feishu_users.id", ondelete="CASCADE"), index=True, ) target_type: Mapped[str] = mapped_column(String(16), index=True) target_id: Mapped[str] = mapped_column(String(256)) prompt: Mapped[str] = mapped_column(Text) schedule_type: Mapped[str] = mapped_column(String(32), index=True) schedule_config: Mapped[dict[str, Any]] = mapped_column(JSON) timezone: Mapped[str] = mapped_column(String(64)) next_run_at: Mapped[datetime | None] = mapped_column( DateTime, nullable=True, index=True, ) status: Mapped[str] = mapped_column( String(32), default=PushSubscriptionStatus.ACTIVE, index=True, ) consented_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now) last_run_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) locked_by: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) locked_until: Mapped[datetime | None] = mapped_column( DateTime, nullable=True, index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, index=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=utc_now, onupdate=utc_now, ) deliveries: Mapped[list["PushDelivery"]] = relationship( back_populates="subscription", cascade="all, delete-orphan", passive_deletes=True, ) class PushDelivery(Base): __tablename__ = "push_deliveries" __table_args__ = ( UniqueConstraint( "subscription_id", "scheduled_for", name="uq_push_delivery_subscription_schedule", ), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) code: Mapped[str] = mapped_column(String(64), unique=True, index=True) subscription_id: Mapped[int] = mapped_column( ForeignKey("push_subscriptions.id", ondelete="CASCADE"), index=True, ) scheduled_for: Mapped[datetime] = mapped_column(DateTime, index=True) idempotency_key: Mapped[str] = mapped_column(String(64), unique=True, index=True) message_uuid: Mapped[str] = mapped_column(String(36), unique=True, index=True) status: Mapped[str] = mapped_column( String(32), default=PushDeliveryStatus.PENDING, index=True, ) attempt_count: Mapped[int] = mapped_column(Integer, default=0) next_attempt_at: Mapped[datetime | None] = mapped_column( DateTime, nullable=True, index=True, ) rendered_content: Mapped[str | None] = mapped_column(Text, nullable=True) provider_message_id: Mapped[str | None] = mapped_column( String(256), nullable=True, index=True, ) last_error: Mapped[str | None] = mapped_column(Text, nullable=True) locked_by: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) locked_until: Mapped[datetime | None] = mapped_column( DateTime, nullable=True, index=True, ) sent_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, index=True) updated_at: Mapped[datetime] = mapped_column( DateTime, default=utc_now, onupdate=utc_now, ) subscription: Mapped[PushSubscription] = relationship(back_populates="deliveries")