diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index acf01f55..ed669e99 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -8282,6 +8282,8 @@ paths: - track_added_to_purchased_album - request_manager - approve_manager_request + - track_collaborator_invite + - track_collaborator_accept - claimable_reward - comment - comment_thread @@ -17078,6 +17080,7 @@ components: - $ref: "#/components/schemas/usdc_purchase_buyer_notification" - $ref: "#/components/schemas/request_manager_notification" - $ref: "#/components/schemas/approve_manager_request_notification" + - $ref: "#/components/schemas/track_collaborator_notification" - $ref: "#/components/schemas/trending_notification" - $ref: "#/components/schemas/trending_playlist_notification" - $ref: "#/components/schemas/trending_underground_notification" @@ -17125,6 +17128,8 @@ components: usdc_purchase_buyer: "#/components/schemas/usdc_purchase_buyer_notification" request_manager: "#/components/schemas/request_manager_notification" approve_manager_request: "#/components/schemas/approve_manager_request_notification" + track_collaborator_invite: "#/components/schemas/track_collaborator_notification" + track_collaborator_accept: "#/components/schemas/track_collaborator_notification" trending: "#/components/schemas/trending_notification" trending_playlist: "#/components/schemas/trending_playlist_notification" trending_underground: "#/components/schemas/trending_underground_notification" @@ -17412,6 +17417,29 @@ components: type: array items: $ref: "#/components/schemas/approve_manager_request_notification_action" + track_collaborator_notification: + required: + - actions + - group_id + - is_seen + - type + type: object + properties: + type: + type: string + enum: + - track_collaborator_invite + - track_collaborator_accept + group_id: + type: string + is_seen: + type: boolean + seen_at: + type: integer + actions: + type: array + items: + $ref: "#/components/schemas/track_collaborator_notification_action" fan_remix_contest_ended_notification_action: required: - data @@ -17493,6 +17521,22 @@ components: type: integer data: $ref: "#/components/schemas/approve_manager_request_notification_action_data" + track_collaborator_notification_action: + required: + - data + - specifier + - timestamp + - type + type: object + properties: + specifier: + type: string + type: + type: string + timestamp: + type: integer + data: + $ref: "#/components/schemas/track_collaborator_notification_action_data" comment_mention_notification: required: - actions @@ -18658,6 +18702,26 @@ components: type: string grantee_address: type: string + track_collaborator_notification_action_data: + required: + - collaborator_user_id + - inviter_user_id + - track_id + type: object + properties: + track_id: + type: string + inviter_user_id: + type: string + collaborator_user_id: + type: string + status: + type: string + description: Current collaborator invite status at response time. + enum: + - pending + - accepted + - rejected save_of_repost_notification_action: required: - data diff --git a/api/v1_notifications.go b/api/v1_notifications.go index 2c8f8743..6543977d 100644 --- a/api/v1_notifications.go +++ b/api/v1_notifications.go @@ -74,12 +74,17 @@ SELECT n.group_id AS group_id, json_agg( json_build_object( - 'type', type, - 'specifier', specifier, - 'timestamp', EXTRACT(EPOCH FROM timestamp), - 'data', data + 'type', n.type, + 'specifier', n.specifier, + 'timestamp', EXTRACT(EPOCH FROM n.timestamp), + 'data', + CASE + WHEN n.type = 'track_collaborator_invite' AND tc.status IS NOT NULL + THEN jsonb_set(n.data, '{status}', to_jsonb(tc.status), true) + ELSE n.data + END ) - ORDER BY timestamp DESC + ORDER BY n.timestamp DESC )::jsonb AS actions, CASE -- If seen at is not null, we were able to match a window between seen events @@ -113,6 +118,12 @@ LEFT JOIN playlists p ON n.data ? 'playlist_id' AND p.playlist_id = (n.data->>'playlist_id')::integer AND p.is_current = true +LEFT JOIN track_collaborators tc ON + n.type = 'track_collaborator_invite' AND + n.data ? 'track_id' AND + n.data ? 'collaborator_user_id' AND + tc.track_id = (n.data->>'track_id')::integer AND + tc.collaborator_user_id = (n.data->>'collaborator_user_id')::integer WHERE (ARRAY[@user_id] && n.user_ids) AND (n.type = ANY(@types) OR @types IS NULL) diff --git a/api/v1_notifications_test.go b/api/v1_notifications_test.go index 5e7b5ffd..4ca6f196 100644 --- a/api/v1_notifications_test.go +++ b/api/v1_notifications_test.go @@ -1,8 +1,10 @@ package api import ( + "context" "strconv" "testing" + "time" "api.audius.co/database" "api.audius.co/trashid" @@ -463,10 +465,59 @@ func TestV1Notifications_AnnouncementRequiresUserIdInUserIds(t *testing.T) { assert.Equal(t, 200, status) jsonAssert(t, body, map[string]any{ - "data.notifications.#": 1, - "data.notifications.0.type": "announcement", - "data.notifications.0.group_id": "announcement:target-user-1", - "data.notifications.0.actions.0.data.title": "For user 1", + "data.notifications.#": 1, + "data.notifications.0.type": "announcement", + "data.notifications.0.group_id": "announcement:target-user-1", + "data.notifications.0.actions.0.data.title": "For user 1", + }) +} + +func TestV1Notifications_TrackCollaboratorInviteIncludesCurrentStatus(t *testing.T) { + app := emptyTestApp(t) + ctx := context.Background() + + const trackID = 700 + const collaboratorUserID = 1 + const inviterUserID = 500 + + invitedAt := time.Now() + _, err := app.pool.Replicas[0].Exec(ctx, ` + INSERT INTO track_collaborators ( + track_id, + collaborator_user_id, + invited_by, + status, + created_at, + updated_at, + txhash + ) + VALUES ($1, $2, $3, 'pending', $4, $4, 'invite-tx') + `, trackID, collaboratorUserID, inviterUserID, invitedAt) + assert.NoError(t, err) + + _, err = app.pool.Replicas[0].Exec(ctx, ` + UPDATE track_collaborators + SET status = 'accepted', updated_at = $3 + WHERE track_id = $1 AND collaborator_user_id = $2 + `, trackID, collaboratorUserID, invitedAt.Add(time.Second)) + assert.NoError(t, err) + + status, body := testGet( + t, + app, + "/v1/notifications/"+ + trashid.MustEncodeHashID(collaboratorUserID)+ + "?types=track_collaborator_invite", + ) + assert.Equal(t, 200, status) + + jsonAssert(t, body, map[string]any{ + "data.notifications.#": 1, + "data.notifications.0.type": "track_collaborator_invite", + "data.notifications.0.actions.0.data.track_id": trashid.MustEncodeHashID(trackID), + "data.notifications.0.actions.0.data.collaborator_user_id": trashid.MustEncodeHashID(collaboratorUserID), + "data.notifications.0.actions.0.data.inviter_user_id": trashid.MustEncodeHashID(inviterUserID), + "data.notifications.0.actions.0.data.status": "accepted", }) } @@ -582,4 +633,3 @@ func TestV1Notifications_RelatedEntities(t *testing.T) { assert.Contains(t, gotUserIds, trashid.MustEncodeHashID(saver), "saver must appear in related.users") } -