import datetime import decimal from dataclasses import dataclass, field from anydi import singleton from fansifter_common.auth.requests import AuthRequest from fansifter_common.auth.services import AuthService from fansifter_common.auth.types import Permission from dmp.ad_reporting.dtos import ( AdReportingCampaignCountryToplinePerformance, AdReportingCampaignCriteria, AdReportingCampaignToplinePerformance, ) from dmp.ad_reporting.enums import AdReportingObjective, AdReportingPlatform from dmp.ad_reporting.models import ( AdReportingCampaignBenchmarkByAccountArtistCountryDbt, AdReportingCampaignBenchmarkByAccountArtistDbt, AdReportingCampaignBenchmarkByAccountCountryDbt, AdReportingCampaignBenchmarkByAccountDbt, AdReportingReport, ) from dmp.ad_reporting.repositories import ( AdReportingCampaignBenchmarkByAccountArtistCountryRepository, AdReportingCampaignBenchmarkByAccountArtistRepository, AdReportingCampaignBenchmarkByAccountCountryRepository, AdReportingCampaignBenchmarkByAccountRepository, AdReportingCampaignCountryRepository, AdReportingCampaignRepository, ) from dmp.ad_reporting.services import ( AdReportingAccountService, AdReportingReportService, ) @dataclass(kw_only=True, frozen=True) class Account: vendor_id: int subaccount_id: int @dataclass(kw_only=True, frozen=True) class AccountArtist: vendor_id: int subaccount_id: int global_participant_id: str @dataclass(kw_only=True) class GetAdReportingReportOverviewRequest(AuthRequest): report_id: str @dataclass(kw_only=True) class GetAdReportingReportOverviewResponse: id: str name: str created_at: datetime.datetime created_by: str campaigns_count: int = field(default=0, init=False) start_at: datetime.datetime | None = field(default=None, init=False) end_at: datetime.datetime | None = field(default=None, init=False) impressions: int = field(default=0, init=False) clicks: int = field(default=0, init=False) views_p25: int = field(default=0, init=False) views: int = field(default=0, init=False) currency: str | None = field(default=None, init=False) spend_usd: decimal.Decimal = field(default=decimal.Decimal(0.0), init=False) likes: int | None = field(default=None, init=False) complete_payment: int | None = field(default=None, init=False) checkout: int | None = field(default=None, init=False) topline_performance: list[AdReportingCampaignToplinePerformance] topline_country_performance: list[AdReportingCampaignCountryToplinePerformance] def __post_init__(self) -> None: currencies = {item.currency for item in self.topline_performance} currency = None if self.topline_performance: if len(currencies) == 1: currency = self.topline_performance[0].currency campaigns_count = 0 start_at_dates = [] end_at_dates = [] impressions = 0 clicks = 0 views_p25 = 0 views = 0 spend = decimal.Decimal(0) spend_usd = decimal.Decimal(0) likes = 0 complete_payment = 0 checkout = 0 for item in self.topline_performance: campaigns_count += item.campaigns_count start_at_dates.append(item.start_at) end_at_dates.append(item.end_at) impressions += item.impressions clicks += item.clicks views_p25 += item.views_p25 views += item.views if currency and (item.spend is not None): spend += item.spend spend_usd += item.spend_usd likes += item.likes or 0 complete_payment += item.complete_payment or 0 checkout += item.checkout or 0 self.campaigns_count = campaigns_count self.start_at = min(start_at_dates) if start_at_dates else None self.end_at = max(end_at_dates) if end_at_dates else None self.impressions = impressions self.clicks = clicks self.views_p25 = views_p25 self.views = views self.currency = currency self.spend = spend self.spend_usd = spend_usd self.likes = likes self.complete_payment = complete_payment self.checkout = checkout @singleton class GetAdReportingReportOverviewHandler: permission = Permission("ad_report", "view") def __init__( self, auth_service: AuthService, campaign_repository: AdReportingCampaignRepository, campaign_country_repository: AdReportingCampaignCountryRepository, report_service: AdReportingReportService, account_service: AdReportingAccountService, campaign_benchmark_by_account_repository: AdReportingCampaignBenchmarkByAccountRepository, campaign_benchmark_by_account_artist_repository: AdReportingCampaignBenchmarkByAccountArtistRepository, campaign_benchmark_by_account_country_repository: AdReportingCampaignBenchmarkByAccountCountryRepository, campaign_benchmark_by_account_artist_country_repository: AdReportingCampaignBenchmarkByAccountArtistCountryRepository, ) -> None: self.auth_service = auth_service self.campaign_repository = campaign_repository self.campaign_country_repository = campaign_country_repository self.report_service = report_service self.account_service = account_service self.campaign_benchmark_by_account_repository = ( campaign_benchmark_by_account_repository ) self.campaign_benchmark_by_account_artist_repository = ( campaign_benchmark_by_account_artist_repository ) self.campaign_benchmark_by_account_country_repository = ( campaign_benchmark_by_account_country_repository ) self.campaign_benchmark_by_account_artist_country_repository = ( campaign_benchmark_by_account_artist_country_repository ) def handle( self, request: GetAdReportingReportOverviewRequest ) -> GetAdReportingReportOverviewResponse: report = self.report_service.get_report( report_id=request.report_id, identity_id=request.identity_id, ) self.auth_service.check_owned_resource( report.identity_id, permission=self.permission, resource_id=report.id, ) account, account_artist = self._get_account_and_account_artist(report) topline_performance = self._get_topline_performance( account=account, account_artist=account_artist, report=report, ) topline_country_performance = self._get_topline_country_performance( account=account, account_artist=account_artist, report=report, ) return GetAdReportingReportOverviewResponse( id=report.id, name=report.name, created_at=report.created_at, created_by=report.created_by, topline_performance=topline_performance, topline_country_performance=topline_country_performance, ) def _get_benchmarks( self, account: Account | None ) -> dict[ tuple[AdReportingPlatform, AdReportingObjective], AdReportingCampaignBenchmarkByAccountDbt, ]: """Get benchmarks for account.""" if account is None: return {} return { (item.platform, item.objective): item for item in ( self.campaign_benchmark_by_account_repository.find_by_account_id( vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) ) } def _get_country_benchmarks( self, account: Account | None ) -> dict[ tuple[AdReportingPlatform, AdReportingObjective, str], AdReportingCampaignBenchmarkByAccountCountryDbt, ]: """Get benchmarks for account.""" if account is None: return {} return { (item.platform, item.objective, item.country_code): item for item in ( self.campaign_benchmark_by_account_country_repository.find_by_account_id( vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) ) } def _get_artist_benchmarks( self, account_artist: AccountArtist | None ) -> dict[ tuple[AdReportingPlatform, AdReportingObjective], AdReportingCampaignBenchmarkByAccountArtistDbt, ]: """Get benchmarks for account artist.""" if account_artist is None: return {} return { (item.platform, item.objective): item for item in ( self.campaign_benchmark_by_account_artist_repository.find_by_account_and_artist_id( vendor_id=account_artist.vendor_id, subaccount_id=account_artist.subaccount_id, global_participant_id=account_artist.global_participant_id, ) ) } def _get_artist_country_benchmarks( self, account_artist: AccountArtist | None ) -> dict[ tuple[AdReportingPlatform, AdReportingObjective, str], AdReportingCampaignBenchmarkByAccountArtistCountryDbt, ]: """Get benchmarks for account artist.""" if account_artist is None: return {} return { (item.platform, item.objective, item.country_code): item for item in ( self.campaign_benchmark_by_account_artist_country_repository.find_by_account_and_artist_id( vendor_id=account_artist.vendor_id, subaccount_id=account_artist.subaccount_id, global_participant_id=account_artist.global_participant_id, ) ) } def _get_account_and_account_artist( self, report: AdReportingReport ) -> tuple[Account | None, AccountArtist | None]: """Get account and account artist for report.""" # Get report campaigns campaigns = self.campaign_repository.find_by_criteria( AdReportingCampaignCriteria(campaign_ids=set(report.campaign_ids)), ) # Get unique account ids account_ids = {campaign.account_id for campaign in campaigns} # Get account labels ad_account_labels = self.account_service.get_ad_account_labels( account_ids=account_ids ) accounts = set() account_artists = set() for campaign in campaigns: campaign_accounts = ad_account_labels.get(campaign.account_id, set()) for account in campaign_accounts: accounts.add( Account( vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) ) for global_participant_id in campaign.global_participant_ids: account_artists.add( AccountArtist( vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, ) ) # Break if we have more than 2 account and account artist if len(accounts) >= 2 and len(account_artists) >= 2: break return ( None if (not accounts) or len(accounts) > 1 else accounts.pop(), None if (not account_artists) or len(accounts) > 1 or len(account_artists) > 1 else account_artists.pop(), ) def _get_topline_performance( self, account: Account | None, account_artist: AccountArtist | None, report: AdReportingReport, ) -> list[AdReportingCampaignToplinePerformance]: # Get benchmarks benchmarks = self._get_benchmarks(account) # Get artist benchmarks artist_benchmarks = self._get_artist_benchmarks(account_artist) topline_performance = ( self.campaign_repository.find_by_ids_group_by_platform_and_objective( campaign_ids=set(report.campaign_ids), ) ) for item in topline_performance: # Set benchmark benchmark = benchmarks.get((item.platform, item.objective)) if benchmark: item.benchmark = benchmark.rate # Set artist benchmark only if all campaign global participant ids # are present and artist benchmark exists. artist_benchmark = artist_benchmarks.get((item.platform, item.objective)) if artist_benchmark and ( len(item.global_participant_ids) == item.campaigns_count and artist_benchmark.global_participant_id in item.global_participant_ids ): item.artist_benchmark = artist_benchmark.rate return topline_performance def _get_topline_country_performance( self, account: Account | None, account_artist: AccountArtist | None, report: AdReportingReport, ) -> list[AdReportingCampaignCountryToplinePerformance]: # Get benchmarks country_benchmarks = self._get_country_benchmarks(account) # Get artist benchmarks artist_country_benchmarks = self._get_artist_country_benchmarks(account_artist) topline_country_performance = self.campaign_country_repository.find_by_ids_group_by_platform_objective_and_country( campaign_ids=set(report.campaign_ids), ) for item in topline_country_performance: # Set benchmark benchmark = country_benchmarks.get( (item.platform, item.objective, item.country_code) ) if benchmark: item.benchmark = benchmark.rate # Set artist benchmark only if all campaign global participant ids # are present and artist benchmark exists. artist_benchmark = artist_country_benchmarks.get( (item.platform, item.objective, item.country_code) ) if artist_benchmark and ( len(item.global_participant_ids) == item.campaigns_count and artist_benchmark.global_participant_id in item.global_participant_ids and artist_benchmark.country_code == item.country_code ): item.artist_benchmark = artist_benchmark.rate return topline_country_performance