Skip to content

feat(catalogs): artist_account_id filter + no-cap pagination on GET /api/catalogs/measurements#763

Open
sweetmantech wants to merge 1 commit into
mainfrom
feat/catalog-measurements-artist-filter
Open

feat(catalogs): artist_account_id filter + no-cap pagination on GET /api/catalogs/measurements#763
sweetmantech wants to merge 1 commit into
mainfrom
feat/catalog-measurements-artist-filter

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Implements the data plumbing v2: artist-scoped valuation api item of recoupable/chat#1850 — the contract is recoupable/docs#267 (merge that first).

What changed (same read path as api#757, two behaviors)

1. Fix: the silent 1,000-row cap (P1)

The endpoint computed total_streams + the valuation band over at most 1,000 rows — the Supabase default limit — with no error. Live repro on a 2,679-song catalog: exactly 1,000 measurements / 4.83B streams / $39.3M mid vs SQL ground truth 2,679 / 16.31B / ≈$132.5M (3.4× undervalued): #757 (comment)

Now every read on the path paginates to exhaustion before dedupe/derivation:

  • lib/supabase/catalog_songs/selectCatalogSongTitles.ts.range() loop (the tracklist itself capped at 1,000)
  • lib/supabase/song_measurements/selectAllSongMeasurements.ts (new) — ISRC list chunked at 500 per query (bounded IN clause / URL length) and each chunk .range()-paginated to exhaustion, with a stable secondary order for deterministic pages
  • lib/supabase/song_artists/selectSongArtistIsrcs.ts (new) — .range() loop

2. Feature: optional artist_account_id query param

  • Validated as uuid (400 on malformed) in validateGetCatalogMeasurementsQuery
  • When present, the read covers only the catalog's songs linked to that artist account via song_artists (catalog_songs ∩ song_artists, computed in the new lib/catalog/resolveCatalogSongsInScope.ts); absent → whole catalog (unchanged behavior)
  • Response echoes the applied filter: new artist_account_id field (uuid when scoped, null when whole-catalog) so clients can verify the response scope before rendering artist-labeled numbers — pre-v2 deployments ignore the unknown param, and without the echo a client would show whole-catalog money under an artist label (the trust bug chat#1850 exists to kill)

Verification

  • TDD throughout — RED confirmed before GREEN for every unit (pagination via >1,000-row fixtures, chunking, intersection, validator, handler echo)
  • Full suite: 716 files / 3,945 tests passing
  • pnpm exec tsc --noEmit: 200 errors, all pre-existing in test files (0 new; 0 in touched files)
  • pnpm lint clean
  • Preview verification against the live [TEST] Full Roster Catalog (aggregate) fixture posted as a PR comment once the preview deploy is Ready

Merge order

recoupable/docs#267 (contract) → this PR → chat#1852 rework (hero consumes the filter + echo). Refs recoupable/chat#1850.

🤖 Generated with Claude Code


Summary by cubic

Adds an optional artist scope to GET /api/catalogs/measurements and removes the silent 1,000‑row cap by paginating all reads to exhaustion. Totals and valuation now cover all measured songs, and the response echoes the applied scope.

  • New Features

    • Add artist_account_id (UUID; 400 on malformed) to scope results to catalog_songs ∩ song_artists.
    • Echo artist_account_id in the response (UUID when scoped, null otherwise) so clients can verify the scope.
  • Bug Fixes

    • Remove the 1,000‑row cap: paginate catalog_songs, song_artists, and song_measurements to exhaustion; chunk song_measurements IN lists at 500 and use stable ordering for deterministic pages.
    • Totals and valuation reflect every measured song in the catalog or filtered set.

Written for commit 637d8b8. Summary will update on new commits.

Review in cubic

…api/catalogs/measurements (chat#1850)

Two fixes on the same read path (v2 of api#757):

- FIX the silent 1,000-row cap: the catalog tracklist, the measurement
  series, and the artist-link read all paginate to exhaustion with
  .range() loops (and the measurements IN clause is chunked at 500 ISRCs
  to stay under URL limits) BEFORE dedupe/derivation, so total_streams
  and the valuation band cover every measured song. Live repro was a
  2,679-song catalog valued 3.4x low off exactly 1,000 rows.

- ADD optional artist_account_id (uuid, 400 on malformed): scopes
  measurements + valuation to the catalog's songs linked to that artist
  account via song_artists (catalog_songs ∩ song_artists). The response
  echoes the applied filter as artist_account_id (null when unfiltered)
  so clients can verify the scope before rendering artist-labeled money.

Per the docs contract in recoupable/docs#267. TDD: RED confirmed before
GREEN for every unit; full suite 3,945 tests, tsc 0 new errors, lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 7, 2026 12:13am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: de772bfc-57eb-4703-a8cf-f81dce5d116b

📥 Commits

Reviewing files that changed from the base of the PR and between 7ed9338 and 637d8b8.

⛔ Files ignored due to path filters (6)
  • lib/catalog/__tests__/getCatalogMeasurementsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/resolveCatalogSongsInScope.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/validateGetCatalogMeasurementsQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/catalog_songs/__tests__/selectCatalogSongTitles.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/song_artists/__tests__/selectSongArtistIsrcs.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/song_measurements/__tests__/selectAllSongMeasurements.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (6)
  • lib/catalog/getCatalogMeasurementsHandler.ts
  • lib/catalog/resolveCatalogSongsInScope.ts
  • lib/catalog/validateGetCatalogMeasurementsQuery.ts
  • lib/supabase/catalog_songs/selectCatalogSongTitles.ts
  • lib/supabase/song_artists/selectSongArtistIsrcs.ts
  • lib/supabase/song_measurements/selectAllSongMeasurements.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/catalog-measurements-artist-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 12 files

Confidence score: 2/5

  • In lib/supabase/catalog_songs/selectCatalogSongTitles.ts and lib/supabase/song_artists/selectSongArtistIsrcs.ts, pagination errors on later pages currently drop already-fetched results and return [], which is indistinguishable from a truly empty catalog and can silently undercount downstream data — preserve partial results or propagate an explicit error state before merging.
  • In lib/supabase/song_measurements/selectAllSongMeasurements.ts, any page/chunk failure can cause GET /api/catalogs/measurements to look like zero data/zero valuation, creating a concrete user-facing misreporting risk — return a typed failure (or throw) so callers can surface an error instead of treating it as empty data.
  • In lib/catalog/getCatalogMeasurementsHandler.ts, artist-scoped valuation mixes artist-filtered stream totals with catalog-wide age, which can skew outputs for scoped views — derive catalog_age_years from the same scoped song set before merge to keep calculations consistent.
  • Tests in lib/supabase/song_measurements/__tests__/selectAllSongMeasurements.test.ts miss coverage for the secondary id sort assertion and for verifying console.error emission, so regressions in deterministic pagination/error observability may slip through — tighten these assertions to de-risk future changes.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/supabase/catalog_songs/selectCatalogSongTitles.ts">

<violation number="1" location="lib/supabase/catalog_songs/selectCatalogSongTitles.ts:19">
P1: Pagination error on a later page silently discards data accumulated from earlier pages. If page 1 succeeds (fetching up to 1,000 rows) and page 2 errors, the function returns `[]` — the same value as an empty catalog. A transient failure on any page beyond the first produces a false empty result that the caller cannot distinguish from a genuinely empty catalog, leading to silent data loss in production for large catalogs. Consider either: (a) returning the partial data with a flag or log indicating truncation, (b) retrying the failed page before giving up, or (c) accumulating `all` through the loop and only returning `[]` when no pages at all were fetched (i.e., page 1 itself failed).</violation>
</file>

<file name="lib/supabase/song_measurements/selectAllSongMeasurements.ts">

<violation number="1" location="lib/supabase/song_measurements/selectAllSongMeasurements.ts:48">
P2: GET /api/catalogs/measurements can report an empty catalog/zero valuation when any measurement page or chunk fails. Since this helper returns [] on `error`, the handler cannot distinguish a real no-data result from a failed exhaustive read; throwing here would let the existing handler return its 500 path.</violation>
</file>

<file name="lib/supabase/song_measurements/__tests__/selectAllSongMeasurements.test.ts">

<violation number="1" location="lib/supabase/song_measurements/__tests__/selectAllSongMeasurements.test.ts:57">
P2: Missing assertion for the secondary sort column `id`. The implementation uses `.order("id", { ascending: false })` to ensure deterministic pagination, but the test for the small-batch path only asserts `.order("captured_at", ...)` was called. If the `id` sort were accidentally removed, pagination could produce unstable pages (rows shifting between pages, silently duplicated or dropped during pagination).</violation>

<violation number="2" location="lib/supabase/song_measurements/__tests__/selectAllSongMeasurements.test.ts:100">
P3: The error test doesn't verify `console.error` was called. The test mocks `console.error` and asserts the return value, but never asserts the log was actually emitted. If a future refactor accidentally drops the `console.error` call, this test won't catch it. Add `expect(consoleError).toHaveBeenCalledWith("Error fetching song_measurements:", { message: "boom" })` before the restore.</violation>
</file>

<file name="lib/catalog/getCatalogMeasurementsHandler.ts">

<violation number="1" location="lib/catalog/getCatalogMeasurementsHandler.ts:48">
P2: Artist-scoped valuation can be skewed because the stream total is filtered to the artist while `catalog_age_years` still comes from the whole catalog. Consider resolving the earliest release date from the scoped song set (or a scoped album/source lookup) when `artist_account_id` is provided.</violation>
</file>

<file name="lib/supabase/song_artists/selectSongArtistIsrcs.ts">

<violation number="1" location="lib/supabase/song_artists/selectSongArtistIsrcs.ts:17">
P1: Same partial-failure data loss as `selectCatalogSongTitles.ts`. If the first page of `song_artists` succeeds but a later page errors, the accumulated ISRCs from earlier pages are discarded and `[]` is returned. This would cause the downstream catalog-measurement scope resolution to appear empty (no songs for the artist) when in reality data was partially fetched and then lost to a transient error. Consider applying the same fix strategy as the sibling function for consistency.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as API Client
    participant Handler as getCatalogMeasurementsHandler
    participant Validator as validateGetCatalogMeasurementsQuery
    participant Auth as validateAuthContext
    participant DBCatalog as selectAccountCatalog
    participant Resolver as resolveCatalogSongsInScope
    participant DBSongs as selectCatalogSongTitles
    participant DBArtists as selectSongArtistIsrcs
    participant DBMeas as selectAllSongMeasurements

    Note over Handler: NEW: pagination + artist filter + response echo

    Client->>Handler: GET /api/catalogs/measurements?catalogId=&artist_account_id=
    Handler->>Validator: validate query params
    Validator-->>Handler: {catalogId, artist_account_id} or 400
    alt malformed artist_account_id (not uuid)
        Handler-->>Client: 400 error
    end

    Handler->>Auth: validateAuthContext(request)
    Auth-->>Handler: {accountId}

    Handler->>DBCatalog: selectAccountCatalog({accountId, catalogId})
    DBCatalog-->>Handler: catalog link or null
    alt catalog not found or forbidden
        Handler-->>Client: 404
    end

    Note over Handler: NEW: resolve scope (whole catalog vs artist filtered)
    Handler->>Resolver: resolveCatalogSongsInScope({catalogId, artistAccountId})
    Resolver->>DBSongs: CHANGED: selectCatalogSongTitles(catalogId)
    Note over DBSongs: Paginates past 1,000-row default. Loops .range() until <1000 rows.
    DBSongs-->>Resolver: all song {isrc, title} pairs

    alt artistAccountId provided
        Resolver->>DBArtists: NEW: selectSongArtistIsrcs(artistAccountId)
        Note over DBArtists: Also paginates with .range() to exhaustion.
        DBArtists-->>Resolver: ISRCs linked to artist
        Note over Resolver: Intersect catalog songs with artist ISRCs
    end

    Resolver-->>Handler: songs in scope (possibly empty)

    Note over Handler: CHANGED: empty scope skips measurement fetch
    alt songs empty
        Handler->>Handler: derive zero totals, zero valuation band
    else songs non-empty
        Handler->>DBMeas: NEW: selectAllSongMeasurements(songs, platform, metric)
        Note over DBMeas: Chunks ISRC list (500 per chunk).<br/>Each chunk paginated with .range() to exhaustion.<br/>Order: captured_at descending, id descending.
        DBMeas-->>Handler: all measurement rows
        Handler->>Handler: latest per ISRC, total_streams, catalog_age_years, valuation band
    end

    Note over Handler: NEW: echo applied filter in response
    Handler-->>Client: {measurements, total_streams, valuation, artist_account_id, catalog_age_years}
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

console.error("Error fetching catalog_songs:", error);
return [];
}
for (let from = 0; ; from += PAGE_SIZE) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Pagination error on a later page silently discards data accumulated from earlier pages. If page 1 succeeds (fetching up to 1,000 rows) and page 2 errors, the function returns [] — the same value as an empty catalog. A transient failure on any page beyond the first produces a false empty result that the caller cannot distinguish from a genuinely empty catalog, leading to silent data loss in production for large catalogs. Consider either: (a) returning the partial data with a flag or log indicating truncation, (b) retrying the failed page before giving up, or (c) accumulating all through the loop and only returning [] when no pages at all were fetched (i.e., page 1 itself failed).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/catalog_songs/selectCatalogSongTitles.ts, line 19:

<comment>Pagination error on a later page silently discards data accumulated from earlier pages. If page 1 succeeds (fetching up to 1,000 rows) and page 2 errors, the function returns `[]` — the same value as an empty catalog. A transient failure on any page beyond the first produces a false empty result that the caller cannot distinguish from a genuinely empty catalog, leading to silent data loss in production for large catalogs. Consider either: (a) returning the partial data with a flag or log indicating truncation, (b) retrying the failed page before giving up, or (c) accumulating `all` through the loop and only returning `[]` when no pages at all were fetched (i.e., page 1 itself failed).</comment>

<file context>
@@ -2,24 +2,35 @@ import supabase from "../serverClient";
-    console.error("Error fetching catalog_songs:", error);
-    return [];
-  }
+  for (let from = 0; ; from += PAGE_SIZE) {
+    const { data, error } = await supabase
+      .from("catalog_songs")
</file context>

export async function selectSongArtistIsrcs(artistAccountId: string): Promise<string[]> {
const all: string[] = [];

for (let from = 0; ; from += PAGE_SIZE) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Same partial-failure data loss as selectCatalogSongTitles.ts. If the first page of song_artists succeeds but a later page errors, the accumulated ISRCs from earlier pages are discarded and [] is returned. This would cause the downstream catalog-measurement scope resolution to appear empty (no songs for the artist) when in reality data was partially fetched and then lost to a transient error. Consider applying the same fix strategy as the sibling function for consistency.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/song_artists/selectSongArtistIsrcs.ts, line 17:

<comment>Same partial-failure data loss as `selectCatalogSongTitles.ts`. If the first page of `song_artists` succeeds but a later page errors, the accumulated ISRCs from earlier pages are discarded and `[]` is returned. This would cause the downstream catalog-measurement scope resolution to appear empty (no songs for the artist) when in reality data was partially fetched and then lost to a transient error. Consider applying the same fix strategy as the sibling function for consistency.</comment>

<file context>
@@ -0,0 +1,34 @@
+export async function selectSongArtistIsrcs(artistAccountId: string): Promise<string[]> {
+  const all: string[] = [];
+
+  for (let from = 0; ; from += PAGE_SIZE) {
+    const { data, error } = await supabase
+      .from("song_artists")
</file context>


if (error) {
console.error("Error fetching song_measurements:", error);
return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: GET /api/catalogs/measurements can report an empty catalog/zero valuation when any measurement page or chunk fails. Since this helper returns [] on error, the handler cannot distinguish a real no-data result from a failed exhaustive read; throwing here would let the existing handler return its 500 path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/song_measurements/selectAllSongMeasurements.ts, line 48:

<comment>GET /api/catalogs/measurements can report an empty catalog/zero valuation when any measurement page or chunk fails. Since this helper returns [] on `error`, the handler cannot distinguish a real no-data result from a failed exhaustive read; throwing here would let the existing handler return its 500 path.</comment>

<file context>
@@ -0,0 +1,58 @@
+
+      if (error) {
+        console.error("Error fetching song_measurements:", error);
+        return [];
+      }
+
</file context>

expect(builders[0].in).toHaveBeenCalledWith("song", ["ISRC1", "ISRC2"]);
expect(builders[0].eq).toHaveBeenCalledWith("platform", "spotify");
expect(builders[0].eq).toHaveBeenCalledWith("metric", "platform_displayed_play_count");
expect(builders[0].order).toHaveBeenCalledWith("captured_at", { ascending: false });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing assertion for the secondary sort column id. The implementation uses .order("id", { ascending: false }) to ensure deterministic pagination, but the test for the small-batch path only asserts .order("captured_at", ...) was called. If the id sort were accidentally removed, pagination could produce unstable pages (rows shifting between pages, silently duplicated or dropped during pagination).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/song_measurements/__tests__/selectAllSongMeasurements.test.ts, line 57:

<comment>Missing assertion for the secondary sort column `id`. The implementation uses `.order("id", { ascending: false })` to ensure deterministic pagination, but the test for the small-batch path only asserts `.order("captured_at", ...)` was called. If the `id` sort were accidentally removed, pagination could produce unstable pages (rows shifting between pages, silently duplicated or dropped during pagination).</comment>

<file context>
@@ -0,0 +1,102 @@
+    expect(builders[0].in).toHaveBeenCalledWith("song", ["ISRC1", "ISRC2"]);
+    expect(builders[0].eq).toHaveBeenCalledWith("platform", "spotify");
+    expect(builders[0].eq).toHaveBeenCalledWith("metric", "platform_displayed_play_count");
+    expect(builders[0].order).toHaveBeenCalledWith("captured_at", { ascending: false });
+    expect(builders[0].range).toHaveBeenCalledWith(0, 999);
+    expect(result).toEqual([row("ISRC1", 200), row("ISRC2", 50)]);
</file context>

}

const songs = await selectCatalogSongTitles(catalogId);
const songs = await resolveCatalogSongsInScope({ catalogId, artistAccountId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Artist-scoped valuation can be skewed because the stream total is filtered to the artist while catalog_age_years still comes from the whole catalog. Consider resolving the earliest release date from the scoped song set (or a scoped album/source lookup) when artist_account_id is provided.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/catalog/getCatalogMeasurementsHandler.ts, line 48:

<comment>Artist-scoped valuation can be skewed because the stream total is filtered to the artist while `catalog_age_years` still comes from the whole catalog. Consider resolving the earliest release date from the scoped song set (or a scoped album/source lookup) when `artist_account_id` is provided.</comment>

<file context>
@@ -34,18 +38,18 @@ export async function getCatalogMeasurementsHandler(request: NextRequest): Promi
     }
 
-    const songs = await selectCatalogSongTitles(catalogId);
+    const songs = await resolveCatalogSongsInScope({ catalogId, artistAccountId });
     const titles = new Map(songs.map(s => [s.isrc, s.title]));
     const [rows, earliestReleaseDate] = await Promise.all([
</file context>

mockPages([{ data: null, error: { message: "boom" } }]);

expect(await selectAllSongMeasurements({ songs: ["ISRC1"], ...params })).toEqual([]);
consoleError.mockRestore();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The error test doesn't verify console.error was called. The test mocks console.error and asserts the return value, but never asserts the log was actually emitted. If a future refactor accidentally drops the console.error call, this test won't catch it. Add expect(consoleError).toHaveBeenCalledWith("Error fetching song_measurements:", { message: "boom" }) before the restore.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/song_measurements/__tests__/selectAllSongMeasurements.test.ts, line 100:

<comment>The error test doesn't verify `console.error` was called. The test mocks `console.error` and asserts the return value, but never asserts the log was actually emitted. If a future refactor accidentally drops the `console.error` call, this test won't catch it. Add `expect(consoleError).toHaveBeenCalledWith("Error fetching song_measurements:", { message: "boom" })` before the restore.</comment>

<file context>
@@ -0,0 +1,102 @@
+    mockPages([{ data: null, error: { message: "boom" } }]);
+
+    expect(await selectAllSongMeasurements({ songs: ["ISRC1"], ...params })).toEqual([]);
+    consoleError.mockRestore();
+  });
+});
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — full authed pass ✅

Preview deployment for 637d8b8a (Ready): https://api-kwowjfmvg-recoup.vercel.app

Ground truth computed with read-only SQL against the shared prod DB (latest capture per ISRC over catalog_songs ∩ song_artists, platform=spotify, metric=platform_displayed_play_count) for the live fixture catalog 7d3e5c35-1fac-4b88-aa90-e2e4a52dfe78 "[TEST] Full Roster Catalog (aggregate)". Note the exact SQL numbers vs the issue's approximations: J Alvarez is 608 songs / 6,717,937,120 streams (issue said ≈610 / 6.81B).

Authed checks (bearer for the owning test account)

Scope SQL ground truth (measured / total_streams) Endpoint actual (measurements / total_streams) Echo artist_account_id valuation.mid Match
Whole catalog (no filter) 2,679 / 16,308,837,441 2,679 / 16,308,837,441 null $132,456,300 ✅ exact
Elvis Crespo b1814076-8e19-4a77-9dea-2ec150e26aaa 322 / 1,799,402,184 322 / 1,799,402,184 b1814076… $14,614,295 ✅ exact
September Mourning d85f9606-c574-4cf3-bfd2-0b82c02b0a78 40 / 21,325,663 40 / 21,325,663 d85f9606… $173,202 ✅ exact
Apache ebae4bb9-e38f-4763-b3c8-ff30e99f5d01 208 / 742,453,110 208 / 742,453,110 ebae4bb9… $6,030,019 ✅ exact
J Alvarez 6f1d797f-5d2f-4754-8c92-d0f491c7bc2f 608 / 6,717,937,120 608 / 6,717,937,120 6f1d797f… $54,561,406 ✅ exact

catalog_age_years: 5 in all responses (default fallback — the fixture's songs weren't materialized from a source run with resolvable album release dates).

  • Cap fix confirmed: the unfiltered read returns all 2,679 measurements / 16.31B streams — vs 1,000 / 4.83B / $39.3M mid on the pre-fix endpoint (repro), a 3.4× undervaluation now gone.
  • Filter + echo confirmed: per-artist scopes return exactly the catalog_songs ∩ song_artists intersection and echo the applied filter; unfiltered echoes null.

Unauthed checks

Check Expected Actual
GET missing catalogId 400 ✅ 400 catalogId parameter is required
GET catalogId=not-a-uuid 400 ✅ 400 catalogId must be a valid UUID
GET catalogId=<valid>&artist_account_id=not-a-uuid 400 ✅ 400 artist_account_id must be a valid UUID
GET valid params, no credentials 401 ✅ 401
OPTIONS 200 ✅ 200

Nothing pending: the test-account bearer authenticated against the preview (prod rejected it — previews use a different key salt — so the earlier prod 401 was salt, not expiry), so the full authed Done-when pass ran.

🤖 Generated with Claude Code

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