Skip to content

feat(notifications): tastemaker + trending notification triggers#870

Merged
raymondjacobson merged 3 commits into
mainfrom
api/trending-tastemaker-notifs
May 29, 2026
Merged

feat(notifications): tastemaker + trending notification triggers#870
raymondjacobson merged 3 commits into
mainfrom
api/trending-tastemaker-notifs

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

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 on user_challenges (trending challenge rows) to emit a trending notification to the track owner.
  • ddl/functions/handle_tastemaker.sql — emits a tastemaker notification.

Plus their tests in jobs/challenges/{trending,tastemaker}_test.go.

⚠️ Needs schema-dump regen before CI is green

These commits add the trigger functions but not the regeneration of sql/01_schema.sql. The test harness (jobs/challenges/processor_test.gowithChallengesDB) builds the test_jobs template 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 with no rows in result set / expected ... notif.

To finish this PR:

make test-schema   # brings up fresh DB, runs migrations (incl. these ddl/functions), dumps to sql/01_schema.sql

then commit the regenerated sql/01_schema.sql. After that the handle_trending / handle_tastemaker triggers will be in the test template and TestTrending_EmitsNotification / TestTastemaker_EmitsNotification will 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

@raymondjacobson raymondjacobson force-pushed the api/trending-tastemaker-notifs branch from 129730e to cc2b9fd Compare May 29, 2026 19:35
raymondjacobson and others added 3 commits May 29, 2026 14:06
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>
@raymondjacobson raymondjacobson force-pushed the api/trending-tastemaker-notifs branch from cc2b9fd to fead757 Compare May 29, 2026 21:07
@raymondjacobson raymondjacobson merged commit d0f0bf4 into main May 29, 2026
5 checks passed
@raymondjacobson raymondjacobson deleted the api/trending-tastemaker-notifs branch May 29, 2026 21:14
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant