import datetime import decimal from unittest import mock import faker import pytest from anydi import Container from dirty_equals import IsList from fansifter_common.auth.account import Account from starlette.testclient import TestClient from dmp.adapters.features import AUDIENCE_SHOW_QA_FAN_SEGMENTATION_MODEL from dmp.artists.dtos import ( ArtistAccount, ArtistFansCount, FansSegmentExplained, FansSegmentValue, FansShareByAgeRange, FansShareByCountry, FansShareByGender, FansShareByHeavyRotation, FansTimeseries, FansTimeseriesValue, Label, ) from dmp.artists.enums import ArtistFanDataType from dmp.artists.handlers import ( GetArtistAccountsAllHandler, GetArtistAccountsHandler, GetArtistAccountsWithFanDataHandler, GetArtistFansCountBySegmentHandler, GetArtistFansCountByTypeTimeseriesHandler, GetArtistFansCountHandler, GetArtistFansShareByAgeRangeHandler, GetArtistFansShareByAgeRangeResponse, GetArtistFansShareByGenderHandler, GetArtistFansShareByGenderResponse, GetArtistFansShareByLocationHandler, GetArtistFansShareByLocationResponse, GetArtistMaxSpendHandler, GetArtistSegmentsExplainedHandler, ) from dmp.artists.handlers.get_artist_accounts_with_fandata import ( GetArtistAccountsWithFanDataResponse, ) from dmp.artists.handlers.get_artist_fans_share_by_heavy_rotation import ( GetArtistFansShareByHeavyRotationHandler, GetArtistFansShareByHeavyRotationResponse, ) from dmp.artists.models import VirtualParticipant from dmp.fandata.enums import ( AcquisitionChannel, AgeRange, FanSegment, Gender, HeavyRotation, SegmentCategory, ) from dmp.fandata.models import ( DimCountry, FansByArtistAccountAcquisitionCampaignDailyDbt, FansByArtistAccountAcquisitionChannelDailyDbt, FansByArtistAccountAcquisitionCountryDailyDbt, FansByArtistAccountCategoryCountryDbt, FansByArtistAccountCategoryDbt, FansByArtistAccountDbt, FansByArtistAccountHeavyRotationDailyDbt, FansByArtistAccountOtherArtistDbt, FansByArtistAccountSegmentDailyDbt, GlobalFansByArtistDbt, QaFansByArtistAccountSegmentDailyDbt, ) from dmp.rosters.models import ( ArtistRosterMainRep, CustomList, GlobalParticipantAccountDbt, ) from dmp.rosters.services import GlobalFanDataAccessService from tests.unit.faker import FakerTyped from tests.unit.types import BuildModel, CreateReportingModel, EnableFeatures def test_get_artist_accounts( client: TestClient, container: Container, fake: FakerTyped, build_model: BuildModel, ) -> None: global_participant_id = fake.uuid4_string() account1 = build_model(ArtistAccount) account2 = build_model(ArtistAccount) handler_mock = mock.MagicMock(spec=GetArtistAccountsHandler) handler_mock.handle.return_value = [account1, account2] with container.override(GetArtistAccountsHandler, instance=handler_mock): response = client.get(f"/artists/{global_participant_id}/accounts") assert response.status_code == 200 assert response.json() == [ { "vendorId": account1.vendor_id, "subaccountId": account1.subaccount_id, "fansCount": account1.fans_count, }, { "vendorId": account2.vendor_id, "subaccountId": account2.subaccount_id, "fansCount": account2.fans_count, }, ] def test_get_artist_accounts_with_fandata( client: TestClient, container: Container, fake: FakerTyped, build_model: BuildModel, ) -> None: global_participant_id = fake.uuid4_string() account = build_model(Label) handler_mock = mock.MagicMock(spec=GetArtistAccountsWithFanDataHandler) handler_mock.handle.return_value = GetArtistAccountsWithFanDataResponse( has_fandata=True, has_global_fandata_list=True, accounts=[account], ) with container.override(GetArtistAccountsWithFanDataHandler, instance=handler_mock): response = client.get(f"/artists/{global_participant_id}/accounts-with-fandata") assert response.status_code == 200 assert response.json() == { "hasFanData": True, "hasGlobalFanDataList": True, "accounts": [ { "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, }, ], } def test_get_artist_accounts_all( client: TestClient, container: Container, fake: FakerTyped, build_model: BuildModel, ) -> None: global_participant_id = fake.uuid4_string() vendor_id_1 = fake.integer() vendor_id_2 = vendor_id_1 + 1 account1 = build_model(Label, vendor_id=vendor_id_1) account2 = build_model(Label, vendor_id=vendor_id_2) handler_mock = mock.MagicMock(spec=GetArtistAccountsAllHandler) handler_mock.handle.return_value = [account1, account2] with container.override(GetArtistAccountsAllHandler, instance=handler_mock): response = client.get(f"/artists/{global_participant_id}/accounts/all") assert response.status_code == 200 assert response.json() == [ { "vendorId": account1.vendor_id, "subaccountId": account1.subaccount_id, }, { "vendorId": account2.vendor_id, "subaccountId": account2.subaccount_id, }, ] @pytest.mark.db def test_get_artist_main_rep_accounts( client: TestClient, container: Container, faker: faker.Faker, create_reporting_model: CreateReportingModel, ) -> None: global_participant_id = faker.uuid4() vendor_id_1 = faker.pyint() vendor_id_2 = faker.pyint() main_rep_1 = create_reporting_model( ArtistRosterMainRep, global_participant_id=global_participant_id, vendor_id=vendor_id_1, ) main_rep_2 = create_reporting_model( ArtistRosterMainRep, global_participant_id=global_participant_id, vendor_id=vendor_id_2, ) global_fandata_access_service_mock = mock.MagicMock( spec=GlobalFanDataAccessService, get_enabled_for_any_vendor=mock.Mock( return_value=[ vendor_id_1, vendor_id_2, ] ), ) with container.override( GlobalFanDataAccessService, global_fandata_access_service_mock ): response = client.get(f"/artists/{global_participant_id}/main-rep-accounts") assert response.status_code == 200 assert response.json() == IsList( { "vendorId": main_rep_1.vendor_id, "subaccountId": main_rep_1.subaccount_id, }, { "vendorId": main_rep_2.vendor_id, "subaccountId": main_rep_2.subaccount_id, }, check_order=False, ) def test_get_artist_fans_count( client: TestClient, container: Container, fake: FakerTyped ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() handler_mock = mock.MagicMock(spec=GetArtistFansCountHandler) handler_mock.handle.return_value = ArtistFansCount( fans_count=1000, email_consent_fans_count=1000, ad_consent_fans_count=1000, sms_consent_fans_count=1000, ) with container.override(GetArtistFansCountHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/fans-count", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == { "fansCount": 1000, "emailConsentFansCount": 1000, "adConsentFansCount": 1000, "smsConsentFansCount": 1000, } def test_get_artist_fans_count_by_segment( client: TestClient, container: Container, faker: faker.Faker ) -> None: vendor_id = faker.pyint() subaccount_id = faker.pyint() global_participant_id = faker.uuid4() handler_mock = mock.MagicMock(spec=GetArtistFansCountBySegmentHandler) handler_mock.handle.return_value = [ FansSegmentValue(label=FanSegment.SUPER_FANS, value=100), FansSegmentValue(label=FanSegment.ENGAGED_FANS, value=0), FansSegmentValue(label=FanSegment.CASUAL_FANS, value=500), FansSegmentValue(label=FanSegment.FANS_TO_WIN_BACK, value=0), FansSegmentValue(label=FanSegment.NEW_FANS, value=120), ] with container.override(GetArtistFansCountBySegmentHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/segments", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == [ {"label": "SUPER_FANS", "value": 100}, {"label": "ENGAGED_FANS", "value": 0}, {"label": "CASUAL_FANS", "value": 500}, {"label": "FANS_TO_WIN_BACK", "value": 0}, {"label": "NEW_FANS", "value": 120}, ] def test_get_artist_fans_count_by_acquisition_channel_timeseries( client: TestClient, container: Container, fake: FakerTyped ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() date = datetime.date(year=2023, month=6, day=13) handler_mock = mock.MagicMock(spec=GetArtistFansCountByTypeTimeseriesHandler) handler_mock.handle.return_value = [ FansTimeseries( id=AcquisitionChannel.SMF_GENERAL_PRESAVE, items=[FansTimeseriesValue(date=date, value=123)], ), FansTimeseries( id=AcquisitionChannel.SHOPIFY, items=[FansTimeseriesValue(date=date, value=256)], ), FansTimeseries( id=AcquisitionChannel.MAKE_THE_LINK, items=[FansTimeseriesValue(date=date, value=785)], ), ] with container.override(GetArtistFansCountByTypeTimeseriesHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/timeseries", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (date - datetime.timedelta(days=6)).isoformat(), "endDate": date.isoformat(), "type": ArtistFanDataType.ACQUISITION_CHANNEL, }, ) assert response.status_code == 200 assert response.json() == [ { "id": "SMF_GENERAL_PRESAVE", "name": None, "channel": None, "items": [{"date": "2023-06-13", "value": 123}], }, { "id": "SHOPIFY", "name": None, "channel": None, "items": [{"date": "2023-06-13", "value": 256}], }, { "id": "MAKE_THE_LINK", "name": None, "channel": None, "items": [{"date": "2023-06-13", "value": 785}], }, ] @pytest.mark.db def test_get_artist_fans_count_by_segment_timeseries( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() date = datetime.date(year=2023, month=6, day=13) create_reporting_model( FansByArtistAccountSegmentDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.SUPER_FANS, report_date=date, fans_count=100, ) create_reporting_model( FansByArtistAccountSegmentDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.CASUAL_FANS, report_date=date, fans_count=70, ) response = client.get( f"/artists/{global_participant_id}/timeseries", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (date - datetime.timedelta(days=3)).isoformat(), "endDate": (date + datetime.timedelta(days=3)).isoformat(), "type": ArtistFanDataType.FAN_SEGMENT, }, ) assert response.status_code == 200 assert response.json() == [ { "id": "SUPER_FANS", "name": None, "channel": None, "items": [ {"date": "2023-06-10", "value": 0}, {"date": "2023-06-11", "value": 0}, {"date": "2023-06-12", "value": 0}, {"date": "2023-06-13", "value": 100}, {"date": "2023-06-14", "value": 100}, {"date": "2023-06-15", "value": 100}, {"date": "2023-06-16", "value": 100}, ], }, { "id": "CASUAL_FANS", "name": None, "channel": None, "items": [ {"date": "2023-06-10", "value": 0}, {"date": "2023-06-11", "value": 0}, {"date": "2023-06-12", "value": 0}, {"date": "2023-06-13", "value": 70}, {"date": "2023-06-14", "value": 70}, {"date": "2023-06-15", "value": 70}, {"date": "2023-06-16", "value": 70}, ], }, { "id": "TOTALS", "name": None, "channel": None, "items": [ {"date": "2023-06-10", "value": 0}, {"date": "2023-06-11", "value": 0}, {"date": "2023-06-12", "value": 0}, {"date": "2023-06-13", "value": 170}, {"date": "2023-06-14", "value": 170}, {"date": "2023-06-15", "value": 170}, {"date": "2023-06-16", "value": 170}, ], }, ] def test_get_artist_fans_share_by_location( client: TestClient, container: Container, fake: FakerTyped ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() available_fans_share = decimal.Decimal("0.512") handler_mock = mock.MagicMock(spec=GetArtistFansShareByLocationHandler) handler_mock.handle.return_value = GetArtistFansShareByLocationResponse( available_fans_share=available_fans_share, items=[ FansShareByCountry( country_code="US", fans_share=decimal.Decimal("0.5"), ), FansShareByCountry( country_code="EE", fans_share=decimal.Decimal("0.1"), ), FansShareByCountry( country_code="UK", fans_share=decimal.Decimal("0.3"), ), ], ) with container.override(GetArtistFansShareByLocationHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/locations", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == { "availableFansShare": 0.512, "items": [ {"countryCode": "US", "fansShare": 0.5}, {"countryCode": "EE", "fansShare": 0.1}, {"countryCode": "UK", "fansShare": 0.3}, ], } def test_get_artist_fans_share_by_gender( client: TestClient, container: Container, fake: FakerTyped ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() handler_mock = mock.MagicMock(spec=GetArtistFansShareByGenderHandler) handler_mock.handle.return_value = GetArtistFansShareByGenderResponse( available_fans_share=decimal.Decimal("0.512"), items=[ FansShareByGender( gender=Gender.MALE, fans_share=decimal.Decimal("0.5"), ), FansShareByGender( gender=Gender.FEMALE, fans_share=decimal.Decimal("0.1"), ), ], ) with container.override(GetArtistFansShareByGenderHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/genders", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == { "availableFansShare": 0.512, "items": [ {"gender": "MALE", "fansShare": 0.5}, {"gender": "FEMALE", "fansShare": 0.1}, ], } @pytest.mark.db def test_get_artist_fans_count_by_acquisition_channel_summary( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() today = datetime.date.today() create_reporting_model( FansByArtistAccountAcquisitionChannelDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquisition_channel=AcquisitionChannel.SHOPIFY, acquired_at_date=today, daily_running_total_fans_count=100, ) response = client.get( f"/artists/{global_participant_id}/summary", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (today - datetime.timedelta(days=6)).isoformat(), "endDate": today.isoformat(), "type": ArtistFanDataType.ACQUISITION_CHANNEL, }, ) assert response.status_code == 200 assert response.json() == [ {"id": "SHOPIFY", "value": 100, "change": None}, ] @pytest.mark.db def test_get_artist_fans_count_by_heavy_rotation_summary( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() today = datetime.date.today() create_reporting_model( FansByArtistAccountHeavyRotationDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, heavy_rotation=HeavyRotation.IN, daily_running_total_fans_count=100, ) response = client.get( f"/artists/{global_participant_id}/summary", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (today - datetime.timedelta(days=6)).isoformat(), "endDate": today.isoformat(), "type": ArtistFanDataType.HEAVY_ROTATION, }, ) assert response.status_code == 200 assert response.json() == [ {"id": "IN", "value": 100, "change": None}, ] @pytest.mark.db def test_get_artist_fans_count_by_segment_summary( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() today = datetime.date.today() create_reporting_model( FansByArtistAccountSegmentDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.SUPER_FANS, report_date=today, fans_count=100, ) response = client.get( f"/artists/{global_participant_id}/summary", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (today - datetime.timedelta(days=6)).isoformat(), "endDate": today.isoformat(), "type": ArtistFanDataType.FAN_SEGMENT, }, ) assert response.status_code == 200 assert response.json() == [ { "id": "SUPER_FANS", "value": 100, "change": None, }, ] @pytest.mark.db def test_get_artist_fans_count_by_segment_summary_use_qa_model( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, enable_features: EnableFeatures, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() today = datetime.date.today() create_reporting_model( QaFansByArtistAccountSegmentDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.SUPER_FANS, report_date=today, fans_count=200, ) with enable_features([AUDIENCE_SHOW_QA_FAN_SEGMENTATION_MODEL]): response = client.get( f"/artists/{global_participant_id}/summary", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (today - datetime.timedelta(days=6)).isoformat(), "endDate": today.isoformat(), "type": ArtistFanDataType.FAN_SEGMENT, }, ) assert response.status_code == 200 assert response.json() == [ { "id": "SUPER_FANS", "value": 200, "change": None, }, ] @pytest.mark.db def test_get_artist_fans_count_by_acquisition_country_summary( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() country = "EE" today = datetime.date.today() create_reporting_model(DimCountry, country_code=country) create_reporting_model( FansByArtistAccountAcquisitionCountryDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquired_at_date=today, country_iso2=country, daily_running_total_fans_count=100, ) response = client.get( f"/artists/{global_participant_id}/summary", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (today - datetime.timedelta(days=6)).isoformat(), "endDate": today.isoformat(), "type": ArtistFanDataType.ACQUISITION_COUNTRY, }, ) assert response.status_code == 200 assert response.json() == [ {"id": country, "value": 100, "change": 100}, ] def test_get_artist_max_spend( client: TestClient, container: Container, fake: FakerTyped ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() handler_mock = mock.MagicMock(spec=GetArtistMaxSpendHandler) handler_mock.handle.return_value = decimal.Decimal(100) with container.override(GetArtistMaxSpendHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/max-spend", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == {"maxSpend": 100} def test_get_artist_fans_share_by_age_range( client: TestClient, container: Container, fake: FakerTyped ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() handler_mock = mock.MagicMock(spec=GetArtistFansShareByAgeRangeHandler) handler_mock.handle.return_value = GetArtistFansShareByAgeRangeResponse( available_fans_share=decimal.Decimal("0.485"), items=[ FansShareByAgeRange( age_range=AgeRange.FROM_13_TO_17, fans_share=decimal.Decimal("0.5"), ), FansShareByAgeRange( age_range=AgeRange.FROM_18_TO_22, fans_share=decimal.Decimal("0.1"), ), FansShareByAgeRange( age_range=AgeRange.FROM_41_TO_51, fans_share=decimal.Decimal("0.3"), ), ], ) with container.override(GetArtistFansShareByAgeRangeHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/age-ranges", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == { "availableFansShare": 0.485, "items": [ {"ageRange": "FROM_13_TO_17", "fansShare": 0.5}, {"ageRange": "FROM_18_TO_22", "fansShare": 0.1}, {"ageRange": "FROM_41_TO_51", "fansShare": 0.3}, ], } def test_get_artist_fans_share_by_heavy_rotation( client: TestClient, container: Container, fake: FakerTyped ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() handler_mock = mock.MagicMock(spec=GetArtistFansShareByHeavyRotationHandler) handler_mock.handle.return_value = GetArtistFansShareByHeavyRotationResponse( available_fans_share=decimal.Decimal("0.512"), items=[ FansShareByHeavyRotation( heavy_rotation=HeavyRotation.IN, fans_share=decimal.Decimal("0.5"), ), FansShareByHeavyRotation( heavy_rotation=HeavyRotation.NOT_IN, fans_share=decimal.Decimal("0.1"), ), ], ) with container.override(GetArtistFansShareByHeavyRotationHandler, handler_mock): response = client.get( f"/artists/{global_participant_id}/heavy-rotation", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == { "availableFansShare": 0.512, "items": [ {"heavyRotation": "IN", "fansShare": 0.5}, {"heavyRotation": "NOT_IN", "fansShare": 0.1}, ], } @pytest.mark.db def test_get_artist_fans_also_listening( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() fans_share_1 = create_reporting_model( FansByArtistAccountOtherArtistDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, global_participant_id_listened_together=fake.uuid4_string(), fans_share=decimal.Decimal("0.2"), ) fans_share_2 = create_reporting_model( FansByArtistAccountOtherArtistDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, global_participant_id_listened_together=fake.uuid4_string(), fans_share=decimal.Decimal("0.42"), ) fans_share_3 = create_reporting_model( FansByArtistAccountOtherArtistDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, global_participant_id_listened_together=fake.uuid4_string(), fans_share=decimal.Decimal("0.1"), ) response = client.get( f"/artists/{global_participant_id}/fans-also-listening", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == { "items": [ { "globalParticipantId": fans_share_2.global_participant_id_listened_together, "fansShare": float(fans_share_2.fans_share), }, { "globalParticipantId": fans_share_1.global_participant_id_listened_together, "fansShare": float(fans_share_1.fans_share), }, { "globalParticipantId": fans_share_3.global_participant_id_listened_together, "fansShare": float(fans_share_3.fans_share), }, ], } @pytest.mark.db def test_get_artists( client: TestClient, create_reporting_model: CreateReportingModel, account: Account ) -> None: artist = create_reporting_model( FansByArtistAccountDbt, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = client.get("/artists") assert response.status_code == 200 assert response.json() == { "total": 1, "items": [ { "name": artist.global_participant_name, "globalParticipantId": artist.global_participant_id, "labels": [ { "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, } ], } ], "limit": 50, "offset": 0, } @pytest.mark.db def test_get_artists_limit( client: TestClient, create_reporting_model: CreateReportingModel, account: Account ) -> None: create_reporting_model( FansByArtistAccountDbt, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) create_reporting_model( FansByArtistAccountDbt, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = client.get("/artists", params={"limit": 1}) assert response.status_code == 200 assert len(response.json()["items"]) == 1 assert response.json()["total"] == 2 assert response.json()["limit"] == 1 @pytest.mark.db def test_get_artists_ordering( client: TestClient, create_reporting_model: CreateReportingModel, account: Account, ) -> None: artist_1 = create_reporting_model( FansByArtistAccountDbt, global_participant_name="bbb", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) artist_2 = create_reporting_model( FansByArtistAccountDbt, global_participant_name="ccc", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) artist_3 = create_reporting_model( FansByArtistAccountDbt, global_participant_name="aaa", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = client.get("/artists") assert response.status_code == 200 assert response.json() == { "total": 3, "items": [ { "name": artist_3.global_participant_name, "globalParticipantId": artist_3.global_participant_id, "labels": [ { "vendorId": artist_3.vendor_id, "subaccountId": artist_3.subaccount_id, } ], }, { "name": artist_1.global_participant_name, "globalParticipantId": artist_1.global_participant_id, "labels": [ { "vendorId": artist_1.vendor_id, "subaccountId": artist_1.subaccount_id, } ], }, { "name": artist_2.global_participant_name, "globalParticipantId": artist_2.global_participant_id, "labels": [ { "vendorId": artist_2.vendor_id, "subaccountId": artist_2.subaccount_id, } ], }, ], "limit": 50, "offset": 0, } @pytest.mark.db def test_get_artists_offset( client: TestClient, create_reporting_model: CreateReportingModel, account: Account, ) -> None: artist_1 = create_reporting_model( FansByArtistAccountDbt, global_participant_name="bbb", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) create_reporting_model( FansByArtistAccountDbt, global_participant_name="aaa", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) artist_3 = create_reporting_model( FansByArtistAccountDbt, global_participant_name="ccc", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = client.get("/artists", params={"offset": 1}) assert response.status_code == 200 assert response.json() == { "total": 3, "items": [ { "name": artist_1.global_participant_name, "globalParticipantId": artist_1.global_participant_id, "labels": [ { "vendorId": artist_1.vendor_id, "subaccountId": artist_1.subaccount_id, } ], }, { "name": artist_3.global_participant_name, "globalParticipantId": artist_3.global_participant_id, "labels": [ { "vendorId": artist_3.vendor_id, "subaccountId": artist_3.subaccount_id, } ], }, ], "limit": 50, "offset": 1, } @pytest.mark.db def test_get_artist_segment_share_by_categories( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() create_reporting_model( FansByArtistAccountCategoryDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.FANS_TO_WIN_BACK, segment_fans_count=100, category=SegmentCategory.SIGN_UP, fans_share=0.75, ) create_reporting_model( FansByArtistAccountCategoryDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.FANS_TO_WIN_BACK, segment_fans_count=100, category=SegmentCategory.HEAVY_LISTENING, fans_share=0.25, ) create_reporting_model( FansByArtistAccountCategoryDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.CASUAL_FANS, segment_fans_count=200, category=SegmentCategory.SIGN_UP, fans_share=0.25, ) create_reporting_model( FansByArtistAccountCategoryDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.CASUAL_FANS, segment_fans_count=200, category=SegmentCategory.HEAVY_LISTENING, fans_share=0.75, ) response = client.get( f"/artists/{global_participant_id}/segments-activities", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, }, ) assert response.status_code == 200 assert response.json() == [ { "id": "SUPER_FANS", "count": 0, "items": [ {"label": "SIGN_UP", "value": 0.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "ENGAGED_FANS", "count": 0, "items": [ {"label": "SIGN_UP", "value": 0.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "CASUAL_FANS", "count": 200, "items": [ {"label": "SIGN_UP", "value": 0.25}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.75}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "FANS_TO_WIN_BACK", "count": 100, "items": [ {"label": "SIGN_UP", "value": 0.75}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.25}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "NEW_FANS", "count": 0, "items": [ {"label": "SIGN_UP", "value": 0.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, ] @pytest.mark.db def test_get_artist_segment_share_by_categories_country( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() create_reporting_model( FansByArtistAccountCategoryCountryDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.FANS_TO_WIN_BACK, segment_fans_count=300, category=SegmentCategory.HEAVY_LISTENING, fans_share=1.0, fans_count=100, country_iso2="US", ) create_reporting_model( FansByArtistAccountCategoryCountryDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, segment_name=FanSegment.FANS_TO_WIN_BACK, segment_fans_count=300, category=SegmentCategory.SIGN_UP, fans_share=1.0, fans_count=200, country_iso2="GB", ) response = client.get( f"/artists/{global_participant_id}/segments-activities", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "countries": ["GB"], }, ) assert response.status_code == 200 assert response.json() == [ { "id": "SUPER_FANS", "count": 0, "items": [ {"label": "SIGN_UP", "value": 0.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "ENGAGED_FANS", "count": 0, "items": [ {"label": "SIGN_UP", "value": 0.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "CASUAL_FANS", "count": 0, "items": [ {"label": "SIGN_UP", "value": 0.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "FANS_TO_WIN_BACK", "count": 300, "items": [ {"label": "SIGN_UP", "value": 1.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, { "id": "NEW_FANS", "count": 0, "items": [ {"label": "SIGN_UP", "value": 0.0}, {"label": "CAMPAIGN_PARTICIPATION", "value": 0.0}, {"label": "EMAIL_CAMPAIGN", "value": 0.0}, {"label": "SMS_CAMPAIGN", "value": 0.0}, {"label": "LISTENING", "value": 0.0}, {"label": "FOLLOW_ARTIST", "value": 0.0}, {"label": "ADDED_ARTISTS_MUSIC", "value": 0.0}, {"label": "HEAVY_LISTENING", "value": 0.0}, {"label": "MERCH", "value": 0.0}, {"label": "CONTEST", "value": 0.0}, {"label": "CONCERT_TICKET", "value": 0.0}, {"label": "LIVE_STREAMING", "value": 0.0}, {"label": "FAN_CLUB", "value": 0.0}, ], }, ] def test_get_artist_segments_explained( client: TestClient, container: Container, fake: FakerTyped ) -> None: global_participant_id = fake.uuid4_string() language = "en" handler_mock = mock.MagicMock(spec=GetArtistSegmentsExplainedHandler) handler_mock.handle.return_value = [ FansSegmentExplained( segment=FanSegment.CASUAL_FANS, explanation="This fan segment is highly engaged with the artist", ) ] with container.override( GetArtistSegmentsExplainedHandler, handler_mock, ): response = client.get( f"/artists/{global_participant_id}/explain-segments", params={ "language": language, }, ) assert response.status_code == 200 assert response.json() == [ { "segment": "CASUAL_FANS", "explanation": "This fan segment is highly engaged with the artist", }, ] @pytest.mark.db def test_get_artist_fans_count_by_campaign_timeseries( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() campaign_id = "campaign A" campaign2_id = "campaign B" campaign_name = fake.pystr() campaign2_name = fake.pystr() date = datetime.date(year=2025, month=10, day=13) create_reporting_model( FansByArtistAccountAcquisitionCampaignDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquisition_channel=AcquisitionChannel.SMF_GENERAL_PRESAVE, acquisition_campaign_id=campaign_id, acquisition_campaign=campaign_name, acquired_at_date=date, daily_running_total_fans_count=100, ) create_reporting_model( FansByArtistAccountAcquisitionCampaignDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquisition_channel=AcquisitionChannel.SMF_GENERAL_PRESAVE, acquisition_campaign_id=campaign_id, acquisition_campaign=campaign_name, acquired_at_date=date + datetime.timedelta(days=2), daily_running_total_fans_count=150, ) create_reporting_model( FansByArtistAccountAcquisitionCampaignDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquisition_channel=AcquisitionChannel.SHOPIFY, acquisition_campaign_id=campaign2_id, acquisition_campaign=campaign2_name, acquired_at_date=date + datetime.timedelta(days=2), daily_running_total_fans_count=50, ) response = client.get( f"/artists/{global_participant_id}/timeseries", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (date - datetime.timedelta(days=3)).isoformat(), "endDate": (date + datetime.timedelta(days=3)).isoformat(), "type": ArtistFanDataType.ACQUISITION_CAMPAIGN, }, ) assert response.status_code == 200 assert response.json() == [ { "id": campaign_id, "name": campaign_name, "channel": AcquisitionChannel.SMF_GENERAL_PRESAVE.value, "items": [ {"date": "2025-10-10", "value": 0}, {"date": "2025-10-11", "value": 0}, {"date": "2025-10-12", "value": 0}, {"date": "2025-10-13", "value": 100}, {"date": "2025-10-14", "value": 100}, {"date": "2025-10-15", "value": 150}, {"date": "2025-10-16", "value": 150}, ], }, { "id": campaign2_id, "name": campaign2_name, "channel": AcquisitionChannel.SHOPIFY.value, "items": [ {"date": "2025-10-10", "value": 0}, {"date": "2025-10-11", "value": 0}, {"date": "2025-10-12", "value": 0}, {"date": "2025-10-13", "value": 0}, {"date": "2025-10-14", "value": 0}, {"date": "2025-10-15", "value": 50}, {"date": "2025-10-16", "value": 50}, ], }, { "id": "TOTALS", "name": None, "channel": None, "items": [ {"date": "2025-10-10", "value": 0}, {"date": "2025-10-11", "value": 0}, {"date": "2025-10-12", "value": 0}, {"date": "2025-10-13", "value": 100}, {"date": "2025-10-14", "value": 100}, {"date": "2025-10-15", "value": 200}, {"date": "2025-10-16", "value": 200}, ], }, ] @pytest.mark.db def test_get_artist_fans_count_by_acquisition_campaign_summary( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id = fake.uuid4_string() campaign_id = fake.pystr() campaign_name = fake.pystr() campaign_url = fake.url() acquired_date = datetime.date(2025, 1, 1) create_reporting_model( FansByArtistAccountAcquisitionCampaignDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquisition_channel=AcquisitionChannel.SMF_FAN_CARD_ACTIVATION, acquisition_campaign_id=campaign_id, acquisition_campaign=campaign_name, campaign_url=campaign_url, acquired_at_date=acquired_date, daily_running_total_fans_count=100, ) create_reporting_model( FansByArtistAccountAcquisitionCampaignDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquisition_channel=AcquisitionChannel.SMF_FAN_CARD_ACTIVATION, acquisition_campaign_id=campaign_id, acquisition_campaign=campaign_name, campaign_url=campaign_url, acquired_at_date=acquired_date + datetime.timedelta(days=3), daily_running_total_fans_count=125, ) create_reporting_model( FansByArtistAccountAcquisitionCampaignDailyDbt, vendor_id=vendor_id, subaccount_id=subaccount_id, global_participant_id=global_participant_id, acquisition_channel=AcquisitionChannel.SMF_FAN_CARD_ACTIVATION, acquisition_campaign_id=campaign_id, acquisition_campaign=campaign_name, campaign_url=campaign_url, acquired_at_date=acquired_date + datetime.timedelta(days=12), daily_running_total_fans_count=150, ) response = client.get( f"/artists/{global_participant_id}/summary-by-campaign", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "startDate": (acquired_date - datetime.timedelta(days=2)).isoformat(), "endDate": (acquired_date + datetime.timedelta(days=8)).isoformat(), }, ) assert response.status_code == 200 assert response.json() == [ { "id": campaign_id, "name": campaign_name, "url": campaign_url, "channel": AcquisitionChannel.SMF_FAN_CARD_ACTIVATION.value, "value": 125, "change": None, "createdAt": acquired_date.isoformat(), }, ] @pytest.mark.db def test_get_participants( client: TestClient, create_reporting_model: CreateReportingModel, account: Account, faker: faker.Faker, ) -> None: global_participant_id = faker.uuid4() create_reporting_model( ArtistRosterMainRep, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=global_participant_id, ) create_reporting_model( GlobalFansByArtistDbt, global_participant_id=global_participant_id, global_participant_name="Test Artist", ) create_reporting_model( GlobalParticipantAccountDbt, id=global_participant_id, spotify_id="spotify:artist:test123", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) virtual_participant = create_reporting_model( VirtualParticipant, name="Virtual Test Artist", spotify_id="spotify:artist:virtual123", crm_mailing_list_id="a0STy000007iGg9MAE", ) create_reporting_model( CustomList, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, virtual_participant_id=virtual_participant.id, ) response = client.get( "/artists/participants", params={ "vendorId": account.vendor_id, "subaccountId": account.subaccount_id, }, ) assert response.status_code == 200 data = response.json() assert data["total"] == 2 assert len(data["items"]) == 2 global_artist = next( (p for p in data["items"] if p["spotifyId"] == "spotify:artist:test123"), None ) assert global_artist is not None assert global_artist["name"] == "Test Artist" assert global_artist["virtualParticipantId"] is None assert global_artist["crmMailingListId"] is None assert isinstance(global_artist["labels"], list) assert len(global_artist["labels"]) == 1 assert global_artist["labels"][0]["vendorId"] == account.vendor_id assert global_artist["labels"][0]["subaccountId"] == account.subaccount_id virtual_artist = next( ( p for p in data["items"] if p["virtualParticipantId"] == virtual_participant.id ), None, ) assert virtual_artist is not None assert virtual_artist["name"] == "Virtual Test Artist" assert virtual_artist["spotifyId"] == "spotify:artist:virtual123" assert virtual_artist["crmMailingListId"] == "a0STy000007iGg9MAE" assert isinstance(virtual_artist["labels"], list) assert len(virtual_artist["labels"]) == 1