from dataclasses import dataclass import email_validator from anydi import singleton from fansifter_common.auth.requests import AuthAccountRequest from fansifter_common.auth.services import AuthService from fansifter_common.auth.types import Permission from fansifter_common.email import EmailType from email_campaigns.adapters.db.decorators import transactional from email_campaigns.audiences.exceptions import InvalidAudienceIdError from email_campaigns.audiences.models import Audience from email_campaigns.audiences.repositories import AudienceRepository from email_campaigns.campaigns.exceptions import ( EmailCampaignMustBeDraftError, EmailCampaignSenderEmailNotValidError, MalformedEmailCampaignError, ) from email_campaigns.campaigns.models import EmailCampaign from email_campaigns.campaigns.services import EmailCampaignService from email_campaigns.campaigns.validators import CampaignValidator from email_campaigns.emails.cache import EmailPublicPreviewCache from email_campaigns.emails.services import EmailDomainService from email_campaigns.rosters.services import FanDataListService from email_campaigns.rosters.types import FanDataListId @dataclass(kw_only=True) class UpdateCampaignRequest(AuthAccountRequest): campaign_id: str name: str fandata_list_id: FanDataListId sender_name: str | None email_domain_id: str | None email_username: str | None subject: str | None preview_text: str | None audience_id: str | None @singleton class UpdateCampaignHandler: permission = Permission("email_campaign", "edit") audience_permission = Permission("audience", "create_email_target") def __init__( self, auth_service: AuthService, campaign_service: EmailCampaignService, campaign_validator: CampaignValidator, email_domain_service: EmailDomainService, audience_repository: AudienceRepository, fandata_list_service: FanDataListService, preview_cache: EmailPublicPreviewCache, ) -> None: self.auth_service = auth_service self.campaign_service = campaign_service self.campaign_validator = campaign_validator self.email_domain_service = email_domain_service self.audience_repository = audience_repository self.fandata_list_service = fandata_list_service self.preview_cache = preview_cache @transactional def handle(self, request: UpdateCampaignRequest) -> EmailCampaign: campaign = self.campaign_service.get_campaign(request.campaign_id) # Check if user has permission to edit email campaign self.auth_service.authorize_account( request.identity_id, account=request.account, permission=self.permission, ) # Check if email campaign is not draft if not campaign.is_draft: raise EmailCampaignMustBeDraftError # Validate name self.campaign_validator.validate_name( name=request.name, account=request.account, campaign=campaign, ) # Validate global participant id if changed if ( request.fandata_list_id.is_artist and request.fandata_list_id.id != campaign.global_participant_id ): self.fandata_list_service.resolve_global_participant( request.fandata_list_id.id ) elif ( request.fandata_list_id.is_custom_list and request.fandata_list_id.id != campaign.custom_list_id ): self.fandata_list_service.resolve_allowed_custom_list( request.fandata_list_id.id, account=request.account, ) # Check if audience is allowed for email campaign self.auth_service.authorize_account( request.identity_id, account=request.account, permission=self.audience_permission, ) # Validate audience id audience = self._validate_audience_id(request) # Resolve allowed email domain email_domain = self.email_domain_service.resolve_allowed_domain( email_domain_id=request.email_domain_id, identity_id=request.identity_id, account=request.account, ) # Validate sender email if email_domain and request.sender_name and request.email_username: try: email_validator.validate_email( f"{request.email_username}@{email_domain.domain}", check_deliverability=False, allow_smtputf8=False, ) except email_validator.EmailNotValidError as exc: raise EmailCampaignSenderEmailNotValidError( f"Invalid email username '{request.email_username}': {exc}" ) from exc # Validate main rep only artists if changed if request.fandata_list_id.is_artist and ( request.fandata_list_id.id != campaign.global_participant_id ): self.fandata_list_service.ensure_artist_access( request.fandata_list_id, account=request.account, ) campaign.name = request.name campaign.account = request.account campaign.fandata_list_id = request.fandata_list_id campaign.sender_name = request.sender_name campaign.email_domain_id = request.email_domain_id campaign.email_domain = email_domain campaign.email_username = request.email_username campaign.subject = request.subject campaign.preview_text = request.preview_text campaign.updated_by = request.identity_id campaign.audience = audience self.campaign_service.save_campaign(campaign) # Invalidate cache after campaign update self.preview_cache.delete(campaign.id, EmailType.CAMPAIGN) return campaign def _validate_audience_id(self, request: UpdateCampaignRequest) -> Audience | None: if request.audience_id is None: return None audience = self.audience_repository.get(request.audience_id) # Audience does not exist if audience is None: raise InvalidAudienceIdError # Check if audience account and global_participant_id are # the same as changed email campaign if ( audience.account != request.account or ( request.fandata_list_id.is_artist and request.fandata_list_id.id not in audience.global_participant_ids ) or ( request.fandata_list_id.is_custom_list and request.fandata_list_id.id not in audience.custom_list_ids ) ): raise MalformedEmailCampaignError( "Audience account or fandata list does not match email campaign." ) return audience