feat(catalogs): GET /api/catalogs/measurements — per-ISRC latest playcounts + derived valuation band (chat#1850)

Merged
SW
4mo ago
+901-0

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 or x-api-key); ownership checked through account_catalogs — a catalog that doesn't exist or belongs to another account is an indistinguishable 404.
  • Measurements: catalog_songs → songs (isrc + title) → song_measurements filtered to spotify / 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) in lib/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 getCatalogEarliestReleaseDate reads album_ids off the catalog's newest playcount_snapshots row and fetches release dates from Spotify (new batched lib/spotify/getAlbums.ts, 20 ids/request, ~2 requests for a typical catalog). Best-effort: any gap falls back to the marketing card's documented default_5y age, surfaced as catalog_age_years in the response. This is the one input that can differ from a given marketing run (e.g. ISRC-scoped snapshots have no album_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 on main, all in test files excluded from next build; identical count with this branch).
  • eslint --fix clean; prettier --check clean.
  • 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_count per ISRC from song_measurements (sorted desc); unmeasured songs omitted.
    • Auth + ownership enforced via validateAuthContext and account_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.

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.