import datetime import decimal import random from collections.abc import Mapping from typing import Any from fansifter_common.testing.factories import SQLAlchemyFactory from fansifter_common.utils import timezone from fansifter_common.utils.uuid import uuid_string from polyfactory import Ignore, Use from polyfactory.decorators import post_generated from dmp.ad_accounts.models import AdAccountDbt from dmp.ad_reporting.models import ( AdReportingAdCountryDbt, AdReportingAdDbt, AdReportingAdSetCountryDbt, AdReportingAdSetDbt, AdReportingCampaignBenchmarkByAccountArtistCountryDbt, AdReportingCampaignBenchmarkByAccountArtistDbt, AdReportingCampaignBenchmarkByAccountCountryDbt, AdReportingCampaignBenchmarkByAccountDbt, AdReportingCampaignCountryDbt, AdReportingCampaignDbt, AdReportingReport, ) from dmp.app_connections.enums import AppConnectionStatus from dmp.audiences.enums import AudienceExportStatus, AudienceTarget from dmp.audiences.filters import AudienceFilters from dmp.audiences.models import ( Audience, AudienceExport, AudienceFanDataList, AudienceShare, AudienceSnapshot, ) from dmp.fandata.enums import HeavyRotationTmp from dmp.fandata.models import ( AudienceFiltersAdCampaignDbt, AudienceFiltersCustomListAdCampaignDbt, AudienceFiltersCustomListEmailCampaignDbt, AudienceFiltersEmailCampaignDbt, AudienceFiltersTextCampaignDbt, EmailDomainToISPMapping, FansByArtistAccountGenderCountryDbt, GlobalAudienceFiltersAdCampaignDbt, GlobalAudienceFiltersEmailCampaignDbt, TwilioFanSubscriptionStatus, ) from dmp.fandata.utils import make_fan_id from dmp.google.models import ( GoogleAudience, GoogleUserAdAccount, GoogleUserConnection, GoogleUserConnectionAdAccount, ) from dmp.locations.models import City from dmp.meta.models import ( MetaAdAccount, MetaAdReportingConnection, MetaAudience, MetaUserAdAccount, MetaUserConnection, ) from dmp.rosters.enums import ArtistRosterStatus from dmp.rosters.models import ArtistRosterMainRep from dmp.shopify.models import ShopifyShop, ShopifyStoreAssociation from dmp.songwhip.models import SongwhipPresavePageDbt from dmp.tiktok.models import ( TikTokAdAccount, TikTokAdReportingConnection, TikTokAudience, TikTokUserAdAccount, ) # Locations class CityFactory(SQLAlchemyFactory[City]): name = "New York" country_iso2 = "US" country = Ignore() region = Ignore() # Shopify class ShopifyShopFactory(SQLAlchemyFactory[ShopifyShop]): domain = Use(lambda: SQLAlchemyFactory.__faker__.domain_name()) @post_generated # noqa @classmethod def myshopify_domain(cls, name: str) -> str: return f"{name}.myshopify.com" class ShopifyStoreAssociationFactory(SQLAlchemyFactory[ShopifyStoreAssociation]): shop_domain = Use(lambda: SQLAlchemyFactory.__faker__.domain_name()) status = AppConnectionStatus.PENDING errors: list[Mapping[str, Any]] | None = None @post_generated # noqa @classmethod def shop(cls, fivetran_schema: str) -> ShopifyShop: return ShopifyShopFactory.build(source_schema=fivetran_schema) # Audiences class AudienceFactory(SQLAlchemyFactory[Audience]): id = Use(uuid_string) filters = Use(AudienceFilters.model_construct) target = AudienceTarget.ADS archived_at = None archived_by = None fandata_lists: list[AudienceFanDataList] = [] snapshots: list[AudienceSnapshot] = [] class AudienceSnapshotFactory(SQLAlchemyFactory[AudienceSnapshot]): fan_count = Use(lambda: SQLAlchemyFactory.__faker__.pyint(min_value=1)) audience = Use(AudienceFactory.build) target = Ignore() filters = Ignore() class AudienceExportFactory(SQLAlchemyFactory[AudienceExport]): id = Use(uuid_string) snapshot = Use(AudienceSnapshotFactory.build) key = Ignore() status = AudienceExportStatus.PENDING @post_generated # noqa @classmethod def audience(cls, snapshot: AudienceSnapshot) -> Audience: return snapshot.audience class AudienceShareFactory(SQLAlchemyFactory[AudienceShare]): id = Use(uuid_string) snapshot = Use(AudienceSnapshotFactory.build) fans_count = Ignore() key = Ignore() status = AudienceExportStatus.PENDING @post_generated # noqa @classmethod def audience(cls, snapshot: AudienceSnapshot) -> Audience: return snapshot.audience # Fandata class FansByArtistAccountGenderCountryDbtFactory( SQLAlchemyFactory[FansByArtistAccountGenderCountryDbt] ): @classmethod def fans_share(cls) -> decimal.Decimal: base = decimal.Decimal(str(cls.__random__.uniform(0.001, 0.999))) return base.quantize(decimal.Decimal(10) ** -3) class AudienceFiltersAdCampaignDbtFactory( SQLAlchemyFactory[AudienceFiltersAdCampaignDbt] ): fan_id = Use(lambda: make_fan_id(SQLAlchemyFactory.__faker__.email())) dsp: Any = [] genre_list: Any = [] fan_language_list: Any = [] country_iso2 = Use(lambda: SQLAlchemyFactory.__random__.choice(["US", "GB", "EE"])) heavy_rotation = HeavyRotationTmp.UNKNOWN is_primary_fan = True @classmethod def total_spend(cls) -> decimal.Decimal: base = decimal.Decimal(str(cls.__random__.uniform(10, 1000))) return base.quantize(decimal.Decimal(10) ** -2) class AudienceFiltersEmailCampaignDbtFactory( SQLAlchemyFactory[AudienceFiltersEmailCampaignDbt] ): fan_id = Use(lambda: make_fan_id(SQLAlchemyFactory.__faker__.email())) dsp: Any = [] genre_list: Any = [] fan_language_list: Any = [] country_iso2 = Use(lambda: SQLAlchemyFactory.__random__.choice(["US", "GB", "EE"])) heavy_rotation = HeavyRotationTmp.UNKNOWN is_primary_fan = True @classmethod def total_spend(cls) -> decimal.Decimal: base = decimal.Decimal(str(cls.__random__.uniform(10, 1000))) return base.quantize(decimal.Decimal(10) ** -2) class AudienceFiltersTextCampaignDbtFactory( SQLAlchemyFactory[AudienceFiltersTextCampaignDbt] ): fan_id = Use(lambda: make_fan_id(SQLAlchemyFactory.__faker__.email())) dsp: Any = [] genre_list: Any = [] fan_language_list: Any = [] country_iso2 = Use(lambda: SQLAlchemyFactory.__random__.choice(["US", "GB", "EE"])) heavy_rotation = HeavyRotationTmp.UNKNOWN is_primary_fan = True @classmethod def total_spend(cls) -> decimal.Decimal: base = decimal.Decimal(str(cls.__random__.uniform(10, 1000))) return base.quantize(decimal.Decimal(10) ** -2) class AudienceFiltersCustomListAdCampaignDbtFactory( SQLAlchemyFactory[AudienceFiltersCustomListAdCampaignDbt] ): fan_id = Use(lambda: make_fan_id(SQLAlchemyFactory.__faker__.email())) dsp: Any = [] genre_list: Any = [] fan_language_list: Any = [] class AudienceFiltersCustomListEmailCampaignDbtFactory( SQLAlchemyFactory[AudienceFiltersCustomListEmailCampaignDbt] ): fan_id = Use(lambda: make_fan_id(SQLAlchemyFactory.__faker__.email())) dsp: Any = [] genre_list: Any = [] fan_language_list: Any = [] class GlobalAudienceFiltersAdCampaignDbtFactory( SQLAlchemyFactory[GlobalAudienceFiltersAdCampaignDbt] ): fan_id = Use(lambda: make_fan_id(SQLAlchemyFactory.__faker__.email())) dsp: Any = [] genre_list: Any = [] fan_language_list: Any = [] is_primary_fan = True @classmethod def total_spend(cls) -> decimal.Decimal: base = decimal.Decimal(str(cls.__random__.uniform(10, 1000))) return base.quantize(decimal.Decimal(10) ** -2) class GlobalAudienceFiltersEmailCampaignDbtFactory( SQLAlchemyFactory[GlobalAudienceFiltersEmailCampaignDbt] ): fan_id = Use(lambda: make_fan_id(SQLAlchemyFactory.__faker__.email())) dsp: Any = [] genre_list: Any = [] fan_language_list: Any = [] is_primary_fan = True @classmethod def total_spend(cls) -> decimal.Decimal: base = decimal.Decimal(str(cls.__random__.uniform(10, 1000))) return base.quantize(decimal.Decimal(10) ** -2) class EmailDomainToISPMappingFactory(SQLAlchemyFactory[EmailDomainToISPMapping]): pass # Meta class MetaUserConnectionFactory(SQLAlchemyFactory[MetaUserConnection]): expires_at = Use(lambda: timezone.now() + datetime.timedelta(days=60)) ad_accounts: list[MetaAdAccount] = [] ad_account_associations = Ignore() class MetaUserAdAccountFactory(SQLAlchemyFactory[MetaUserAdAccount]): ad_account = Ignore() class MetaAdReportingConnectionFactory(SQLAlchemyFactory[MetaAdReportingConnection]): status = AppConnectionStatus.PENDING ad_accounts: list[MetaAdAccount] = [] class MetaAudienceFactory(SQLAlchemyFactory[MetaAudience]): share = Use(AudienceShareFactory.build) fans_count = Ignore() # Ad Accounts class AdAccountDbtFactory(SQLAlchemyFactory[AdAccountDbt]): pass # Ad Reporting def _bounded_decimal(scale: int = 3) -> decimal.Decimal: """Return a random Decimal safe for NUMERIC(10, scale) columns (max < 10^7).""" return decimal.Decimal(str(round(random.uniform(0.001, 9999.0), scale))) def _bounded_integer(max_value: int = 5000) -> int: """Return a random int safe for metrics that drive calculations into NUMERIC(10,3). Max value keeps frequency/rate calculations within NUMERIC(10,3) bounds (< 10^7). """ return random.randint(1, max_value) class AdReportingCampaignDbtFactory(SQLAlchemyFactory[AdReportingCampaignDbt]): artist_associations = Ignore() global_participant_id = None impressions = Use(lambda: _bounded_integer()) clicks = Use(lambda: _bounded_integer(1000)) reach = Use(lambda: _bounded_integer(1000)) views = Use(lambda: _bounded_integer()) views_p25 = Use(lambda: _bounded_integer(1000)) estimated_ad_recallers = Use(lambda: _bounded_integer(500)) actions = Use(lambda: _bounded_integer(500)) rate = Use(_bounded_decimal) cost_per_action = Use(_bounded_decimal) cost_per_action_usd = Use(_bounded_decimal) frequency = Use(_bounded_decimal) spend = Use(lambda: _bounded_decimal(2)) spend_usd = Use(lambda: _bounded_decimal(2)) class AdReportingCampaignCountryDbtFactory( SQLAlchemyFactory[AdReportingCampaignCountryDbt] ): impressions = Use(lambda: _bounded_integer()) clicks = Use(lambda: _bounded_integer(1000)) reach = Use(lambda: _bounded_integer(1000)) views = Use(lambda: _bounded_integer()) views_p25 = Use(lambda: _bounded_integer(1000)) estimated_ad_recallers = Use(lambda: _bounded_integer(500)) actions = Use(lambda: _bounded_integer(500)) rate = Use(_bounded_decimal) cost_per_action = Use(_bounded_decimal) cost_per_action_usd = Use(_bounded_decimal) frequency = Use(_bounded_decimal) spend = Use(lambda: _bounded_decimal(2)) spend_usd = Use(lambda: _bounded_decimal(2)) class AdReportingAdSetDbtFactory(SQLAlchemyFactory[AdReportingAdSetDbt]): impressions = Use(lambda: _bounded_integer()) clicks = Use(lambda: _bounded_integer(1000)) reach = Use(lambda: _bounded_integer(1000)) views = Use(lambda: _bounded_integer()) views_p25 = Use(lambda: _bounded_integer(1000)) estimated_ad_recallers = Use(lambda: _bounded_integer(500)) actions = Use(lambda: _bounded_integer(500)) rate = Use(_bounded_decimal) cost_per_action = Use(_bounded_decimal) cost_per_action_usd = Use(_bounded_decimal) frequency = Use(_bounded_decimal) spend = Use(lambda: _bounded_decimal(2)) spend_usd = Use(lambda: _bounded_decimal(2)) class AdReportingAdSetCountryDbtFactory(SQLAlchemyFactory[AdReportingAdSetCountryDbt]): impressions = Use(lambda: _bounded_integer()) clicks = Use(lambda: _bounded_integer(1000)) reach = Use(lambda: _bounded_integer(1000)) views = Use(lambda: _bounded_integer()) views_p25 = Use(lambda: _bounded_integer(1000)) estimated_ad_recallers = Use(lambda: _bounded_integer(500)) actions = Use(lambda: _bounded_integer(500)) rate = Use(_bounded_decimal) cost_per_action = Use(_bounded_decimal) cost_per_action_usd = Use(_bounded_decimal) frequency = Use(_bounded_decimal) spend = Use(lambda: _bounded_decimal(2)) spend_usd = Use(lambda: _bounded_decimal(2)) class AdReportingAdDbtFactory(SQLAlchemyFactory[AdReportingAdDbt]): impressions = Use(lambda: _bounded_integer()) clicks = Use(lambda: _bounded_integer(1000)) reach = Use(lambda: _bounded_integer(1000)) views = Use(lambda: _bounded_integer()) views_p25 = Use(lambda: _bounded_integer(1000)) estimated_ad_recallers = Use(lambda: _bounded_integer(500)) actions = Use(lambda: _bounded_integer(500)) rate = Use(_bounded_decimal) cost_per_action = Use(_bounded_decimal) cost_per_action_usd = Use(_bounded_decimal) frequency = Use(_bounded_decimal) spend = Use(lambda: _bounded_decimal(2)) spend_usd = Use(lambda: _bounded_decimal(2)) class AdReportingAdCountryDbtFactory(SQLAlchemyFactory[AdReportingAdCountryDbt]): impressions = Use(lambda: _bounded_integer()) clicks = Use(lambda: _bounded_integer(1000)) reach = Use(lambda: _bounded_integer(1000)) views = Use(lambda: _bounded_integer()) views_p25 = Use(lambda: _bounded_integer(1000)) estimated_ad_recallers = Use(lambda: _bounded_integer(500)) actions = Use(lambda: _bounded_integer(500)) rate = Use(_bounded_decimal) cost_per_action = Use(_bounded_decimal) cost_per_action_usd = Use(_bounded_decimal) frequency = Use(_bounded_decimal) spend = Use(lambda: _bounded_decimal(2)) spend_usd = Use(lambda: _bounded_decimal(2)) class AdReportingCampaignBenchmarkByAccountDbtFactory( SQLAlchemyFactory[AdReportingCampaignBenchmarkByAccountDbt] ): rate = Use(_bounded_decimal) class AdReportingCampaignBenchmarkByAccountCountryDbtFactory( SQLAlchemyFactory[AdReportingCampaignBenchmarkByAccountCountryDbt] ): rate = Use(_bounded_decimal) class AdReportingCampaignBenchmarkByAccountArtistDbtFactory( SQLAlchemyFactory[AdReportingCampaignBenchmarkByAccountArtistDbt] ): rate = Use(_bounded_decimal) class AdReportingCampaignBenchmarkByAccountArtistCountryDbtFactory( SQLAlchemyFactory[AdReportingCampaignBenchmarkByAccountArtistCountryDbt] ): rate = Use(_bounded_decimal) class AdReportingReportFactory(SQLAlchemyFactory[AdReportingReport]): pass # TikTok class TikTokUserAdAccountFactory(SQLAlchemyFactory[TikTokUserAdAccount]): ad_account = Ignore() class TikTokAdReportingConnectionFactory( SQLAlchemyFactory[TikTokAdReportingConnection] ): status = AppConnectionStatus.PENDING ad_accounts: list[TikTokAdAccount] = [] class TikTokAudienceFactory(SQLAlchemyFactory[TikTokAudience]): share = Use(AudienceShareFactory.build) fans_count = Ignore() # Google class GoogleUserAdAccountFactory(SQLAlchemyFactory[GoogleUserAdAccount]): ad_account = Ignore() class GoogleAudienceFactory(SQLAlchemyFactory[GoogleAudience]): share = Use(AudienceShareFactory.build) fans_count = Ignore() # Songwhip class SongwhipPresavePageDbtFactory(SQLAlchemyFactory[SongwhipPresavePageDbt]): pass # Rosters class ArtistRosterMainRepFactory(SQLAlchemyFactory[ArtistRosterMainRep]): status = ArtistRosterStatus.ACTIVE class GoogleUserConnectionAdAccountFactory( SQLAlchemyFactory[GoogleUserConnectionAdAccount] ): user_connection_id = Ignore() parent_ad_account_external_id = None class GoogleUserConnectionFactory(SQLAlchemyFactory[GoogleUserConnection]): connection_ad_accounts: list[Any] = [] class TwilioFanSubscriptionStatusFactory( SQLAlchemyFactory[TwilioFanSubscriptionStatus] ): fan_country = "US" channel = "SMS"