from typing import Annotated, Any from anydi import Inject from fansifter_common.api.schemas import response_errors from fansifter_common.artist.exceptions import InvalidGlobalParticipantIdError from fansifter_common.auth.exceptions import InvalidAccountError from fastapi import APIRouter, Depends, Path, Query from starlette import status from starlette.responses import Response from ows_text_campaigns.api import auth, schemas from ows_text_campaigns.artist.exceptions import ( ArtistNotConfiguredError, ArtistTwilioAccountNotConfiguredError, ) from ows_text_campaigns.assets.exceptions import ( InvalidContentTypeError, InvalidFileSizeError, InvalidImageSizeError, ) from ows_text_campaigns.audiences.exceptions import ( ArchivedAudienceError, EmptyAudienceError, InvalidAudienceIdError, NotSupportedAudienceTargetError, ) from ows_text_campaigns.campaigns.dtos import CampaignLinkAnalytics from ows_text_campaigns.campaigns.exceptions import ( CampaignAlreadyQueuedError, CampaignAttachmentLimitExceededError, CampaignAudienceRequiredError, CampaignMustBeDraftError, CampaignMustBeScheduledError, CampaignNotFoundError, CampaignSendAtMustBeFutureError, CampaignUniqueNameError, ChannelNotAllowedInCountryError, EmptyCampaignContentError, InvalidCampaignAttachmentError, InvalidShortLinkTagError, InvalidUrlDomainIdError, MalformedCampaignError, PersonalizedAttributesLimitExceededError, ShorteningPathIsNotAvailableError, ) from ows_text_campaigns.campaigns.handlers import ( CreateCampaignAttachmentHandler, CreateCampaignAttachmentRequest, CreateCampaignAttachmentUploadCredentialsHandler, CreateCampaignAttachmentUploadCredentialsRequest, CreateCampaignHandler, CreateCampaignRequest, DeleteCampaignHandler, DeleteCampaignRequest, DuplicateCampaignHandler, DuplicateCampaignRequest, GetCampaignAnalyticsHandler, GetCampaignAnalyticsRequest, GetCampaignAttachmentsHandler, GetCampaignAttachmentsRequest, GetCampaignHandler, GetCampaignLinkAnalyticsHandler, GetCampaignLinkAnalyticsRequest, GetCampaignLiveStatusHandler, GetCampaignLiveStatusRequest, GetCampaignMessagePreviewHandler, GetCampaignMessagePreviewRequest, GetCampaignPricingEstimationHandler, GetCampaignPricingEstimationRequest, GetCampaignRequest, GetCampaignSendTimezonesHandler, GetCampaignSendTimezonesRequest, GetCampaignsHandler, GetCampaignShortenedUrlsHandler, GetCampaignShortenedUrlsRequest, GetCampaignsRequest, RenderCampaignMessagesBatchHandler, RenderCampaignMessagesBatchRequest, ScanCampaignContentHandler, ScanCampaignContentRequest, ScheduleCampaignHandler, ScheduleCampaignRequest, SendCampaignTestMessageHandler, SendCampaignTestMessageRequest, UnscheduleCampaignHandler, UnscheduleCampaignRequest, UpdateCampaignContentHandler, UpdateCampaignContentRequest, UpdateCampaignHandler, UpdateCampaignRequest, ValidateCampaignShortenedUrlHandler, ValidateCampaignShortenedUrlRequest, ) from ows_text_campaigns.rosters.exceptions import MainRepArtistOnlyAllowedError router = APIRouter(tags=["Campaigns"]) @router.get( "/campaigns", operation_id="getCampaigns", response_model=schemas.LimitOffsetPage[schemas.Campaign], response_model_by_alias=True, ) def get_campaigns( identity_id: Annotated[str, Depends(auth.get_identity_id)], params: Annotated[schemas.GetCampaignsInput, Query()], handler: Annotated[GetCampaignsHandler, Inject()], ) -> Any: order_by = params.order_by or GetCampaignsRequest.DEFAULT_ORDER_BY limit = params.limit or GetCampaignsRequest.DEFAULT_LIMIT offset = params.offset or GetCampaignsRequest.DEFAULT_OFFSET response = handler.handle( GetCampaignsRequest( identity_id=identity_id, search=params.search, vendor_id=params.vendor_id, subaccount_id=params.subaccount_id, global_participant_ids=params.global_participant_ids, statuses=params.statuses, order_by=order_by, limit=limit, offset=offset, ) ) return schemas.LimitOffsetPage( total=response.total, items=list(response.items), limit=limit, offset=offset, ) @router.post( "/campaigns", operation_id="createCampaign", response_model=schemas.Campaign, response_model_by_alias=True, status_code=status.HTTP_201_CREATED, responses=response_errors( InvalidGlobalParticipantIdError, InvalidAccountError, CampaignUniqueNameError, ArtistNotConfiguredError, MainRepArtistOnlyAllowedError, ), ) def create_campaign( identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.CreateCampaignInput, handler: Annotated[CreateCampaignHandler, Inject()], ) -> Any: return handler.handle( CreateCampaignRequest( identity_id=identity_id, name=data.name, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, global_participant_id=data.global_participant_id, audience_id=data.audience_id, ) ) @router.get( "/campaigns/{campaignId}", operation_id="getCampaign", response_model=schemas.Campaign, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], handler: Annotated[GetCampaignHandler, Inject()], ) -> Any: return handler.handle( GetCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.delete( "/campaigns/{campaignId}", operation_id="deleteCampaignContent", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( CampaignNotFoundError, CampaignMustBeDraftError, ), ) def delete_campaign( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], handler: Annotated[DeleteCampaignHandler, Inject()], ) -> None: handler.handle( DeleteCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.put( "/campaigns/{campaignId}", operation_id="updateCampaign", response_model=schemas.Campaign, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, CampaignMustBeDraftError, CampaignUniqueNameError, InvalidGlobalParticipantIdError, InvalidAudienceIdError, ArtistNotConfiguredError, MalformedCampaignError, ArchivedAudienceError, NotSupportedAudienceTargetError, EmptyAudienceError, MainRepArtistOnlyAllowedError, ), ) def update_campaign( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.UpdateCampaignInput, handler: Annotated[UpdateCampaignHandler, Inject()], ) -> Any: return handler.handle( UpdateCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, global_participant_id=data.global_participant_id, name=data.name, audience_id=data.audience_id, ) ) @router.get( "/campaigns/{campaignId}/content", operation_id="getCampaignContent", response_model=schemas.CampaignContent, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_content( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], handler: Annotated[GetCampaignHandler, Inject()], ) -> Any: return handler.handle( GetCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.patch( "/campaigns/{campaignId}/content", operation_id="updateCampaignContent", response_model=schemas.CampaignContent, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, CampaignMustBeDraftError, CampaignAttachmentLimitExceededError, InvalidShortLinkTagError, ), ) def update_campaign_content( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.UpdateCampaignContentInput, handler: Annotated[UpdateCampaignContentHandler, Inject()], ) -> Any: return handler.handle( UpdateCampaignContentRequest( identity_id=identity_id, campaign_id=campaign_id, content=data.content, asset_ids=data.asset_ids, ) ) @router.post( "/campaigns/{campaignId}/content/scan", operation_id="scanCampaignContent", response_model=schemas.ScanCampaignContentOutput, response_model_by_alias=True, responses=response_errors(CampaignNotFoundError), ) def scan_campaign_content( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], data: schemas.ScanCampaignContentInput, handler: Annotated[ScanCampaignContentHandler, Inject()], ) -> Any: return handler.handle( ScanCampaignContentRequest( identity_id=identity_id, campaign_id=campaign_id, content=data.content, ) ) @router.post( "/campaigns/{campaignId}/duplicate", operation_id="duplicateCampaign", response_model=schemas.Campaign, response_model_by_alias=True, status_code=status.HTTP_201_CREATED, responses=response_errors( CampaignNotFoundError, CampaignUniqueNameError, ), ) def duplicate_campaign( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.DuplicateCampaignInput, handler: Annotated[DuplicateCampaignHandler, Inject()], ) -> Any: return handler.handle( DuplicateCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, name=data.name, ) ) @router.get( "/campaigns/{campaignId}/attachments", operation_id="getCampaignAttachments", response_model=list[schemas.Asset], response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_attachments( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], handler: Annotated[GetCampaignAttachmentsHandler, Inject()], ) -> Any: return handler.handle( GetCampaignAttachmentsRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.post( "/campaigns/{campaignId}/attachments", operation_id="createCampaignAttachment", response_model=schemas.Asset, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, InvalidContentTypeError, InvalidFileSizeError, InvalidImageSizeError, InvalidCampaignAttachmentError, ), status_code=status.HTTP_201_CREATED, ) def create_campaign_attachment( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.CreateCampaignAttachmentInput, handler: Annotated[CreateCampaignAttachmentHandler, Inject()], ) -> Any: return handler.handle( CreateCampaignAttachmentRequest( identity_id=identity_id, campaign_id=campaign_id, attachment_key=data.attachment_key, ) ) @router.post( "/campaigns/{campaignId}/attachments/upload/credentials", operation_id="createCampaignAttachmentUploadCredentials", response_model=schemas.UploadCredentials, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), status_code=status.HTTP_201_CREATED, ) def create_campaign_attachment_upload_credentials( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.CreateCampaignAttachmentUploadCredentialsInput, handler: Annotated[CreateCampaignAttachmentUploadCredentialsHandler, Inject()], ) -> Any: return handler.handle( CreateCampaignAttachmentUploadCredentialsRequest( identity_id=identity_id, campaign_id=campaign_id, filename=data.filename, ) ) @router.get( "/campaigns/{campaignId}/shortened-urls", operation_id="getCampaignShortenedUrls", response_model=list[schemas.ShortenedUrl], response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_shortened_urls( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], handler: Annotated[GetCampaignShortenedUrlsHandler, Inject()], ) -> Any: return handler.handle( GetCampaignShortenedUrlsRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.post( "/campaigns/{campaignId}/shortened-urls/validate", operation_id="validateCampaignShortenedUrl", response_model=schemas.ShortenedUrl, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, InvalidUrlDomainIdError, ShorteningPathIsNotAvailableError, ), ) def validate_campaign_shortened_url( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.ValidateCampaignShortenedUrlInput, handler: Annotated[ValidateCampaignShortenedUrlHandler, Inject()], ) -> Any: return handler.handle( ValidateCampaignShortenedUrlRequest( identity_id=identity_id, campaign_id=campaign_id, shortened_url_id=data.id, url=str(data.url), method=data.method, domain=data.domain, path=data.path, ) ) @router.post( "/campaigns/{campaignId}/schedule", operation_id="scheduleCampaign", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( ArtistNotConfiguredError, ArtistTwilioAccountNotConfiguredError, CampaignNotFoundError, CampaignMustBeDraftError, CampaignSendAtMustBeFutureError, EmptyCampaignContentError, InvalidAudienceIdError, ArchivedAudienceError, NotSupportedAudienceTargetError, EmptyAudienceError, ChannelNotAllowedInCountryError, ), ) def schedule_campaign( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.ScheduleCampaignInput, handler: Annotated[ScheduleCampaignHandler, Inject()], ) -> None: handler.handle( ScheduleCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, send_at=data.send_at, timezone=data.timezone, adjustments=data.adjustments, edit_mode=False, ) ) @router.put( "/campaigns/{campaignId}/schedule", operation_id="editCampaignSchedule", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( ArtistNotConfiguredError, CampaignNotFoundError, CampaignMustBeScheduledError, CampaignAlreadyQueuedError, CampaignSendAtMustBeFutureError, EmptyCampaignContentError, InvalidAudienceIdError, ArchivedAudienceError, NotSupportedAudienceTargetError, EmptyAudienceError, ChannelNotAllowedInCountryError, ), ) def edit_campaign_schedule( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], data: schemas.ScheduleCampaignInput, handler: Annotated[ScheduleCampaignHandler, Inject()], ) -> None: handler.handle( ScheduleCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, send_at=data.send_at, timezone=data.timezone, adjustments=data.adjustments, edit_mode=True, ) ) @router.post( "/campaigns/{campaignId}/unschedule", operation_id="unscheduleCampaign", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( CampaignNotFoundError, CampaignMustBeScheduledError, CampaignAlreadyQueuedError, ), ) def unschedule_campaign( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], handler: Annotated[UnscheduleCampaignHandler, Inject()], ) -> None: handler.handle( UnscheduleCampaignRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.get( "/campaigns/{campaignId}/analytics", operation_id="getCampaignAnalytics", response_model=schemas.CampaignDetailedAnalytics, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_analytics( campaign_id: Annotated[str, Path(alias="campaignId")], identity_id: Annotated[str, Depends(auth.get_identity_id)], handler: Annotated[GetCampaignAnalyticsHandler, Inject()], ) -> Any: return handler.handle( GetCampaignAnalyticsRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.get( "/campaigns/{campaignId}/analytics/links", operation_id="getCampaignLinkAnalytics", response_model=list[schemas.CampaignLinkAnalytics], response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_link_analytics( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], handler: Annotated[GetCampaignLinkAnalyticsHandler, Inject()], ) -> list[CampaignLinkAnalytics]: return handler.handle( GetCampaignLinkAnalyticsRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.post( "/campaigns/{campaignId}/messages/batch-render", operation_id="renderCampaignMessagesBatch", summary="Render messages for a campaign with personalized attributes.", tags=["Internal"], response_model=schemas.RenderCampaignMessagesBatchOutput, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, PersonalizedAttributesLimitExceededError, ), ) def render_campaign_messages_batch( campaign_id: Annotated[str, Path(alias="campaignId")], data: schemas.RenderCampaignMessagesBatchInput, handler: Annotated[RenderCampaignMessagesBatchHandler, Inject()], ) -> Any: return handler.handle( RenderCampaignMessagesBatchRequest( campaign_id=campaign_id, personalized_attributes=data.personalized_attributes, ) ) @router.get( "/campaigns/{campaignId}/messages/preview", operation_id="getCampaignMessagePreview", tags=["Internal"], response_model=schemas.GetCampaignMessagePreviewOutput, response_model_by_alias=True, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_message_preview( campaign_id: Annotated[str, Path(alias="campaignId")], data: Annotated[schemas.GetCampaignMessagePreviewInput, Query()], handler: Annotated[GetCampaignMessagePreviewHandler, Inject()], ) -> Any: return handler.handle( GetCampaignMessagePreviewRequest( campaign_id=campaign_id, channel=data.channel, include_unsubscribe_text=data.include_unsubscribe_text, ) ) @router.post( "/campaigns/{campaignId}/messages/send-test", operation_id="sendCampaignTestMessage", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( ArtistNotConfiguredError, ArtistTwilioAccountNotConfiguredError, CampaignNotFoundError, EmptyCampaignContentError, ), ) def send_campaign_test_message( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], data: schemas.SendTestCampaignInput, handler: Annotated[SendCampaignTestMessageHandler, Inject()], ) -> None: handler.handle( SendCampaignTestMessageRequest( identity_id=identity_id, campaign_id=campaign_id, phone_number=data.phone_number, ) ) @router.get( "/campaigns/{campaignId}/status", operation_id="getCampaignLiveStatus", status_code=status.HTTP_200_OK, response_model=schemas.GetCampaignLiveStatusOutput, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_live_status( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], handler: Annotated[GetCampaignLiveStatusHandler, Inject()], ) -> Any: return handler.handle( GetCampaignLiveStatusRequest( identity_id=identity_id, campaign_id=campaign_id, ) ) @router.get( "/campaigns/{campaignId}/pricing-estimation", operation_id="getCampaignPricingEstimation", status_code=status.HTTP_200_OK, response_model=schemas.GetCampaignPricingEstimationResponse, responses=response_errors( CampaignNotFoundError, CampaignAudienceRequiredError, ), ) def get_campaign_pricing_estimation( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], params: Annotated[schemas.GetCampaignPricingEstimationInput, Query()], handler: Annotated[GetCampaignPricingEstimationHandler, Inject()], ) -> Any: return handler.handle( GetCampaignPricingEstimationRequest( identity_id=identity_id, campaign_id=campaign_id, reply_share=params.reply_share, segments_count=params.segments_count, ) ) @router.get( "/campaigns/{campaignId}/send-timezones", operation_id="getCampaignSendTimezones", status_code=status.HTTP_200_OK, response_model=schemas.GetCampaignSendTimezonesOutput, responses=response_errors( CampaignNotFoundError, ), ) def get_campaign_send_timezones( identity_id: Annotated[str, Depends(auth.get_identity_id)], campaign_id: Annotated[str, Path(alias="campaignId")], params: Annotated[schemas.GetCampaignSendTimezonesInput, Query()], handler: Annotated[GetCampaignSendTimezonesHandler, Inject()], ) -> Any: return handler.handle( GetCampaignSendTimezonesRequest( identity_id=identity_id, campaign_id=campaign_id, send_at=params.send_at, ) )