Skip to content

feat(jobs/challenges): Phase 2 — 7 more poll-based processors#841

Open
raymondjacobson wants to merge 3 commits into
api/challenges-phase-1from
api/challenges-phase-2
Open

feat(jobs/challenges): Phase 2 — 7 more poll-based processors#841
raymondjacobson wants to merge 3 commits into
api/challenges-phase-1from
api/challenges-phase-2

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

Summary

Stacked on #835 (Phase 1). Adds 7 more challenge processors. No new infra — all source tables already exist.

ID Source Notes
`c` first_weekly_comment `comments` grouped by ISO week One row per (user, ISO-year, ISO-week)
`cp` comment_pin `tracks.pinned_comment_id` + `users.is_verified` Commenter earns when verified track owner pins their (non-self) comment
`cs` cosign `remixes` + `reposts`/`saves` + `users.is_verified` Verified parent owner saves/reposts a remix → remixer earns. Cap: 5 cosigns per parent-owner per rolling 30 days. Currently `active=false` in catalog.
`t` tastemaker `track_trending_scores` (top-10) + `reposts`/`saves` Earliest 10 reposters + 10 savers per top-10 trending track. Top-N is 10 per direction (apps had 5).
`w` remix_contest_winner `events.event_data->>'winners'` JSONB Verified-host winners earn. Max 5 winners/contest, max 5 host rewards/rolling week.
`b` audio_matching_buyer `v_usdc_purchases` (incremental on slot) `amount = 1 × dollars` (dollars = micro-USDC / 1e6)
`s` audio_matching_seller same `amount = 5 × dollars`, gated on seller `is_verified`

Confirmations folded in

  • `cs` cap: 5 cosigns / 30 days ✓
  • `t` top-N: 10 (was 5 in apps) ✓
  • `w`: read winners directly from `events.event_data->>'winners'` JSONB ✓
  • `b`/`s` amount: catalog × dollars (b=1×, s=5×) ✓

Stack

```
#834 parity jobs
└─ #835 phase 1 challenges
└─ this PR — phase 2 challenges
```

Test plan

11 DB-backed tests in addition to the 12 from Phase 1:

  • `TestFirstWeeklyComment_OneRowPerUserPerWeek` — dedupe within week, separate weeks → separate rows
  • `TestCommentPin_VerifiedOwnerPinsOthersComment` — happy path
  • `TestCommentPin_SkippedWhenOwnerNotVerified` — gate enforced
  • `TestCommentPin_SkippedForSelfPin` — gate enforced
  • `TestCosign_VerifiedParentReposting` — happy path (activates the inactive catalog row for the test)
  • `TestCosign_MonthCap` — 6th cosign within 30d blocked
  • `TestTastemaker_EarliestRepostersAndSavers` — both action types earn
  • `TestRemixContestWinner_VerifiedHostMintsRows` — two winners → two rows
  • `TestRemixContestWinner_UnverifiedHostSkipped` — gate enforced
  • `TestAudioMatching_BuyerAndSeller` — both rows minted with correct amounts
  • `TestAudioMatching_SellerUnverifiedNoRow` — verified-seller gate
  • `TestAudioMatching_InvalidPurchaseExcluded` — `v_usdc_purchases` filters
  • All Phase 1 tests still pass (no regressions)
  • `go build ./...` clean

Next

PR B (signals endpoint + 5 Phase 3 processors) follows.

🤖 Generated with Claude Code

Adds 7 more challenge processors to api/jobs/challenges/ following the
same poll-based reconciliation pattern from Phase 1. No new infra; all
source tables already exist.

Processors:
  c   first_weekly_comment   one row per (user, ISO-week)
  cp  comment_pin            commenter earns when verified track owner
                             pins their comment (skips self-pin)
  cs  cosign                 verified parent owner saves/reposts a remix;
                             remixer earns; cap 5 cosigns per parent-owner
                             per rolling 30 days; currently inactive in catalog
  t   tastemaker             earliest 10 reposters + earliest 10 savers
                             of each top-10 trending track
  w   remix_contest_winner   winners of verified-host remix contest; max
                             5 winners per contest, max 5 winner rewards
                             per host per rolling week
  b   audio_matching_buyer   per USDC purchase; amount = 1 × dollars
  s   audio_matching_seller  same purchase; amount = 5 × dollars; gated
                             on seller verification

Audio matching reads from v_usdc_purchases (filtered to is_valid = TRUE);
checkpoint by sol_purchases.slot for incremental scanning.

Migration 0204 seeds the catalog rows from challenges.json with ON
CONFLICT DO UPDATE.

Tastemaker top-N bumped to 10 per direction (apps' historical value was
5). Per-track threshold of 10 reposters/savers kept as apps.

All 7 new processors registered in IndexChallengesJob (now 18 total).

Tests: 11 new DB-backed tests covering each processor's happy path and
its main gates (verified/unverified, cap enforcement, invalid purchases).
All passing against test_jobs template DB.
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>
@raymondjacobson
Copy link
Copy Markdown
Member Author

Adding the tastemaker + trending notification side that the processors didn't cover (closes the parity gap with apps' index_tastemaker.py + index_trending.py notification halves).

Pushed 3277ac0 — two new triggers on user_challenges AFTER INSERT, filtered by challenge_id via WHEN clauses. Pattern-matches handle_user_challenges.sql (sibling) and handle_comment_remix_contest_update.sql.

Notes

  • Schema dump regen pending follow-up commit (precedent: 4da78ab).
  • Trending notification tests are Friday-gated, same as the existing processor tests.

cc @raymondjacobson — flagging since you authored the parent PRs.

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>
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