diff --git a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_fan_club_post.py b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_fan_club_post.py index 10613de269f..32cee955353 100644 --- a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_fan_club_post.py +++ b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_fan_club_post.py @@ -689,3 +689,166 @@ def test_fan_club_post_and_track_comments_coexist(app, mocker): ).all() assert len(notifs) == 1 assert notifs[0].group_id == "comment:1:type:Track" + + +# --------------------------------------------------------------------------- +# FAN CLUB TEXT POST NOTIFICATION +# --------------------------------------------------------------------------- + + +def test_fan_club_text_post_notification_sent_to_followers(app, mocker): + """ + When an artist creates a root-level fan club text post, each follower + receives a fan_club_text_post notification. + """ + entities_with_follows = { + **fan_club_post_entities, + "follows": [ + {"follower_user_id": 2, "followee_user_id": 1}, + {"follower_user_id": 3, "followee_user_id": 1}, + ], + } + + tx_receipts = { + "ArtistFanClubPost": [ + { + "args": AttributeDict( + { + "_entityId": 1, + "_entityType": "Comment", + "_userId": 1, + "_action": "Create", + "_metadata": f'{{"cid": "", "data": {fan_club_post_metadata_json}}}', + "_signer": "user1wallet", + } + ) + }, + ], + } + + db, index_transaction = setup_test(app, mocker, entities_with_follows, tx_receipts) + + with db.scoped_session() as session: + index_transaction(session) + + # Should create one notification per follower + notifs = ( + session.query(Notification) + .filter(Notification.type == "fan_club_text_post") + .all() + ) + assert len(notifs) == 2 + + notif_user_ids = sorted([n.user_ids[0] for n in notifs]) + assert notif_user_ids == [2, 3] + + # Verify notification data + for notif in notifs: + assert notif.data["entity_user_id"] == 1 + assert notif.data["comment_id"] == 1 + assert notif.group_id == "fan_club_text_post:1:user:1" + + # No "comment" notification since artist is posting on their own fan club + comment_notifs = ( + session.query(Notification) + .filter(Notification.type == "comment") + .all() + ) + assert len(comment_notifs) == 0 + + +def test_fan_club_text_post_notification_not_sent_for_replies(app, mocker): + """ + Replies within a fan club thread should NOT send fan_club_text_post + notifications (only root-level posts do). + """ + reply_entities = { + **fan_club_post_entities, + "follows": [ + {"follower_user_id": 2, "followee_user_id": 1}, + ], + "comments": [ + { + "comment_id": 1, + "user_id": 1, + "entity_id": 1, + "entity_type": "FanClub", + } + ], + } + + reply_metadata = json.dumps({ + **fan_club_post_metadata, + "body": "replying to my own post", + "parent_comment_id": 1, + }) + + tx_receipts = { + "ArtistReply": [ + { + "args": AttributeDict( + { + "_entityId": 2, + "_entityType": "Comment", + "_userId": 1, + "_action": "Create", + "_metadata": f'{{"cid": "", "data": {reply_metadata}}}', + "_signer": "user1wallet", + } + ) + }, + ], + } + + db, index_transaction = setup_test(app, mocker, reply_entities, tx_receipts) + + with db.scoped_session() as session: + index_transaction(session) + + # No fan_club_text_post notifications for replies + notifs = ( + session.query(Notification) + .filter(Notification.type == "fan_club_text_post") + .all() + ) + assert len(notifs) == 0 + + +def test_fan_club_text_post_notification_no_followers(app, mocker): + """ + If the artist has no followers, no fan_club_text_post notifications + are created (but the post itself is still indexed). + """ + tx_receipts = { + "ArtistPostNoFollowers": [ + { + "args": AttributeDict( + { + "_entityId": 1, + "_entityType": "Comment", + "_userId": 1, + "_action": "Create", + "_metadata": f'{{"cid": "", "data": {fan_club_post_metadata_json}}}', + "_signer": "user1wallet", + } + ) + }, + ], + } + + db, index_transaction = setup_test(app, mocker, fan_club_post_entities, tx_receipts) + + with db.scoped_session() as session: + index_transaction(session) + + # Post is created + comments = session.query(Comment).all() + assert len(comments) == 1 + + # No notifications + notifs = ( + session.query(Notification) + .filter(Notification.type == "fan_club_text_post") + .all() + ) + assert len(notifs) == 0 diff --git a/packages/discovery-provider/src/tasks/entity_manager/entities/comment.py b/packages/discovery-provider/src/tasks/entity_manager/entities/comment.py index cb75a7371e7..bf6f5d0bca4 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/comment.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/comment.py @@ -10,6 +10,7 @@ from src.models.comments.comment_thread import CommentThread from src.models.moderation.muted_user import MutedUser from src.models.notifications.notification import Notification +from src.models.social.follow import Follow from src.models.tracks.track import Track from src.models.users.aggregate_user import AggregateUser from src.models.users.user import User @@ -282,6 +283,33 @@ def create_comment(params: ManageEntityParameters): ) safe_add_notification(params.session, comment_notification) + + # Notify followers when an artist creates a root-level fan club text post + if entity_type == FAN_CLUB_ENTITY_TYPE and not is_reply: + follower_user_ids = ( + params.session.query(Follow.follower_user_id) + .filter( + Follow.followee_user_id == entity_user_id, + Follow.is_current == True, + Follow.is_delete == False, + ) + .all() + ) + for (follower_id,) in follower_user_ids: + fan_club_notification = Notification( + blocknumber=params.block_number, + user_ids=[follower_id], + timestamp=params.block_datetime, + type="fan_club_text_post", + specifier=str(follower_id), + group_id=f"fan_club_text_post:{comment_id}:user:{entity_user_id}", + data={ + "entity_user_id": entity_user_id, + "comment_id": comment_id, + }, + ) + safe_add_notification(params.session, fan_club_notification) + if mentions: mention_mutes = ( params.session.query(MutedUser)