feat(notifications): tastemaker + trending notification triggers#870
Merged
Conversation
129730e to
cc2b9fd
Compare
Closes the notification side of the tastemaker (t) and trending
(tt/tut/tp) challenge ports. Phase 1+2 processors mint user_challenges
rows but only handle_user_challenges.sql's generic claimable_reward /
challenge_reward notifications fired — the type-specific tastemaker /
trending / trending_underground / trending_playlist notifications
that apps' index_tastemaker.py and index_trending.py created had no
Go equivalent.
Both new triggers fire AFTER INSERT on user_challenges with a WHEN
clause filtered to their challenge_id. Re-runs hit UpsertUserChallenge's
ON CONFLICT DO UPDATE branch (no AFTER INSERT) so each row's
notification mints exactly once.
handle_tastemaker.sql (challenge_id='t')
- Parses track_id from the processor's specifier "<hex_uid>:t:<hex_tid>"
- Looks up tracks.owner_id and infers repost-vs-save action (repost
wins, matching apps' dedupe_notifications_by_group_id)
- Emits notification with group_id
"tastemaker_user_id:<uid>:tastemaker_item_id:<tid>", specifier=tid,
and data { tastemaker_item_id, tastemaker_item_type:'track',
tastemaker_item_owner_id, action, tastemaker_user_id } — verbatim
apps' Notification(type='tastemaker') shape
handle_trending.sql (challenge_id in 'tt','tut','tp')
- Parses week + rank from "<YYYY-MM-DD>:<rank>"
- Looks up entity_id from trending_results (same processor wrote it
earlier in the same transaction)
- Routes to 'trending' / 'trending_underground' / 'trending_playlist',
swapping the track_id/playlist_id label in group_id and data —
matches apps' index_trending_notifications, ditto for underground +
playlist variants
- Idempotency comes from the AFTER INSERT (not UPDATE) gate plus
the unique (group_id, specifier) constraint on notification
Schema dump regeneration follows in a separate commit (cf. 4da78ab
for the handle_comment_remix_contest_update precedent).
Tests:
- TestTastemaker_EmitsNotification — verifies notification row shape;
asserts repost wins when a user has both a repost and a save
- TestTrending_EmitsNotification — Friday-gated; asserts the rank-1
notification carries track_id, rank=1, and the expected group_id
- TestTrendingPlaylist_EmitsNotification — playlist variant carries
playlist_id (not track_id) in data and group_id
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The trending playlist reward (challenge_id 'tp') is being removed as a product feature, so handle_trending no longer needs to emit `trending_playlist` notifications. Scope: just this PR's trigger. PR #835's NewTrendingPlaylistProcessor + 'tp' catalog seed are still in place — harmless if the upstream feature stays inactive, can be torn out separately if desired. Changes: - handle_trending.sql: drop the 'tp' case from the type switch, WHEN clause, and data_jsonb branch. Trigger now only handles 'tt' and 'tut' (both tracks), so the entity_label variable goes away too. - trending_test.go: remove TestTrendingPlaylist_EmitsNotification. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds handle_trending and handle_tastemaker functions and their on_*_user_challenge triggers to sql/01_schema.sql so the test-schema template includes them and the trending/tastemaker notification tests pass. Stacked on the comment-notifications dump regen. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cc2b9fd to
fead757
Compare
4 tasks
raymondjacobson
added a commit
that referenced
this pull request
May 29, 2026
## Summary Adds the three timer-driven notification creators the legacy Python discovery-provider celery beat produced, as scheduled parity jobs in the `core-indexer`. This closes the remaining functional gap between the vendored Go ETL indexer and the Python indexer for notifications: event-driven notifications are already handled by DB triggers (#851/#870), and these cover the ones that fire on elapsed time rather than an indexed entity. - **EngagementNotificationsJob** (`claimable_reward`): promotes 7-day-cooldown challenges to `claimable_reward` once their cooldown elapses and they're still undisbursed. Complements the `handle_on_user_challenge` trigger, which only emits an immediate `claimable_reward` for `cooldown_days = 0` and `reward_in_cooldown` for `cooldown_days > 0`. Hence the `cooldown_days = 7` filter, matching Python's hardcoded check. Scheduled every 10m. - **ListenStreakReminderJob** (`listen_streak_reminder`): reminds users in the 42-43h window after their last listen (6h of slack before the 48h streak breaks). Tight 1-2m window under `env=stage` for end-to-end testing, matching Python. Scheduled every 10s. - **RemixContestNotificationsJob**: the four `fan`/`artist` `remix_contest` `ended` / `ending_soon` notifications, with the same audience rules (remixers, host followers, parent-track favoriters, event subscribers; host excluded from fan types). Scheduled every 30s. Each job is a set-based `INSERT ... SELECT ... ON CONFLICT (group_id, specifier) DO NOTHING` for idempotency via `uq_notification`, and is wired into `startParityJobs`. Mirrors the corresponding `apps/` Python tasks. ## Test plan - [x] `go test ./jobs/...` — new tests cover the engagement full pipeline + exclusions (cooldown/disbursement/not-elapsed/incomplete), listen-streak window boundaries + idempotency, and remix-contest ended/ending-soon audiences with host exclusion. - [x] Tests account for the `handle_on_user_challenge` and `handle_event` triggers that also fire on seed (verified no overlap with the asserted notification types). - [x] `go vet ./jobs/... ./indexer/...` clean; full `jobs` package passes. - [ ] Observe in stage that the jobs emit the expected notifications on the real schedule. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two notification triggers, split out of #841 (challenges Phase 2) where they were riding along out-of-scope and blocking that PR's CI:
ddl/functions/handle_trending.sql— fires AFTER INSERT onuser_challenges(trending challenge rows) to emit atrendingnotification to the track owner.ddl/functions/handle_tastemaker.sql— emits atastemakernotification.Plus their tests in
jobs/challenges/{trending,tastemaker}_test.go.These commits add the trigger functions but not the regeneration of
sql/01_schema.sql. The test harness (jobs/challenges/processor_test.go→withChallengesDB) builds thetest_jobstemplate from the committed schema dump and intentionally does not run the ddl runner. So without regenerating the dump, the triggers don't exist in the test DB and the two notif tests fail withno rows in result set/expected ... notif.To finish this PR:
then commit the regenerated
sql/01_schema.sql. After that thehandle_trending/handle_tastemakertriggers will be in the test template andTestTrending_EmitsNotification/TestTastemaker_EmitsNotificationwill pass.Why split from #841
#841 is challenges Phase 2 and is cutover-relevant (poll-based challenge processors). These notification triggers are a separate concern, were incomplete (missing the regen), and shouldn't gate the challenges/cutover work. Separated so each can land on its own merits.
Cutover note
If the production cutover (shutting off the Python discovery indexer) happens before this merges + deploys, tastemaker/trending notifications will have a gap for that window (notification triggers fire in real-time; no backfill). Coordinate accordingly.
🤖 Generated with Claude Code