import datetime import decimal from typing import get_args import pytest from dirty_equals import IsList, IsNumber from faker import Faker from fansifter_common.utils import timezone from freezegun import freeze_time from dmp.ad_reporting.dtos import ( AdReportingCampaignCriteria, AdReportingCampaignObjective, AdReportingCampaignObjectiveCriteria, ) from dmp.ad_reporting.enums import ( AdReportingActionType, AdReportingObjective, AdReportingPlatform, AdReportingRateType, ) from dmp.ad_reporting.models import ( AdReportingCampaignDbt, AdReportingReport, ) from dmp.ad_reporting.repositories import AdReportingCampaignRepository from dmp.ad_reporting.types import AdReportingCampaignOrderBy from dmp.adapters.db import ReportingDB from tests.unit.faker import FakerTyped from tests.unit.types import CreateReportingModel class TestAdReportingCampaignRepository: @pytest.mark.db @pytest.mark.parametrize("order_by", get_args(AdReportingCampaignOrderBy)) def test_find_by_criteria_supported_order_by_columns( self, repository: AdReportingCampaignRepository, order_by: AdReportingCampaignOrderBy, ) -> None: campaigns = repository.find_by_criteria( AdReportingCampaignCriteria(), order_by=[order_by], ) assert campaigns == [] @pytest.mark.db def test_find_by_criteria( self, repository: AdReportingCampaignRepository, fake: FakerTyped, ) -> None: campaigns = repository.find_by_criteria( AdReportingCampaignCriteria( search="test", platform=AdReportingPlatform.META, objective=AdReportingObjective.OUTCOME_TRAFFIC, global_participant_id=fake.uuid4_string(), campaign_ids={fake.uuid4_string(), fake.uuid4_string()}, account_ids={fake.uuid4_string(), fake.uuid4_string()}, ), limit=10, offset=5, ) assert campaigns == [] @pytest.mark.db def test_find_by_criteria_fields( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, ) -> None: campaign = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, ) campaigns = repository.find_by_criteria( AdReportingCampaignCriteria(), limit=1, offset=0, ) assert len(campaigns) == 1 result = campaigns[0] assert result.id == campaign.id assert result.external_id == campaign.external_id assert result.name == campaign.name assert result.account_id == campaign.account_id assert result.accounts == set() assert result.global_participant_ids == campaign.global_participant_ids assert result.platform == campaign.platform assert result.objective == campaign.objective assert result.start_at == campaign.start_at assert result.end_at == campaign.end_at assert result.clicks == campaign.clicks assert result.impressions == campaign.impressions assert result.reach == campaign.reach assert result.estimated_ad_recallers == campaign.estimated_ad_recallers assert result.views_p25 == campaign.views_p25 assert result.views == campaign.views assert result.purchases == campaign.purchases assert result.follows == campaign.follows assert result.spend == campaign.spend assert result.currency == campaign.currency assert result.spend_usd == campaign.spend_usd assert result.actions == campaign.actions assert result.action_type == campaign.action_type assert result.rate == campaign.rate assert result.rate_type == campaign.rate_type assert result.cost_per_action == campaign.cost_per_action assert result.cost_per_action_usd == campaign.cost_per_action_usd assert result.frequency == campaign.frequency @pytest.mark.db def test_find_by_account_ids_order_by_name( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, fake: FakerTyped, ) -> None: account_id_1 = fake.uuid4_string() account_id_2 = fake.uuid4_string() account_id_3 = fake.uuid4_string() campaign_1 = create_reporting_model( AdReportingCampaignDbt, account_id=account_id_1, name="campaign_1", ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, account_id=account_id_2, name="campaign_2", ) create_reporting_model( AdReportingCampaignDbt, account_id=account_id_3, name="campaign_3", ) result = repository.find_by_criteria( AdReportingCampaignCriteria( account_ids={account_id_1, account_id_2, account_id_3}, ), order_by=["name.asc"], limit=2, offset=0, ) assert len(result) == 2 assert result[0].id == campaign_1.id assert result[1].id == campaign_2.id @pytest.mark.db def test_find_by_report_id_and_countries( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, ) -> None: campaign_1 = create_reporting_model( AdReportingCampaignDbt, name="campaign_1", targeting_countries=["US"], ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, name="campaign_2", targeting_countries=["US"], ) report = create_reporting_model( AdReportingReport, campaign_ids=[campaign_1.id], ) result = repository.find_by_criteria( AdReportingCampaignCriteria(report_id=report.id, countries=["US"]), order_by=["name.asc"], limit=10, offset=0, ) assert len(result) == 2 assert result[0].id == campaign_1.id assert result[0].is_included_in_report assert result[1].id == campaign_2.id assert not result[1].is_included_in_report @pytest.mark.db def test_find_by_search_order_by_name( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, fake: FakerTyped, ) -> None: account_id_1 = fake.uuid4_string() account_id_2 = fake.uuid4_string() create_reporting_model( AdReportingCampaignDbt, account_id=account_id_1, name="campaign_1", ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, account_id=account_id_2, name="campaign_2 match", ) result = repository.find_by_criteria( AdReportingCampaignCriteria( account_ids={account_id_1, account_id_2}, search="match", ), order_by=["name.asc"], limit=10, offset=0, ) assert len(result) == 1 assert result[0].id == campaign_2.id @pytest.mark.db def test_find_by_account_ids_order_by_rate( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, fake: FakerTyped, ) -> None: account_id_1 = fake.uuid4_string() account_id_2 = fake.uuid4_string() campaign_1 = create_reporting_model( AdReportingCampaignDbt, account_id=account_id_1, objective=AdReportingObjective.OUTCOME_TRAFFIC, rate=decimal.Decimal("0.2"), ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, account_id=account_id_2, objective=AdReportingObjective.OUTCOME_TRAFFIC, rate=decimal.Decimal("0.4"), ) result = repository.find_by_criteria( AdReportingCampaignCriteria( account_ids={account_id_1, account_id_2}, ), order_by=["rate.desc"], limit=10, offset=0, ) assert len(result) == 2 assert result[0].rate == campaign_2.rate assert result[1].rate == campaign_1.rate @pytest.mark.db def test_summary_by_criteria( self, repository: AdReportingCampaignRepository, fake: FakerTyped, ) -> None: summary = repository.summary_by_criteria( AdReportingCampaignCriteria( search="test", platform=AdReportingPlatform.META, objective=AdReportingObjective.OUTCOME_TRAFFIC, global_participant_id=fake.uuid4_string(), campaign_ids={fake.uuid4_string(), fake.uuid4_string()}, account_ids={fake.uuid4_string(), fake.uuid4_string()}, ), ) assert summary.total == 0 @pytest.mark.db def test_summary_by_search( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, fake: FakerTyped, ) -> None: account_id_1 = fake.uuid4_string() account_id_2 = fake.uuid4_string() create_reporting_model( AdReportingCampaignDbt, account_id=account_id_1, name="campaign_1" ) create_reporting_model( AdReportingCampaignDbt, account_id=account_id_2, name="campaign_2 match" ) summary = repository.summary_by_criteria( AdReportingCampaignCriteria( account_ids={account_id_1, account_id_2}, search="match", ), ) assert summary.total == 1 @pytest.mark.db def test_summary_by_objective_and_platform( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, fake: FakerTyped, ) -> None: account_id_1 = fake.uuid4_string() account_id_2 = fake.uuid4_string() account_id_3 = fake.uuid4_string() account_id_4 = fake.uuid4_string() objective = AdReportingObjective.OUTCOME_SALES platform = AdReportingPlatform.META create_reporting_model( AdReportingCampaignDbt, account_id=account_id_1, objective=AdReportingObjective.OUTCOME_AWARENESS, ) create_reporting_model( AdReportingCampaignDbt, account_id=account_id_2, objective=AdReportingObjective.OUTCOME_AWARENESS, ) create_reporting_model( AdReportingCampaignDbt, account_id=account_id_3, objective=AdReportingObjective.OUTCOME_TRAFFIC, ) create_reporting_model( AdReportingCampaignDbt, account_id=account_id_4, objective=objective, platform=platform, ) summary = repository.summary_by_criteria( AdReportingCampaignCriteria( account_ids={account_id_1, account_id_2, account_id_3, account_id_4}, platform=platform, objective=objective, ), ) assert summary.total == 1 @pytest.mark.db def test_find_by_ids_group_by_platform_and_objective( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, ) -> None: campaign = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, objective=AdReportingObjective.OUTCOME_AWARENESS, action_type=AdReportingActionType.EAR, rate_type=AdReportingRateType.EARR, ) result = repository.find_by_ids_group_by_platform_and_objective( campaign_ids={ campaign.id, }, ) assert len(result) == 1 assert result[0].platform == campaign.platform assert result[0].objective == campaign.objective assert result[0].campaigns_count == 1 assert result[0].start_at == campaign.start_at assert result[0].end_at == campaign.end_at assert result[0].clicks == campaign.clicks assert result[0].impressions == campaign.impressions assert result[0].reach == campaign.reach assert result[0].estimated_ad_recallers == campaign.estimated_ad_recallers assert result[0].views_p25 == campaign.views_p25 assert result[0].views == campaign.views assert result[0].purchases == campaign.purchases assert result[0].follows == campaign.follows assert result[0].spend_usd == campaign.spend_usd assert result[0].actions == campaign.actions assert result[0].action_type == campaign.action_type assert campaign.reach assert result[0].rate == IsNumber( approx=decimal.Decimal( (campaign.estimated_ad_recallers or 0) / campaign.reach ) ) assert result[0].rate_type == campaign.rate_type assert result[0].cost_per_action_usd == IsNumber( approx=decimal.Decimal(campaign.spend_usd / campaign.actions) ) assert result[0].frequency == IsNumber( approx=decimal.Decimal(campaign.impressions / campaign.reach) ) @pytest.mark.db def test_find_by_ids_group_by_platform_and_objective_multiple( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, ) -> None: dt_now = timezone.now() campaign_1 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, objective=AdReportingObjective.OUTCOME_TRAFFIC, action_type=AdReportingActionType.CLICKS, rate_type=AdReportingRateType.CTR, start_at=dt_now, end_at=dt_now + datetime.timedelta(days=1), ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, objective=AdReportingObjective.OUTCOME_AWARENESS, action_type=AdReportingActionType.EAR, rate_type=AdReportingRateType.EARR, start_at=dt_now + datetime.timedelta(days=5), end_at=dt_now + datetime.timedelta(days=10), ) campaign_3 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, objective=AdReportingObjective.OUTCOME_TRAFFIC, action_type=AdReportingActionType.CLICKS, rate_type=AdReportingRateType.CTR, start_at=dt_now + datetime.timedelta(days=10), end_at=dt_now + datetime.timedelta(days=15), ) campaign_4 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, objective=AdReportingObjective.OUTCOME_AWARENESS, action_type=AdReportingActionType.EAR, rate_type=AdReportingRateType.EARR, start_at=dt_now + datetime.timedelta(days=15), end_at=dt_now + datetime.timedelta(days=20), ) result = repository.find_by_ids_group_by_platform_and_objective( campaign_ids={ campaign_1.id, campaign_2.id, campaign_3.id, campaign_4.id, }, ) assert len(result) == 2 assert result[0].platform == AdReportingPlatform.META assert result[0].objective == AdReportingObjective.OUTCOME_AWARENESS assert result[0].campaigns_count == 2 assert result[0].start_at == campaign_2.start_at assert result[0].end_at == campaign_4.end_at assert result[0].clicks == campaign_2.clicks + campaign_4.clicks assert result[0].impressions == campaign_2.impressions + campaign_4.impressions assert campaign_2.reach and campaign_4.reach assert result[0].reach == campaign_2.reach + campaign_4.reach assert result[0].estimated_ad_recallers == ( (campaign_2.estimated_ad_recallers or 0) + (campaign_4.estimated_ad_recallers or 0) ) assert result[0].views_p25 == campaign_2.views_p25 + campaign_4.views_p25 assert result[0].views == campaign_2.views + campaign_4.views assert result[0].purchases == (campaign_2.purchases or 0) + ( campaign_4.purchases or 0 ) assert result[0].follows == (campaign_2.follows or 0) + ( campaign_4.follows or 0 ) assert result[0].spend_usd == campaign_2.spend_usd + campaign_4.spend_usd assert result[0].actions == campaign_2.actions + campaign_4.actions assert result[0].action_type == campaign_2.action_type assert result[0].rate == IsNumber( approx=decimal.Decimal( ( (campaign_2.estimated_ad_recallers or 0) + (campaign_4.estimated_ad_recallers or 0) ) / (campaign_2.reach + campaign_4.reach) ), ) assert result[0].rate_type == campaign_2.rate_type assert result[0].cost_per_action_usd == IsNumber( approx=decimal.Decimal( (campaign_2.spend_usd + campaign_4.spend_usd) / (campaign_2.actions + campaign_4.actions) ), ) assert result[0].frequency == IsNumber( approx=decimal.Decimal( (campaign_2.impressions + campaign_4.impressions) / (campaign_2.reach + campaign_4.reach) ), ) @pytest.mark.db @freeze_time("2025-01-01") def test_get_by_id_and_account_ids( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, faker: Faker, ) -> None: campaign_id = faker.uuid4() campaign_1 = create_reporting_model( AdReportingCampaignDbt, id=campaign_id, source_schema="schema_1", synced_at=timezone.now(), ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, id=campaign_id, source_schema="schema_2", synced_at=timezone.now() + datetime.timedelta(seconds=10), ) result = repository.get_by_id_and_account_ids( campaign_id, account_ids={campaign_1.account_id, campaign_2.account_id}, ) assert result == campaign_2 @pytest.mark.db def test_find_unique_global_participant_ids_by_identity_id( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, identity_id: str, fake: FakerTyped, ) -> None: global_participant_id_1 = fake.uuid4_string() global_participant_id_2 = fake.uuid4_string() global_participant_id_3 = fake.uuid4_string() campaign_1 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, _global_participant_ids=[global_participant_id_1], ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, _global_participant_ids=[global_participant_id_2], ) campaign_3 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, global_participant_id=global_participant_id_3, ) create_reporting_model( AdReportingReport, identity_id=identity_id, campaign_ids={campaign_1.id} ) create_reporting_model( AdReportingReport, identity_id=identity_id, campaign_ids={campaign_2.id} ) create_reporting_model( AdReportingReport, identity_id=identity_id, campaign_ids={campaign_3.id} ) result = repository.find_unique_global_participant_ids_by_identity_id( identity_id=identity_id, ) assert result == IsList( global_participant_id_1, global_participant_id_2, global_participant_id_3, check_order=False, ) @pytest.mark.db def test_find_unique_global_participant_ids_by_identity_id_report_artists( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, identity_id: str, fake: FakerTyped, ) -> None: global_participant_id_1 = fake.uuid4_string() global_participant_id_2 = fake.uuid4_string() campaign_1 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, _global_participant_ids=[global_participant_id_1], ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, global_participant_id=global_participant_id_2, ) report = create_reporting_model( AdReportingReport, identity_id=identity_id, campaign_ids=[campaign_1.id], ) create_reporting_model( AdReportingReport, identity_id=identity_id, campaign_ids=[campaign_2.id], ) result = repository.find_unique_global_participant_ids_by_identity_id( identity_id=identity_id, report_id=report.id, ) assert result == [global_participant_id_1] @pytest.mark.db def test_find_unique_global_participant_ids_by_identity_id_empty_reports( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, identity_id: str, fake: FakerTyped, ) -> None: global_participant_id = fake.uuid4_string() create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, _global_participant_ids=[global_participant_id], ) result = repository.find_unique_global_participant_ids_by_identity_id( identity_id=identity_id, report_id=None, ) assert result == [] @pytest.mark.db def test_find_unique_global_participant_ids_by_account_ids( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, faker: Faker, ) -> None: global_participant_id_1 = faker.uuid4() global_participant_id_2 = faker.uuid4() global_participant_id_3 = faker.uuid4() global_participant_id_4 = faker.uuid4() ad_account_id_1 = faker.pystr() ad_account_id_2 = faker.pystr() ad_account_id_3 = faker.pystr() create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id_1, platform=AdReportingPlatform.META, _global_participant_ids=[global_participant_id_1], ) create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id_2, platform=AdReportingPlatform.META, _global_participant_ids=[global_participant_id_2, global_participant_id_4], ) create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id_3, platform=AdReportingPlatform.META, global_participant_id=global_participant_id_3, ) result = repository.find_unique_global_participant_ids_by_account_ids( account_ids={ ad_account_id_1, ad_account_id_2, ad_account_id_3, } ) assert result == IsList( global_participant_id_1, global_participant_id_2, global_participant_id_3, global_participant_id_4, check_order=False, ) @pytest.mark.db def test_get_allowed_campaigns_artists_no_allowed_accounts( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, faker: Faker, ) -> None: global_participant_id = faker.uuid4() create_reporting_model( AdReportingCampaignDbt, platform=AdReportingPlatform.META, global_participant_id=global_participant_id, ) result = repository.find_unique_global_participant_ids_by_account_ids( account_ids={faker.pystr()}, ) assert result == [] @pytest.mark.db def test_find_unique_campaigns_objectives_by_criteria( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, faker: Faker, ) -> None: ad_account_id = faker.pystr() platform_1 = AdReportingPlatform.GOOGLE platform_2 = AdReportingPlatform.META campaign_1 = create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id, platform=platform_2, objective=AdReportingObjective.SEARCH, ) campaign_2 = create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id, platform=AdReportingPlatform.TIKTOK, objective=AdReportingObjective.APP_INSTALLS, ) campaign_3 = create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id, platform=platform_2, objective=AdReportingObjective.APP_INSTALLS, ) campaign_4 = create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id, platform=platform_1, objective=faker.uuid4(), ) campaign_5 = create_reporting_model( AdReportingCampaignDbt, account_id=ad_account_id, platform=platform_1, objective=faker.uuid4(), ) report = create_reporting_model( AdReportingReport, campaign_ids=[ campaign_1.id, campaign_2.id, campaign_3.id, campaign_4.id, campaign_5.id, ], ) result = repository.find_unique_campaigns_objectives_by_criteria( criteria=AdReportingCampaignObjectiveCriteria( account_ids={ad_account_id}, report_id=report.id ) ) assert result assert result == [ AdReportingCampaignObjective( platform=platform_1, objective=AdReportingObjective.OTHER ), AdReportingCampaignObjective( platform=platform_2, objective=AdReportingObjective.APP_INSTALLS ), AdReportingCampaignObjective( platform=platform_2, objective=AdReportingObjective.SEARCH ), AdReportingCampaignObjective( platform=AdReportingPlatform.TIKTOK, objective=AdReportingObjective.APP_INSTALLS, ), ] @pytest.mark.db def test_filter_campaigns_by_global_participant_id( self, repository: AdReportingCampaignRepository, create_reporting_model: CreateReportingModel, reporting_db: ReportingDB, ) -> None: campaign_1 = create_reporting_model(AdReportingCampaignDbt) campaign_1.global_participant_ids = {"gp1", "gp2"} campaign_2 = create_reporting_model(AdReportingCampaignDbt) campaign_2.global_participant_ids = {"gp3", "gp4"} reporting_db.session.commit() campaigns = repository.find_by_criteria( AdReportingCampaignCriteria(global_participant_id="gp1") ) assert len(campaigns) == 1 assert campaigns[0].id == campaign_1.id campaigns = repository.find_by_criteria( AdReportingCampaignCriteria(global_participant_id="gp2") ) assert len(campaigns) == 1 assert campaigns[0].id == campaign_1.id campaigns = repository.find_by_criteria( AdReportingCampaignCriteria(global_participant_id="gp3") ) assert len(campaigns) == 1 assert campaigns[0].id == campaign_2.id campaigns = repository.find_by_criteria( AdReportingCampaignCriteria(global_participant_id="gp4") ) assert len(campaigns) == 1 assert campaigns[0].id == campaign_2.id