from datetime import datetime from fansifter_common.adapters.db.types import ChoiceType from fansifter_common.utils import timezone from fansifter_common.utils.uuid import uuid_string from sqlalchemy.orm import Mapped, mapped_column from app.adapters.db import Model from app.enums import ( CampaignCancelReason, CampaignStatus, MessageSendQueueStatus, ) class Campaign(Model, kw_only=True): __tablename__ = "text_campaign" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) name: Mapped[str] vendor_id: Mapped[int] subaccount_id: Mapped[int] global_participant_id: Mapped[str] status: Mapped[CampaignStatus] = mapped_column( ChoiceType(CampaignStatus), default=CampaignStatus.DRAFT ) audience_id: Mapped[str | None] = mapped_column(default=None) audience_snapshot_id: Mapped[str | None] = mapped_column(default=None) recipients_count: Mapped[int | None] = mapped_column(default=None) content: Mapped[str | None] = mapped_column(default=None) send_at: Mapped[datetime | None] = mapped_column(default=None) send_at_timezone: Mapped[str | None] = mapped_column(default=None) cancel_reason: Mapped[CampaignCancelReason | None] = mapped_column(default=None) cancelled_at: Mapped[datetime | None] = mapped_column(default=None) created_at: Mapped[datetime] = mapped_column(default_factory=timezone.now) created_by: Mapped[str] updated_at: Mapped[datetime] = mapped_column( default_factory=timezone.now, onupdate=timezone.now ) updated_by: Mapped[str] deleted_at: Mapped[datetime | None] = mapped_column(default=None) def cancel(self, reason: CampaignCancelReason) -> None: self.status = CampaignStatus.CANCELLED self.cancel_reason = reason self.cancelled_at = timezone.now() @property def is_processable(self) -> bool: return ( self.status in [ CampaignStatus.SCHEDULED, CampaignStatus.IN_PROGRESS, ] and not self.is_deleted ) @property def is_deleted(self) -> bool: return self.deleted_at is not None @property def is_in_progress(self) -> bool: return self.status == CampaignStatus.IN_PROGRESS @property def is_sent(self) -> bool: return self.status == CampaignStatus.SENT @property def is_cancelled(self) -> bool: return self.status == CampaignStatus.CANCELLED class MessageSendQueue(Model, kw_only=True): __tablename__ = "text_message_send_queue" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) campaign_id: Mapped[str] status: Mapped[MessageSendQueueStatus] = mapped_column( ChoiceType(MessageSendQueueStatus), default=MessageSendQueueStatus.QUEUED ) total: Mapped[int] offset: Mapped[int] = mapped_column(default=0) send_at: Mapped[datetime] first_sent_at: Mapped[datetime | None] = mapped_column(default=None) last_sent_at: Mapped[datetime | None] = mapped_column(default=None) created_at: Mapped[datetime] = mapped_column(default_factory=timezone.now) updated_at: Mapped[datetime] = mapped_column( default_factory=timezone.now, onupdate=timezone.now ) @property def is_queued(self) -> bool: return self.status == MessageSendQueueStatus.QUEUED @property def is_processing(self) -> bool: return self.status == MessageSendQueueStatus.PROCESSING @property def is_completed(self) -> bool: return self.status == MessageSendQueueStatus.COMPLETED @property def is_canceled(self) -> bool: return self.status == MessageSendQueueStatus.CANCELED class AudienceFan(Model, kw_only=True): __tablename__ = "audience_fan" snapshot_id: Mapped[str] = mapped_column(primary_key=True) fan_id: Mapped[str] = mapped_column(primary_key=True) last_engagement_at: Mapped[datetime | None] = mapped_column(default=None) class FanPersonalDataDbt(Model, kw_only=True): __tablename__ = "fan_personal_data_dbt" fan_id: Mapped[str] = mapped_column(primary_key=True) fan_mobile_phone: Mapped[str | None] = mapped_column(default=None)