from datetime import datetime import sqlalchemy as sa from fansifter_common.adapters.db.types import ChoiceType from fansifter_common.core.enums import Brand 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 class Domain(Model, kw_only=True): __tablename__ = "email_domain" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) brand: Mapped[Brand] = mapped_column(ChoiceType(Brand)) domain: Mapped[str] class Campaign(Model, kw_only=True): __tablename__ = "email_campaign" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) name: Mapped[str] vendor_id: Mapped[int] domain_id: Mapped[str | None] = mapped_column(name="email_domain_id", default=None) status: Mapped[CampaignStatus] = mapped_column( ChoiceType(CampaignStatus), default=CampaignStatus.DRAFT ) audience_id: Mapped[str | None] = mapped_column(default=None) recipient_count: Mapped[int] = mapped_column(name="recipients_count", default=0) sender_name: Mapped[str | None] = mapped_column(default=None) email_username: Mapped[str | None] = mapped_column(default=None) subject: Mapped[str | None] = mapped_column(default=None) html_content: Mapped[str | None] = mapped_column(default=None) css_content: Mapped[str | None] = mapped_column(default=None) compressed_content: Mapped[str | None] = mapped_column(default=None) preview_text: Mapped[str | None] = mapped_column(default=None) send_at: Mapped[datetime | None] = mapped_column(default=None) cancel_reason: Mapped[CampaignCancelReason | None] = mapped_column(default=None) prepared_at: Mapped[datetime | None] = mapped_column(default=None) cancelled_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 ) deleted_at: Mapped[datetime | None] = mapped_column(default=None) @property def is_deleted(self) -> bool: return self.deleted_at is not None @property def is_scheduled(self) -> bool: return self.status == CampaignStatus.SCHEDULED @property def is_prepared(self) -> bool: return self.prepared_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 @property def is_active(self) -> bool: """Return True if the campaign is active (scheduled to send or in progress).""" return ( (self.is_scheduled or self.is_in_progress) and not (self.is_cancelled or self.is_deleted) and self.is_prepared and self.send_at is not None and self.send_at <= timezone.now() ) class CampaignBatch(Model, kw_only=True): __tablename__ = "email_campaign_batch" id: Mapped[int] = mapped_column(primary_key=True, init=False) campaign_id: Mapped[str] domain_id: Mapped[str] provider: Mapped[str] batch_size: Mapped[int] batch_offset: Mapped[int] = mapped_column(default=0, server_default=sa.text("0")) first_sent_at: Mapped[datetime | None] = mapped_column(default=None) last_sent_at: Mapped[datetime | None] = mapped_column(default=None) last_hour_sent_emails: Mapped[int] = mapped_column( default=0, server_default=sa.text("0") ) last_hourly_quota: Mapped[float | None] = mapped_column(default=None) completed_at: Mapped[datetime | None] = mapped_column(default=None) cancelled_at: Mapped[datetime | None] = mapped_column(default=None) created_at: Mapped[datetime] = mapped_column( default_factory=timezone.now, server_default=sa.func.now() ) updated_at: Mapped[datetime] = mapped_column( default_factory=timezone.now, onupdate=timezone.now, server_default=sa.func.now(), ) @property def is_active(self) -> bool: return ( self.completed_at is None and self.cancelled_at is None and self.batch_offset < self.batch_size ) @property def is_completed(self) -> bool: return self.completed_at is not None class BatchRecipient(Model, kw_only=True): __tablename__ = "email_campaign_batch_recipient" id: Mapped[int] = mapped_column(primary_key=True, init=False) batch_id: Mapped[int] = mapped_column( sa.ForeignKey(CampaignBatch.id, ondelete="RESTRICT") ) fan_id: Mapped[str] fan_email: Mapped[str] fan_country_iso2: Mapped[str | None] = mapped_column(default=None) profile_id: Mapped[str] fan_first_name: Mapped[str | None] = mapped_column(default=None) class BatchSendRecord(Model, kw_only=True): __tablename__ = "email_campaign_batch_send_record" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) batch_id: Mapped[int] = mapped_column( sa.ForeignKey(CampaignBatch.id, ondelete="RESTRICT") ) batch_offset: Mapped[int] current_hourly_quota: Mapped[float] sent_emails: Mapped[int] sent_at: Mapped[datetime]