import datetime from collections import defaultdict from dataclasses import dataclass from typing import ClassVar import humps from campaigns.auth.dtos import User from campaigns.connectors.ows_account import OwsAccount from campaigns.connectors.ows_product import OwsProduct, Product from campaigns.core.exceptions import FieldError from campaigns.meta import constants from campaigns.meta.dtos import CampaignPost, CampaignValidationSummary from campaigns.meta.enums import CampaignContentType, CampaignLinkType from campaigns.meta.errors import MetaErrors from campaigns.meta.exceptions import ( CampaignPostDoesNotExist, CampaignValidationError, FacebookPageDoesNotExist, FacebookPageInvalidToken, ) from campaigns.meta.models import Campaign, FacebookPage from campaigns.meta.services import CampaignPostService from campaigns.meta.services.facebook_page import FacebookPageService @dataclass class CampaignValidator: ows_account: OwsAccount ows_product: OwsProduct facebook_page_service: FacebookPageService campaign_post_service: CampaignPostService required_fields: ClassVar[tuple[str, ...]] = ( "name", "vendor_id", "global_participant_id", "facebook_page_id", "objective", "start_time", "end_time", "lifetime_budget", "content_type", "platform", "primary_text", "targeting_audience", # targeting_audience is not needed if any other audiences are available ) async def validate( self, campaign: Campaign, *, user: User, with_external: bool = False ) -> None: self.validate_required(campaign) errors = [] try: self.validate_schedule(campaign) except CampaignValidationError as exc: errors.extend(exc.errors) try: await self.validate_lifetime_budget(campaign) except CampaignValidationError as exc: errors.extend(exc.errors) try: await self.validate_content_type(campaign) except CampaignValidationError as exc: errors.extend(exc.errors) try: self.validate_audiences(campaign) except CampaignValidationError as exc: errors.extend(exc.errors) try: self.validate_link_type(campaign) except CampaignValidationError as exc: errors.extend(exc.errors) if with_external: try: await self.validate_external(campaign, user=user) except CampaignValidationError as exc: errors.extend(exc.errors) if errors: raise CampaignValidationError(errors) async def validate_external(self, campaign: Campaign, *, user: User) -> None: errors = [] try: await self.validate_account(campaign, user=user) except CampaignValidationError as exc: errors.extend(exc.errors) try: await self.validate_product_id(campaign, user=user) except CampaignValidationError as exc: errors.extend(exc.errors) try: await self.validate_facebook_page_id(campaign, user=user) except CampaignValidationError as exc: errors.extend(exc.errors) if errors: raise CampaignValidationError(errors) def validate_required(self, campaign: Campaign) -> None: errors = [] for field in self.required_fields: value = getattr(campaign, field, None) if not value: errors.append(FieldError.required(field)) if campaign.content_type == CampaignContentType.IMAGE and not campaign.images: errors.append(FieldError.required("images")) elif campaign.content_type == CampaignContentType.VIDEO and not campaign.videos: errors.append(FieldError.required("videos")) elif ( campaign.content_type == CampaignContentType.POST and not campaign.post_external_id ): errors.append(FieldError.required("post_external_id")) if errors: raise CampaignValidationError(errors=errors) @staticmethod def validate_link_type(campaign: Campaign) -> None: if campaign.content_type == CampaignContentType.POST: return None if campaign.link_type == CampaignLinkType.WEBSITE and not campaign.website_url: raise CampaignValidationError( errors=[ FieldError.required("website_url"), ] ) elif ( campaign.link_type == CampaignLinkType.LANDING_PAGE and not campaign.landing_page_path ): raise CampaignValidationError( errors=[ FieldError.required("landing_page_path"), ] ) async def validate_product_id( self, campaign: Campaign, *, user: User, field: str = "product_id" ) -> Product | None: if not campaign.product_id: return None product = await self.ows_product.get_product_by_id( product_id=campaign.product_id, ) if product is None or ( product and not user.account_access.allowed_for(product) ): raise CampaignValidationError( errors=[ MetaErrors.campaign_not_allowed_product.field_error(field), ] ) return product async def validate_account( self, campaign: Campaign, *, user: User, field: str = "vendor_id" ) -> None: if ( campaign.subaccount_id and not await self.ows_account.validate_subaccount( vendor_id=campaign.vendor_id, subaccount_id=campaign.subaccount_id ) ) or not user.account_access.allowed_for(campaign): raise CampaignValidationError( errors=[ MetaErrors.campaign_not_allowed_label.field_error(field), ] ) async def validate_facebook_page_id( self, campaign: Campaign, *, user: User, field: str = "facebook_page_id" ) -> FacebookPage: try: return await self.facebook_page_service.get_user_valid_page_for_account( page_id=campaign.facebook_page_id, account=campaign.account, user=user, ) except FacebookPageDoesNotExist as exc: raise CampaignValidationError( errors=[ MetaErrors.facebook_page_invalid_id.field_error(field), ] ) from exc except FacebookPageInvalidToken as exc: raise CampaignValidationError( errors=[ MetaErrors.facebook_page_invalid_token.field_error(field), ] ) from exc @staticmethod def validate_start_time( campaign: Campaign, *, field: str = "start_time" ) -> datetime.datetime: if not campaign.start_time: raise CampaignValidationError( errors=[ MetaErrors.campaign_start_time_required.field_error(field), ] ) return campaign.start_time @staticmethod def validate_end_time( campaign: Campaign, *, field: str = "end_time" ) -> datetime.datetime: if not campaign.end_time: raise CampaignValidationError( errors=[ MetaErrors.campaign_end_time_required.field_error(field), ] ) return campaign.end_time def validate_schedule(self, campaign: Campaign, *, field: str = "end_time") -> None: start_time = self.validate_start_time(campaign) end_time = self.validate_end_time(campaign) if start_time >= end_time: raise CampaignValidationError( errors=[ MetaErrors.campaign_invalid_schedule.field_error(field), ] ) async def validate_lifetime_budget( self, campaign: Campaign, *, field: str = "lifetime_budget" ) -> None: if not campaign.lifetime_budget: raise CampaignValidationError( errors=[MetaErrors.campaign_invalid_lifetime_budget.field_error(field)] ) async def validate_content_type(self, campaign: Campaign) -> None: if campaign.content_type == CampaignContentType.POST: await self.validate_post_external_id(campaign) elif campaign.content_type == CampaignContentType.IMAGE: self.validate_images(campaign) elif campaign.content_type == CampaignContentType.VIDEO: self.validate_videos(campaign) @staticmethod def validate_images(campaign: Campaign, *, field: str = "images") -> None: if not campaign.has_valid_images: raise CampaignValidationError( errors=[ MetaErrors.campaign_no_valid_images.field_error(field), ] ) @staticmethod def validate_videos(campaign: Campaign, *, field: str = "videos") -> None: if not campaign.has_valid_videos: raise CampaignValidationError( errors=[ MetaErrors.campaign_no_valid_videos.field_error(field), ] ) async def validate_post_external_id( self, campaign: Campaign, *, post_external_id: str | None = None, field: str = "post_external_id", ) -> CampaignPost: post_external_id = post_external_id or campaign.post_external_id if not post_external_id: raise CampaignValidationError( errors=[ MetaErrors.campaign_post_external_id_required.field_error(field), ] ) try: return await self.campaign_post_service.get_campaign_post( campaign, post_external_id=post_external_id ) except CampaignPostDoesNotExist as exc: raise CampaignValidationError( errors=[ MetaErrors.campaign_invalid_post_external_id.field_error(field), ] ) from exc @staticmethod def validate_audiences(campaign: Campaign, *, field: str = "audiences") -> None: if not campaign.targeting_audience: raise CampaignValidationError( errors=[ MetaErrors.campaign_no_audiences.field_error(field), ] ) async def get_summary( self, campaign: Campaign, *, user: User, camelize: bool = False ) -> list[CampaignValidationSummary]: if not campaign.is_draft: return [] errors_by_summary_id = defaultdict(list) try: await self.validate(campaign, user=user, with_external=False) except CampaignValidationError as exc: for error in exc.errors: summary_id = constants.CAMPAIGN_FIELD_BY_SUMMARY_ID.get(error.field) if not summary_id: continue errors_by_summary_id[summary_id].append( FieldError( field=humps.camelize(error.field) if camelize else error.field, code=error.code, message=error.message, ) ) return [ CampaignValidationSummary(id=summary_id, errors=errors) for summary_id, errors in errors_by_summary_id.items() ]