import logging from collections.abc import Iterator from dataclasses import dataclass from anydi import singleton from cachetools import LRUCache, cachedmethod from emval import validate_email from fansifter_common.adapters.sendgrid.models import FanData, MergeTags from fansifter_common.encrypter import JWTEncrypter from fansifter_common.identifiers.types import FansifterProfileIdentifier from fansifter_common.identifiers.utils import encrypt_profile_token from fansifter_common.legal_info.services import LegalInfoService from fansifter_common.legal_info.types import LegalEntity from fansifter_common.translation.services import TranslationService from app.config import Settings from app.repositories import BatchRecipientRepository logger = logging.getLogger(__name__) @dataclass(slots=True, frozen=True) class ComplianceFooter: opt_in_info: str opt_in_info_external: str unsubscribe_text: str profile_text: str | None = None @singleton class FanDataService: def __init__( self, recipient_repository: BatchRecipientRepository, legal_info_service: LegalInfoService, translation_service: TranslationService, preference_center_encrypter: JWTEncrypter, settings: Settings, ) -> None: self.recipient_repository = recipient_repository self.legal_info_service = legal_info_service self.translation_service = translation_service self.preference_center_encrypter = preference_center_encrypter self.settings = settings # Cached self._legal_cache: LRUCache[str, LegalEntity] = LRUCache(maxsize=256) self._privacy_cache: LRUCache[str, str] = LRUCache(maxsize=256) self._footer_cache: LRUCache[str, ComplianceFooter] = LRUCache(maxsize=256) def get_fans_data( self, *, campaign_id: str, batch_id: int, limit: int, offset: int ) -> Iterator[FanData]: """Get fans data for the given snapshot id.""" recipients = self.recipient_repository.iter_next_in_batch( batch_id=batch_id, limit=limit, offset=offset ) for recipient in recipients: try: validated_email = validate_email( recipient.fan_email, deliverable_address=False ) except Exception: # noqa logger.warning( "Detected invalid email for audience fan: %s", recipient.fan_email, extra={ "campaign_id": campaign_id, "batch_id": batch_id, }, ) continue # Skip non Fansifter email domains if ( self.settings.send_fansifter_only and validated_email.ascii_domain not in self.settings.fansifter_domains ): continue preference_center_profile_token = encrypt_profile_token( self.preference_center_encrypter, identifier=FansifterProfileIdentifier.model_construct( profile_id=recipient.profile_id, email_campaign_id=campaign_id, ), ) fan_country_iso2 = recipient.fan_country_iso2 or "" legal_entity = self.get_legal_entity(fan_country_iso2) privacy_links_block = self.get_privacy_links_block(fan_country_iso2) compliance_footer = self.get_compliance_footer(fan_country_iso2) yield FanData( email=validated_email.normalized, profile_token=str(preference_center_profile_token), privacy_links_block=privacy_links_block, legal_entity_name=legal_entity.name, legal_entity_address=legal_entity.address, country_iso2=fan_country_iso2, opt_in_info=compliance_footer.opt_in_info, opt_in_info_external=compliance_footer.opt_in_info_external, unsubscribe_text=compliance_footer.unsubscribe_text, profile_text=compliance_footer.profile_text, merge_tags=MergeTags(first_name=recipient.fan_first_name), ) @cachedmethod(lambda self: self._legal_cache) def get_legal_entity(self, country_iso2: str) -> LegalEntity: return self.legal_info_service.get_legal_entity(country_iso2) @cachedmethod(lambda self: self._privacy_cache) def get_privacy_links_block(self, country_iso2: str) -> str: return self.legal_info_service.prepare_privacy_links_for_country( country_iso2, self.settings.privacy_footer_utm_source_name ) @cachedmethod(lambda self: self._footer_cache) def get_compliance_footer(self, country_iso2: str) -> ComplianceFooter: translations = self.translation_service.get_translations_by_country_code( country_iso2 ) opt_in_info = "
".join( translation.optInInfo for translation in translations.values() ) opt_in_info_external = "
".join( translation.optInInfoExternal for translation in translations.values() ) unsubscribe = " | ".join( translation.unsubscribe for translation in translations.values() ) profile_texts = [t.profile for t in translations.values() if t.profile] profile_text = " | ".join(profile_texts) if profile_texts else None return ComplianceFooter( opt_in_info=opt_in_info, opt_in_info_external=opt_in_info_external, unsubscribe_text=unsubscribe, profile_text=profile_text, )