from collections import defaultdict from collections.abc import Sequence from typing import TypeVar from anydi import singleton from fansifter_common.utils.text import is_empty from dmp.ad_accounts.enums import AdAccountPlatform from dmp.ad_accounts.models import AdAccountDbt from dmp.ad_accounts.repositories import AdAccountDbtRepository from dmp.adapters.google import GoogleClient, GoogleClientError from dmp.adapters.google.enums import GoogleAdAccountUserAccessRole from dmp.adapters.google.models import ( CampaignsResponse, NestedAdAccount, Summary, UserAdAccount, ) from dmp.core.types import PlainToken from dmp.google.dtos import BusinessCenterAdAccount, GoogleUserAdAccountLabel from dmp.google.exceptions import GoogleAdAccountNotFoundError from dmp.google.models import ( GoogleAdAccount, GoogleAdReportingConnection, GoogleUserConnection, ) from dmp.google.repositories import GoogleAdAccountRepository BusinessCenterAdAccountT = TypeVar( "BusinessCenterAdAccountT", bound=BusinessCenterAdAccount ) @singleton class GoogleAdAccountService: def __init__( self, ad_account_repository: GoogleAdAccountRepository, ad_account_dbt_repository: AdAccountDbtRepository, google_client: GoogleClient, ) -> None: self.ad_account_repository = ad_account_repository self.ad_account_dbt_repository = ad_account_dbt_repository self.google_client = google_client def get_nested_api_ad_accounts_for_account( self, access_token: PlainToken, ad_account_id: int ) -> list[NestedAdAccount]: try: response = self.google_client.get_nested_ad_account_details( access_token=access_token, ad_account_id=ad_account_id ) return response.results or [] except GoogleClientError: return [] def get_api_ad_accounts_for_connection( self, access_token: PlainToken ) -> list[UserAdAccount]: try: response = self.google_client.get_ad_account_resources( access_token=access_token ) resources = response.get_resources() except GoogleClientError as e: if e.is_not_ads_user_error: return [] raise e ad_account_ids = [resource.ad_account_id for resource in resources] ad_accounts: dict[str, UserAdAccount] = {} final_ad_accounts: dict[str, UserAdAccount] = {} for ad_account_id in ad_account_ids: try: api_ad_account_response = self.google_client.get_ad_account_details( access_token=access_token, ad_account_id=ad_account_id, parent_ad_account_id=None, ) except GoogleClientError: continue is_eligible = False api_ad_account = None if api_ad_account_response.results: for result in api_ad_account_response.results: if is_eligible := result.is_eligible: api_ad_account = result break if not is_eligible or not api_ad_account: continue campaigns_response = self.google_client.get_campaign_ids( access_token=access_token, ad_account_id=ad_account_id, login_customer_id=None, ) nested_ad_accounts = self._get_nested_ad_accounts( access_token=access_token, source_ad_account_id=int(api_ad_account.customer.id), access_role=api_ad_account.customer_user_access.access_role, ) final_ad_accounts.update(nested_ad_accounts) ad_accounts[api_ad_account.customer.id] = UserAdAccount( id=api_ad_account.customer.id, name=api_ad_account.customer.descriptive_name or "", user_role=api_ad_account.customer_user_access.access_role, campaigns=CampaignsResponse( summary=Summary(total_count=campaigns_response.total_results_count), ), ) final_ad_accounts.update(ad_accounts) return [value for key, value in final_ad_accounts.items()] def _get_nested_ad_accounts( self, access_token: PlainToken, source_ad_account_id: int, access_role: GoogleAdAccountUserAccessRole, ) -> dict[str, UserAdAccount]: nested_api_ad_accounts = self.get_nested_api_ad_accounts_for_account( access_token=access_token, ad_account_id=source_ad_account_id ) nested_ad_accounts: dict[str, UserAdAccount] = {} for nested_ad_account in nested_api_ad_accounts: if nested_ad_account.is_eligible: nested_campaigns_response = self.google_client.get_campaign_ids( access_token=access_token, ad_account_id=int(nested_ad_account.customer_client.id), login_customer_id=source_ad_account_id, ) nested_ad_accounts[nested_ad_account.customer_client.id] = ( UserAdAccount( id=nested_ad_account.customer_client.id, name=nested_ad_account.customer_client.descriptive_name or "", user_role=access_role, campaigns=CampaignsResponse( summary=Summary( total_count=nested_campaigns_response.total_results_count ), ), login_customer_id=str(source_ad_account_id), ) ) return nested_ad_accounts def _get_ad_accounts_for_connection( self, connection: GoogleUserConnection | GoogleAdReportingConnection, new_ad_accounts_dict: dict[str, UserAdAccount] | dict[str, AdAccountDbt] | dict[str, GoogleAdAccount], ) -> list[GoogleAdAccount]: new_ad_account_ids = set(new_ad_accounts_dict.keys()) if isinstance(connection, GoogleUserConnection): existing_connection_ad_accounts_dict: dict[str, GoogleAdAccount] = { connection_ad_account.ad_account.external_id: connection_ad_account.ad_account for connection_ad_account in connection.connection_ad_accounts } else: existing_connection_ad_accounts_dict = { ad_account.external_id: ad_account for ad_account in connection.ad_accounts } existing_connection_ad_account_ids = set( existing_connection_ad_accounts_dict.keys() ) # Getting matching ad_accounts previously stored in google_ad_account table existing_global_ad_account_dict: dict[str, GoogleAdAccount] = { ad_account.external_id: ad_account for ad_account in self.ad_account_repository.find_by_external_ids( external_ids=new_ad_account_ids - existing_connection_ad_account_ids ) } existing_global_ad_account_ids = set(existing_global_ad_account_dict.keys()) ad_accounts_to_add: list[GoogleAdAccount] = [] for ad_account_id in ( new_ad_account_ids - existing_connection_ad_account_ids - existing_global_ad_account_ids ): ad_account = new_ad_accounts_dict[ad_account_id] if isinstance(ad_account, GoogleAdAccount): ad_accounts_to_add.append(ad_account) else: ad_accounts_to_add.append( GoogleAdAccount( external_id=new_ad_accounts_dict[ad_account_id].id, name=new_ad_accounts_dict[ad_account_id].name, campaigns_count=new_ad_accounts_dict[ ad_account_id ].campaigns_count, ) ) all_related_existing_ad_accounts_dict: dict[str, GoogleAdAccount] = ( existing_connection_ad_accounts_dict | existing_global_ad_account_dict ) all_related_existing_ad_accounts_ids = set( all_related_existing_ad_accounts_dict.keys() ) ad_accounts_to_update = [] for ad_account_id in (all_related_existing_ad_accounts_ids).intersection( new_ad_account_ids ): existing_ad_account = all_related_existing_ad_accounts_dict[ad_account_id] new_ad_account = new_ad_accounts_dict[ad_account_id] existing_ad_account.name = new_ad_account.name existing_ad_account.campaigns_count = new_ad_account.campaigns_count ad_accounts_to_update.append(existing_ad_account) return ad_accounts_to_update + ad_accounts_to_add def get_ad_accounts_for_user_connection( self, connection: GoogleUserConnection, external_ad_accounts: list[UserAdAccount], ) -> list[GoogleAdAccount]: new_ad_account_ids: set[str] = set() new_ad_accounts_dict: dict[str, UserAdAccount] = {} for api_ad_account in external_ad_accounts: new_ad_account_ids.add(api_ad_account.id) new_ad_accounts_dict[api_ad_account.id] = api_ad_account return self._get_ad_accounts_for_connection( connection=connection, new_ad_accounts_dict=new_ad_accounts_dict, ) def get_ad_accounts_for_ad_reporting_connection( self, connection: GoogleAdReportingConnection, external_ad_accounts: list[str], existing_ad_accounts_dict: dict[str, GoogleAdAccount] | None = None, ) -> list[GoogleAdAccount]: new_ad_accounts_dict: dict[str, AdAccountDbt] = { dbt_ad_account.id: dbt_ad_account for dbt_ad_account in self.ad_account_dbt_repository.find_by_ids_and_platform( ids=external_ad_accounts, platform=AdAccountPlatform.GOOGLE, ) } if not new_ad_accounts_dict and existing_ad_accounts_dict: return self._get_ad_accounts_for_connection( connection=connection, new_ad_accounts_dict=existing_ad_accounts_dict, ) else: return self._get_ad_accounts_for_connection( connection=connection, new_ad_accounts_dict=new_ad_accounts_dict, ) def get_ad_account(self, ad_account_id: str) -> GoogleAdAccount: ad_account = self.ad_account_repository.get(ad_account_id) if ad_account is None: raise GoogleAdAccountNotFoundError return ad_account def get_user_ad_accounts_for_assignment( self, identity_id: str, user_id: str ) -> list[GoogleUserAdAccountLabel]: return self.ad_account_repository.find_by_identity_id_and_user_id_for_assigment( identity_id, user_id ) def fill_ad_account_empty_data( self, ad_accounts: Sequence[BusinessCenterAdAccountT] ) -> Sequence[BusinessCenterAdAccountT]: ad_accounts_dict = defaultdict(list) for ad_account in ad_accounts: ad_accounts_dict[ad_account.external_id].append(ad_account) ad_accounts_dbt = self.ad_account_dbt_repository.find_by_ids_and_platform( list(ad_accounts_dict.keys()), platform=AdAccountPlatform.GOOGLE, ) ad_reporting_account_by_id = { ad_account_dbt.id: ad_account_dbt for ad_account_dbt in ad_accounts_dbt } for external_id, _ad_accounts in ad_accounts_dict.items(): for ad_account in _ad_accounts: ad_reporting_account = ad_reporting_account_by_id.get(external_id) if ad_reporting_account is None: ad_account.name = ad_account.name or ad_account.external_id else: if ( ad_account.name == ad_account.external_id and ad_reporting_account.name ) or is_empty(ad_account.name): ad_account.name = ad_reporting_account.name ad_account.campaigns_count = max( ad_account.campaigns_count, ad_reporting_account.campaigns_count or 0, ) return ad_accounts