feat(catalogs): GET /api/catalogs/measurements — per-ISRC latest playcounts + derived valuation band (chat#1850)
What
Implements GET /api/catalogs/measurements?catalogId= per the merged-first docs contract recoupable/docs#265 — the data plumbing for the Catalog HQ homepage hero in recoupable/chat#1850 (deferred from chat#1801 / chat#1803).
Merge order: docs#265 (contract) → this PR. Base is main per api#750 (the test-branch PR flow is retired; repo CLAUDE.md says never target test).
How it works
- Auth / no IDOR: account resolved via
validateAuthContext(Privy bearer orx-api-key); ownership checked throughaccount_catalogs— a catalog that doesn't exist or belongs to another account is an indistinguishable 404. - Measurements:
catalog_songs→ songs (isrc + title) →song_measurementsfiltered tospotify/platform_displayed_play_count, reduced to the newest capture per ISRC (latestMeasurementsPerIsrc), sorted playcount-desc. Never-measured songs are omitted (per contract). - Valuation band
{ low, mid, high }: derived at read time (not persisted, per the chat#1850 architecture decision) inlib/catalog/computeValuationBand.ts, mirroring the marketing card's exact model (marketing/lib/valuation/computeCatalogValuation.ts+nlsBandFromSpotifyGross.ts): annual run-rate = lifetime streams ÷ catalog age (lifetime-average proxy) → NLS ($0.0035/stream × 1.25/1.4/1.6 gross-up × 0.85 × 0.75) → 10×/13×/16× multiples. - Catalog age: release dates aren't persisted anywhere in the DB, so
getCatalogEarliestReleaseDatereadsalbum_idsoff the catalog's newestplaycount_snapshotsrow and fetches release dates from Spotify (new batchedlib/spotify/getAlbums.ts, 20 ids/request, ~2 requests for a typical catalog). Best-effort: any gap falls back to the marketing card's documenteddefault_5yage, surfaced ascatalog_age_yearsin the response. This is the one input that can differ from a given marketing run (e.g. ISRC-scoped snapshots have noalbum_ids) — documented rather than faked.
Files
Route app/api/catalogs/measurements/route.ts → handler lib/catalog/getCatalogMeasurementsHandler.ts → validate (validateGetCatalogMeasurementsQuery, zod uuid) → data fns (lib/supabase/account_catalogs/selectAccountCatalog.ts, lib/supabase/catalog_songs/selectCatalogSongTitles.ts, existing selectSongMeasurements + selectPlaycountSnapshots extended with a catalog filter) → pure shaping (latestMeasurementsPerIsrc, computeValuationBand). One exported function per file; all files < 100 LOC.
Testing (TDD red → green per unit)
- 6 new test files + 1 extended, 22 new tests; full suite: 713 files / 3923 tests pass.
pnpm exec tsc --noEmit: no new errors (200 pre-existing onmain, all in test files excluded fromnext build; identical count with this branch).eslint --fixclean;prettier --checkclean.- Preview verification results to follow in a PR comment.
Error paths (as documented in the contract)
400 missing/invalid catalogId · 401 unauthed · 404 unknown/foreign catalog · 500 unexpected.
🤖 Generated with Claude Code
Summary by cubic
Adds GET /api/catalogs/measurements?catalogId= to return latest per-ISRC Spotify playcounts plus a read-time valuation band for the catalog. Secures access by account and matches the marketing card’s valuation model.
- New Features
- Latest
platform_displayed_play_countper ISRC fromsong_measurements(sorted desc); unmeasured songs omitted. - Auth + ownership enforced via
validateAuthContextandaccount_catalogs; foreign/unknown catalogs return 404. - Returns
{ measurements, valuation: { low, mid, high }, total_streams, catalog_age_years }. - Catalog age pulled from Spotify album release dates on the newest snapshot (batched
getAlbums, 20 ids/request) with a 5y fallback. - CORS preflight supported; errors: 400 invalid
catalogId, 401 unauthenticated, 404 not found, 500 on unexpected errors.
- Latest
Summary by CodeRabbit
-
New Features
- Added a catalog measurements API endpoint that returns measurement data and derived valuation insights.
- Added support for resolving a catalog’s earliest release date and calculating a valuation band from stream data.
- Improved album and track measurement aggregation, including total streams and sorted latest results.
-
Bug Fixes
- Added request validation and clearer error handling for catalog measurements requests.
- Added CORS support for preflight requests on the new measurements endpoint.
- Expanded catalog filtering so playcount snapshot queries can be narrowed by catalog.