from __future__ import annotations import contextlib from datetime import datetime import sqlalchemy as sa from fansifter_common.adapters.db.types import ChoiceType from fansifter_common.auth.account import Account from fansifter_common.utils import timezone from fansifter_common.utils.uuid import uuid_string from sqlalchemy.orm import Mapped, mapped_column, relationship from dmp.adapters.db import Model from dmp.adapters.db.sqltypes import EncryptedTextType from dmp.adapters.google.models import UserAdAccount from dmp.app_connections.enums import AppConnectionStatus from dmp.audiences.models import AudienceShare from dmp.core.types import EncryptedToken class GoogleAdAccount(Model, kw_only=True): __tablename__ = "google_ad_account" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) external_id: Mapped[str] = mapped_column(unique=True) name: Mapped[str | None] = mapped_column(default=None) business_account_id: Mapped[str | None] = mapped_column(default=None) business_account_name: Mapped[str | None] = mapped_column(default=None) campaigns_count: Mapped[int] = mapped_column(default=0) 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 display_name(self) -> str: if self.name and len(self.name) > 0: return self.name return self.external_id class GoogleUserConnectionAdAccount(Model, kw_only=True): __tablename__ = "google_user_connection_ad_account" user_connection_id: Mapped[str] = mapped_column( sa.ForeignKey("google_user_connection.id"), primary_key=True, init=False, ) ad_account_id: Mapped[str] = mapped_column( sa.ForeignKey("google_ad_account.id"), primary_key=True, init=False, ) parent_ad_account_id: Mapped[str | None] = mapped_column(default=None) ad_account: Mapped[GoogleAdAccount] = relationship(lazy="raise", repr=False) def __post_init__(self) -> None: self.ad_account_id = self.ad_account.id class GoogleUserConnection(Model, kw_only=True): __tablename__ = "google_user_connection" __table_args__ = (sa.UniqueConstraint("identity_id", "user_id"),) id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) identity_id: Mapped[str] user_id: Mapped[str] user_name: Mapped[str] = mapped_column(default="") token: Mapped[EncryptedToken] = mapped_column(EncryptedTextType) is_valid: Mapped[bool] = mapped_column(default=True) created_at: Mapped[datetime] = mapped_column(default_factory=timezone.now) updated_at: Mapped[datetime] = mapped_column( default_factory=timezone.now, onupdate=timezone.now ) # relations connection_ad_accounts: Mapped[list[GoogleUserConnectionAdAccount]] = relationship( lazy="raise", default_factory=list, cascade="all, delete-orphan", repr=False, ) @property def ad_accounts_count(self) -> int: return len(self.connection_ad_accounts) def get_connection_ad_account( self, ad_account_id: str ) -> GoogleUserConnectionAdAccount | None: for connection_ad_account in self.connection_ad_accounts: if connection_ad_account.ad_account_id == ad_account_id: return connection_ad_account return None def remove_connection_ad_account( self, connection_ad_account: GoogleUserConnectionAdAccount ) -> None: with contextlib.suppress(ValueError): self.connection_ad_accounts.remove(connection_ad_account) def add_ad_accounts( self, ad_accounts: list[GoogleAdAccount], user_ad_accounts: list[UserAdAccount], ) -> None: parent_ad_account_ids: dict[str, str] = { ad_account.id: ad_account.login_customer_id for ad_account in user_ad_accounts if ad_account.login_customer_id } connection_ad_accounts: list[GoogleUserConnectionAdAccount] = [] for ad_account in ad_accounts: parent_ad_account_id = parent_ad_account_ids.get( ad_account.external_id, None ) connection_ad_accounts.append( GoogleUserConnectionAdAccount( ad_account=ad_account, parent_ad_account_id=parent_ad_account_id, ) ) self.connection_ad_accounts = connection_ad_accounts class GoogleUserAdAccount(Model, kw_only=True): __tablename__ = "google_user_ad_account" __table_args__ = (sa.UniqueConstraint("identity_id", "ad_account_id"),) id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) identity_id: Mapped[str] ad_account_id: Mapped[str] = mapped_column(sa.ForeignKey("google_ad_account.id")) vendor_id: Mapped[int] subaccount_id: Mapped[int] created_at: Mapped[datetime] = mapped_column(default_factory=timezone.now) updated_at: Mapped[datetime] = mapped_column( default_factory=timezone.now, onupdate=timezone.now ) # relations ad_account: Mapped[GoogleAdAccount] = relationship(lazy="raise", init=False) @property def account(self) -> Account: return Account(vendor_id=self.vendor_id, subaccount_id=self.subaccount_id) class GoogleAudience(Model, kw_only=True): __tablename__ = "google_audience" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) name: Mapped[str] external_id: Mapped[str] = mapped_column(unique=True) resource_name: Mapped[str] = mapped_column(unique=True) ad_account_id: Mapped[str] = mapped_column( sa.ForeignKey("google_ad_account.id"), init=False ) fans_count: Mapped[int] = mapped_column(init=False) audience_id: Mapped[str] = mapped_column(sa.ForeignKey("audience.id"), init=False) snapshot_id: Mapped[str] = mapped_column( sa.ForeignKey("audience_snapshot.id"), init=False ) share_id: Mapped[str] = mapped_column( sa.ForeignKey("audience_share.id"), unique=True, init=False ) created_at: Mapped[datetime] = mapped_column(default_factory=timezone.now) # relations ad_account: Mapped[GoogleAdAccount] = relationship(lazy="raise") share: Mapped[AudienceShare] = relationship(lazy="raise") def __post_init__(self) -> None: self.audience_id = self.share.audience_id self.snapshot_id = self.share.snapshot_id self.share_id = self.share.id self.fans_count = self.share.fans_count self.ad_account_id = self.ad_account.id @property def ad_account_external_id(self) -> str: return self.ad_account.external_id @property def url(self) -> str: return ( "https://ads.google.com/aw/audiences/management/details" f"?&userListId={self.external_id}" ) class _GoogleAdReportingConnectionAdAccount(Model, kw_only=True): __tablename__ = "google_ad_reporting_connection_ad_account" connection_id: Mapped[str] = mapped_column( sa.ForeignKey("google_ad_reporting_connection.id"), primary_key=True ) ad_account_id: Mapped[str] = mapped_column( sa.ForeignKey("google_ad_account.id"), primary_key=True ) class GoogleAdReportingConnection(Model, kw_only=True): __tablename__ = "google_ad_reporting_connection" id: Mapped[str] = mapped_column(primary_key=True, default_factory=uuid_string) user_id: Mapped[str] fivetran_connector_id: Mapped[str] identity_id: Mapped[str] fivetran_schema: Mapped[str] = mapped_column(unique=True) status: Mapped[AppConnectionStatus] = mapped_column( ChoiceType(AppConnectionStatus), default=AppConnectionStatus.PENDING ) is_paused: Mapped[bool] = mapped_column(default=False) initial_email_sent: Mapped[bool] = mapped_column(default=False) synced_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 ) # relations ad_accounts: Mapped[list[GoogleAdAccount]] = relationship( secondary=_GoogleAdReportingConnectionAdAccount.__table__, lazy="raise", default_factory=list, repr=False, ) def __str__(self) -> str: return self.fivetran_schema @property def ad_accounts_count(self) -> int: return len(self.ad_accounts) def remove_ad_account(self, ad_account: GoogleAdAccount) -> None: with contextlib.suppress(ValueError): self.ad_accounts.remove(ad_account)