from anydi import singleton from fansifter_common.artist.validators import ArtistValidator from fansifter_common.auth.account import Account from ows_text_campaigns.adapters.ows_url_shortener import OwsUrlShortenerClient from ows_text_campaigns.artist.repositories import ArtistSettingsRepository from ows_text_campaigns.audiences.exceptions import ( ArchivedAudienceError, EmptyAudienceError, InvalidAudienceArtistError, InvalidAudienceIdError, NotSupportedAudienceTargetError, ) from ows_text_campaigns.audiences.models import Audience from ows_text_campaigns.audiences.repositories import AudienceRepository from ows_text_campaigns.campaigns.enums import ShorteningMethod from ows_text_campaigns.campaigns.exceptions import ( CampaignUniqueNameError, ChannelNotAllowedInCountryError, InvalidUrlDomainIdError, MalformedCampaignError, ShorteningPathIsNotAvailableError, ) from ows_text_campaigns.campaigns.models import Campaign, UrlDomain from ows_text_campaigns.campaigns.repositories import CampaignRepository from ows_text_campaigns.campaigns.services import UrlDomainService from ows_text_campaigns.campaigns.types import UrlPathLike from ows_text_campaigns.config import settings from ows_text_campaigns.rosters.exceptions import MainRepArtistOnlyAllowedError from ows_text_campaigns.rosters.repositories import ArtistRosterMainRepRepository @singleton class ShortenedUrlValidator: def __init__( self, url_domain_service: UrlDomainService, ows_url_shortener_client: OwsUrlShortenerClient, ) -> None: self.url_domain_service = url_domain_service self.ows_url_shortener_client = ows_url_shortener_client def validate_availability( self, method: ShorteningMethod, path: UrlPathLike ) -> None: if method != ShorteningMethod.STANDARD: return is_available_for_shortening = self.ows_url_shortener_client.check_path(path) if not is_available_for_shortening: raise ShorteningPathIsNotAvailableError def validate_allowed_domain( self, *, identity_id: str, account: Account, domain: str ) -> UrlDomain: allowed_domains = self.url_domain_service.get_allowed_domains( identity_id=identity_id, account=account ) for allowed_domain in allowed_domains: if allowed_domain.domain == domain: return allowed_domain raise InvalidUrlDomainIdError( "Domain ID is not valid or not allowed for the account." ) @singleton class CampaignValidator: def __init__( self, campaign_repository: CampaignRepository, artist_validator: ArtistValidator, artist_settings_repository: ArtistSettingsRepository, audience_repository: AudienceRepository, artist_roster_main_rep_repository: ArtistRosterMainRepRepository, ) -> None: self.campaign_repository = campaign_repository self.artist_validator = artist_validator self.artist_settings_repository = artist_settings_repository self.audience_repository = audience_repository self.artist_roster_main_rep_repository = artist_roster_main_rep_repository def validate_name( self, name: str, *, account: Account, campaign: Campaign | None = None, ) -> None: """Validate audience name under account.""" if self.campaign_repository.exists_by_name_and_account( name=name, account=account, exclude=[campaign.id] if campaign else None, ): raise CampaignUniqueNameError def validate_audience_id( self, audience_id: str, account: Account, *, global_participant_id: str | None = None, ) -> Audience: """Validate audience id under account.""" audience: Audience | None = self.audience_repository.get(audience_id) # Audience does not exist if audience is None: raise InvalidAudienceIdError if audience.account != account: raise MalformedCampaignError("Audience does not belong to the account") # Validate audience self.validate_audience( audience, global_participant_id=global_participant_id, ) return audience def validate_channel_country(self, campaign: Campaign) -> None: """Reject a campaign whose channel may not target one of its countries.""" blocked_countries = settings.channel_country_blocklist.get( campaign.channel.as_literal() ) if not blocked_countries or campaign.audience_id is None: return fan_counts = self.campaign_repository.get_fan_count_by_country_channel( campaign.audience_id ) blocked = sorted( { fan.country for fan in fan_counts if fan.channel == campaign.channel and fan.count > 0 and fan.country in blocked_countries } ) if blocked: raise ChannelNotAllowedInCountryError( channel=campaign.channel.as_literal(), countries=blocked ) @staticmethod def validate_audience( audience: Audience, *, global_participant_id: str | None = None ) -> Audience: """Validate that audience is compatible for a campaign.""" # Check if audience is archived if audience.is_archived: raise ArchivedAudienceError # Check if audience is text audience if not audience.is_text: raise NotSupportedAudienceTargetError("Only text audiences are supported.") # Check if global participant ID is valid if ( global_participant_id and global_participant_id not in audience.global_participant_ids or not audience.global_participant_ids ): raise InvalidAudienceArtistError( "Audience does not contain the specified global participant ID." ) # Ensure that the audience is not empty if audience.fan_count < 1: raise EmptyAudienceError("Audience must have at least one fan.") return audience def validate_main_rep_only_artist( self, global_participant_id: str, account: Account ) -> None: result = self.artist_roster_main_rep_repository.exists_by_account_and_global_participant_id( vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, ) if not result: raise MainRepArtistOnlyAllowedError( global_participant_id=global_participant_id, )