from datetime import datetime import sqlalchemy as sa 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, relationship 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) domain: Mapped[str] class DomainToISPMapping(Model, kw_only=True): __tablename__ = "email_domain_to_isp_mapping" domain: Mapped[str] = mapped_column(primary_key=True) isp: Mapped[str] = mapped_column(primary_key=True) 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( sa.ForeignKey(Domain.id), 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) send_at: Mapped[datetime | None] = mapped_column(default=None) scheduled_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) def is_ready_to_dispatch(self) -> bool: return ( (self.is_scheduled or self.is_in_progress) and self.send_at is not None and self.send_at <= timezone.now() and not self.is_sent and not self.is_cancelled and not self.is_deleted ) @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 class CampaignBatch(Model, kw_only=True): __tablename__ = "email_campaign_batch" id: Mapped[int] = mapped_column(primary_key=True, init=False) campaign_id: Mapped[str] = mapped_column( sa.ForeignKey(Campaign.id, ondelete="RESTRICT") ) 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(), ) campaign: Mapped[Campaign] = relationship(lazy="raise", viewonly=True, init=False) 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 DeliveryQuota(Model, kw_only=True): __tablename__ = "email_campaign_delivery_quota" domain_id: Mapped[str] = mapped_column(primary_key=True) provider: Mapped[str] = mapped_column(primary_key=True) last_sent_at: Mapped[datetime | None] = mapped_column(default=None) last_hour_sent_emails: Mapped[int] = mapped_column(default=0) last_hourly_quota: Mapped[float | None] = mapped_column(default=None) class DeliveryQuotaDefault(Model, kw_only=True): __tablename__ = "email_campaign_delivery_quota_default" domain_id: Mapped[str] = mapped_column(primary_key=True) provider: Mapped[str] = mapped_column(primary_key=True) last_hourly_quota: Mapped[float] class DeliveryQuotaGrowthRate(Model, kw_only=True): __tablename__ = "email_campaign_delivery_quota_growth_rate" domain_id: Mapped[str] = mapped_column(primary_key=True) provider: Mapped[str] = mapped_column(primary_key=True) growth_rate: Mapped[float] class AudienceEmailFan(Model, kw_only=True): __tablename__ = "audience_email_fan" audience_id: Mapped[str] = mapped_column(primary_key=True) fan_id: Mapped[str] = mapped_column(primary_key=True) class FanPersonalDataDbt(Model, kw_only=True): __tablename__ = "fan_personal_data_dbt" fan_id: Mapped[str] = mapped_column(primary_key=True) fan_email: Mapped[str | None] = mapped_column(default=None) fan_country_iso2: Mapped[str | None] = mapped_column(default=None) class FansifterTeamPersonalDataDbt(Model, kw_only=True): __tablename__ = "fansifter_team_personal_data_dbt" fan_id: Mapped[str] = mapped_column(primary_key=True) fan_email: Mapped[str] fan_country_iso2: Mapped[str | None] = mapped_column(default=None) class Profile(Model, kw_only=True): __tablename__ = "profile" id: Mapped[str] = mapped_column(primary_key=True) email: Mapped[str] first_name: Mapped[str | None] = mapped_column(default=None) updated_at: Mapped[datetime] = mapped_column( default_factory=timezone.now, onupdate=timezone.now )