import logging
import time
import urllib.parse
from dataclasses import dataclass
from functools import lru_cache
from emval import validate_email
from fansifter_common.adapters.sendgrid.models import FanData, MergeTags
from fansifter_common.identifiers.types import (
DoubleOptInIdentifier,
FansifterProfileIdentifier,
)
from fansifter_common.identifiers.utils import (
encrypt_double_opt_in_token,
encrypt_profile_token,
)
from fansifter_common.legal_info.types import LegalEntity
from app.adapters.preferences import encrypter, legal_info, translations
from app.config import settings
from app.types import CsvRow
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
def _build_double_opt_in_url(row: CsvRow, campaign_id: str) -> str | None:
if not row.is_doi_send or row.confirmation_id is None:
return None
identifier = DoubleOptInIdentifier.model_construct(
event_row_id=row.confirmation_id,
email_id=campaign_id,
automated_email_trigger_id=row.automated_email_trigger_id,
iat=time.time(),
)
token = encrypt_double_opt_in_token(encrypter, identifier=identifier)
encoded_token = urllib.parse.quote(str(token), safe="")
return f"{settings.preference_center_url}/double-opt-in?token={encoded_token}"
def get_fan_data(row: CsvRow, campaign_id: str) -> FanData | None:
try:
validated_email = validate_email(row.fan_email, deliverable_address=False)
except Exception:
logger.warning(
"Detected invalid email for fan: %s***",
row.fan_email[:3],
extra={"campaign_id": campaign_id, "row_id": row.row_id},
)
return None
if (
settings.send_fansifter_only
and validated_email.ascii_domain not in settings.fansifter_domains
):
return None
if row.fan_profile_id:
preference_center_profile_token = encrypt_profile_token(
encrypter,
identifier=FansifterProfileIdentifier.model_construct(
profile_id=row.fan_profile_id,
email_campaign_id=campaign_id,
),
)
else:
preference_center_profile_token = None
fan_country_iso2 = row.fan_country_code or ""
legal_entity = _get_legal_entity(fan_country_iso2)
privacy_links_block = _get_privacy_links_block(fan_country_iso2)
compliance_footer = _get_compliance_footer(fan_country_iso2)
return FanData(
email=validated_email.normalized,
profile_token=str(preference_center_profile_token)
if preference_center_profile_token
else None,
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,
double_opt_in_url=_build_double_opt_in_url(row, campaign_id),
profile_text=compliance_footer.profile_text,
merge_tags=MergeTags(first_name=row.fan_first_name),
)
@lru_cache(maxsize=256)
def _get_legal_entity(country_iso2: str) -> LegalEntity:
return legal_info.get_legal_entity(country_iso2)
@lru_cache(maxsize=256)
def _get_privacy_links_block(country_iso2: str) -> str:
return legal_info.prepare_privacy_links_for_country(
country_iso2, settings.privacy_footer_utm_source_name
)
@lru_cache(maxsize=256)
def _get_compliance_footer(country_iso2: str) -> ComplianceFooter:
all_translations = translations.get_translations_by_country_code(country_iso2)
opt_in_info = "
".join(t.optInInfo for t in all_translations.values())
opt_in_info_external = "
".join(
t.optInInfoExternal for t in all_translations.values()
)
unsubscribe = " | ".join(t.unsubscribe for t in all_translations.values())
profile_texts = [t.profile for t in all_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,
)