Skip to content

fix(catalog): link the caller's account on catalog re-claim (chat#1867) - #775

Closed
sweetmantech wants to merge 2 commits into
mainfrom
fix/link-account-on-reclaim
Closed

fix(catalog): link the caller's account on catalog re-claim (chat#1867)#775
sweetmantech wants to merge 2 commits into
mainfrom
fix/link-account-on-reclaim

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Prerequisite for the post-valuation catalog report (recoupable/chat#1867, the row above chat#1873 in that tracker). Fixes the confirmed onboarding break where a real signup runs a valuation, clicks "Get the full report", and the report renders "No valuation found for this catalog."

Root cause

GET /api/catalogs/{id}/measurements returns 404 only when the caller has no account_catalogs row for the catalog (lib/catalog/getCatalogMeasurementsHandler.ts — missing measurement data returns 200-with-zeros, not 404). The measurement rows themselves are persisted by the funnel snapshot workflow.

The gap: createCatalogHandler's re-claim branch (snapshot.catalog already set) returned the existing catalog without calling insertAccountCatalog. The fresh-claim path (createSnapshotCatalog) links the caller; the re-claim path did not. So whenever the reader's resolved account differs from the account that first claimed the snapshot — account divergence, or a prior double-account race (chat#1875) — the reader is never linked and the report 404s.

What changed

  • lib/catalog/createCatalogHandler.ts — the re-claim branch now calls insertAccountCatalog({ account: accountId, catalog: existing.id }) before returning, guaranteeing the caller owns the catalog (mirrors the fresh-claim path).
  • lib/supabase/account_catalogs/insertAccountCatalog.tsinsert → idempotent upsert (onConflict: "account,catalog", ignoreDuplicates: true, matching the account_catalogs_account_catalog_unique constraint), so linking an already-linked account is a no-op instead of a unique-violation. Mirrors the sibling insertCatalogSongs upsert pattern.

No API contract change (response shapes unchanged) → no docs PR needed.

TDD / verification

  • RED first: added lib/catalog/__tests__/createCatalogHandler.test.ts "re-claims link the caller's account…" (asserts insertAccountCatalog is called on re-claim) and new lib/supabase/account_catalogs/__tests__/insertAccountCatalog.test.ts (idempotent upsert args + error path). Confirmed both fail against the old code.
  • GREEN: full api suite 4123 pass (754 files); tsc --noEmit clean on touched files (repo has a large pre-existing tsc baseline in unrelated files; CI runs pnpm test); eslint + prettier clean on touched files.

Preview verification — pending (honest)

Not yet exercised end-to-end on a preview. The clean way to verify: authenticated POST /api/catalogs { snapshot } re-claim → account_catalogs row exists for the caller → GET /api/catalogs/{id}/measurements returns 200 with rows (was 404). Also recommend a one-time DB spot-check that song_measurements rows exist for a stuck catalog (e.g. a0a4a769-…) to confirm the fix addresses the observed repro rather than a data gap.

Note on the chat report preview: chat previews read test-recoup-api, so verifying chat#1873's happy path against a chat preview needs this fix live on the api deployment that preview reads. Merging here lands it on prod api; getting it onto test-recoup-api is a separate step.

Part of recoupable/chat#1867 (does not close it).

🤖 Generated with Claude Code


Summary by cubic

Links the caller’s account when re-claiming a catalog so the measurements endpoint stops 404ing and the post-valuation report shows data instead of “No valuation found for this catalog.” Makes account–catalog linking idempotent to handle repeated claims safely.

  • Bug Fixes

    • Re-claim path calls upsertAccountCatalog before returning to ensure the caller is linked.
    • upsertAccountCatalog upserts with onConflict "account,catalog" and ignoreDuplicates: true.
    • Added tests for re-claim linking and upsert error handling.
  • Refactors

    • Renamed insertAccountCatalog → upsertAccountCatalog across code and tests; no behavior change.

Written for commit ea1fdc1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved catalog snapshot re-claims so the current account is reliably linked to an existing catalog.
    • Prevented duplicate catalog-account links from causing errors during repeated operations.
    • Preserved existing catalog processing while correctly reporting when no new songs are added.

The re-claim branch of POST /api/catalogs returned an already-created
catalog without writing an account_catalogs row for the caller. Because
GET /api/catalogs/{id}/measurements 404s on a missing account_catalogs
link, a reader whose resolved account differs from the account that first
claimed the snapshot (account divergence / a prior double-account race)
got "No valuation found" on the post-valuation report — the funnel's
measurements are persisted, only the ownership link was missing.

Fix: call insertAccountCatalog in the re-claim branch, and make
insertAccountCatalog an idempotent upsert (onConflict account,catalog,
ignoreDuplicates) so linking an already-linked account is a no-op rather
than a unique-violation. The fresh-claim path already linked; this extends
the same guarantee to re-claims.

TDD red-first. Full suite 4123 pass, tsc clean on touched files, lint +
prettier clean. No API contract change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 21, 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 21, 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 21, 2026 2:27pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 51 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: b5a588bd-fde7-4da6-aa33-4b9deaaf11a9

📥 Commits

Reviewing files that changed from the base of the PR and between 14dc270 and ea1fdc1.

⛔ Files ignored due to path filters (3)
  • lib/catalog/__tests__/createCatalogHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/createSnapshotCatalog.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/account_catalogs/__tests__/upsertAccountCatalog.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • lib/catalog/createCatalogHandler.ts
  • lib/catalog/createSnapshotCatalog.ts
  • lib/supabase/account_catalogs/upsertAccountCatalog.ts
📝 Walkthrough

Walkthrough

The snapshot re-claim path now restores the authenticated account’s link to an existing catalog. Account-catalog links use idempotent upserts targeting the (account, catalog) conflict pair.

Changes

Catalog ownership

Layer / File(s) Summary
Snapshot re-claim account linking
lib/catalog/createCatalogHandler.ts, lib/supabase/account_catalogs/insertAccountCatalog.ts
Existing snapshot catalogs are linked to the current account before continuing, while account-catalog persistence uses duplicate-safe upserts with updated documentation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • recoupable/api#768: Extends the same snapshot re-claim path with canonical-artist attachment.

Poem

A snapshot returns, its link made whole,
Upsert guards the ownership role.
No duplicate sparks, no records collide,
The catalog waits with account beside.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Solid & Clean Code ✅ Passed Both touched files export one primary function matching the filename, and the added idempotent linking logic is small, clear, and covered by focused tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/link-account-on-reclaim

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.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
lib/catalog/createCatalogHandler.ts (1)

31-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the re-claim logic to keep the function under 50 lines.

The createCatalogHandler function is currently 60 lines long, which exceeds the limit. As per path instructions for lib/**/*.ts, functions should be kept under 50 lines. Consider extracting the existing catalog re-claim logic into a separate helper function to improve readability and strictly adhere to the Single Responsibility Principle.

♻️ Proposed refactoring
 export async function createCatalogHandler(request: NextRequest): Promise<NextResponse> {
   try {
     const body = await request.json().catch(() => null);
 
     const validated = validateCreateCatalogBody(body);
     if (validated instanceof NextResponse) {
       return validated;
     }
 
     const authResult = await validateAuthContext(request);
     if (authResult instanceof NextResponse) {
       return authResult;
     }
     const { accountId } = authResult;
 
     if (!validated.snapshot) {
       const catalog = await insertCatalog(validated.name ?? DEFAULT_CATALOG_NAME);
       await insertAccountCatalog({ account: accountId, catalog: catalog.id });
       return successResponse({ catalog, songs_added: 0 });
     }
 
     const [snapshot] = await selectPlaycountSnapshots({ id: validated.snapshot });
     if (!snapshot) {
       return errorResponse("Snapshot not found", 404);
     }
     if (snapshot.account !== accountId) {
       return errorResponse("Snapshot belongs to a different account", 403);
     }
 
-    // Idempotent re-claim: the run already produced a catalog. Link the caller
-    // to it (chat#1867): the report's measurements endpoint 404s on a missing
-    // account_catalogs row, so a reader whose account differs from the original
-    // claimer (account divergence / prior double-account race) otherwise sees
-    // "No valuation found". insertAccountCatalog is idempotent, so this heals
-    // an unlinked reader without duplicating an existing link. Still attach the
-    // canonical artist (chat#1850 P1); the attach is itself idempotent.
-    if (snapshot.catalog) {
-      const existing = await selectCatalogById(snapshot.catalog);
-      if (existing) {
-        await insertAccountCatalog({ account: accountId, catalog: existing.id });
-        const measurements = await selectSongMeasurements({ snapshot: snapshot.id });
-        const isrcs = [...new Set(measurements.map(m => m.song))];
-        if (isrcs.length > 0) {
-          await attachCanonicalArtistToAccount({ accountId, isrcs });
-        }
-        return successResponse({ catalog: existing, songs_added: 0 });
-      }
+    const reClaimResponse = await handleReclaimCatalog(accountId, snapshot);
+    if (reClaimResponse) {
+      return reClaimResponse;
     }
 
     const { catalog, songsAdded } = await createSnapshotCatalog({
       accountId,
       snapshot,
       name: validated.name,
     });
     return successResponse({ catalog, songs_added: songsAdded });
   } catch (error) {
     console.error("Error creating catalog:", error);
     return errorResponse("Internal server error", 500);
   }
 }
+
+async function handleReclaimCatalog(accountId: string, snapshot: any): Promise<NextResponse | null> {
+  if (!snapshot.catalog) return null;
+  
+  const existing = await selectCatalogById(snapshot.catalog);
+  if (!existing) return null;
+  
+  await insertAccountCatalog({ account: accountId, catalog: existing.id });
+  const measurements = await selectSongMeasurements({ snapshot: snapshot.id });
+  const isrcs = [...new Set(measurements.map(m => m.song))];
+  
+  if (isrcs.length > 0) {
+    await attachCanonicalArtistToAccount({ accountId, isrcs });
+  }
+  
+  return successResponse({ catalog: existing, songs_added: 0 });
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/catalog/createCatalogHandler.ts` around lines 31 - 90, Extract the
existing snapshot.catalog re-claim block from createCatalogHandler into a
focused helper that accepts the account ID and snapshot/catalog context,
performs the idempotent account link and canonical-artist attachment, and
returns the existing catalog response data. Update createCatalogHandler to call
the helper while preserving its current behavior and reduce the function below
50 lines; do not alter validation, authorization, or new-catalog creation flows.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/catalog/createCatalogHandler.ts`:
- Around line 31-90: Extract the existing snapshot.catalog re-claim block from
createCatalogHandler into a focused helper that accepts the account ID and
snapshot/catalog context, performs the idempotent account link and
canonical-artist attachment, and returns the existing catalog response data.
Update createCatalogHandler to call the helper while preserving its current
behavior and reduce the function below 50 lines; do not alter validation,
authorization, or new-catalog creation flows.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6f250f61-26de-4193-a5c6-e9caf4cb3814

📥 Commits

Reviewing files that changed from the base of the PR and between 8241b61 and 14dc270.

⛔ Files ignored due to path filters (2)
  • lib/catalog/__tests__/createCatalogHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/account_catalogs/__tests__/insertAccountCatalog.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/catalog/createCatalogHandler.ts
  • lib/supabase/account_catalogs/insertAccountCatalog.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

KISS

  • actual: lib/supabase/account_catalogs/insertAccountCatalog.ts
  • required: lib/supabase/account_catalogs/upsertAccountCatalog.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — renamed insertAccountCatalogupsertAccountCatalog (file + function + all call sites + tests), matching the existing upsertAccountSnapshot convention. Pure rename, no behavior change; 21 tests green, tsc/lint clean. Pushed as ea1fdc19.

…tches the upsert)

Per review (KISS): the function now upserts, so name it for what it does,
matching the repo's existing upsertAccountSnapshot convention. Pure symbol +
file rename across all call sites and tests; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

0 issues found across 6 files (changes from recent commits).

Auto-approved: Fixes a missing account-catalog link on re-claim that caused a 404. Refactors insert to idempotent upsert. No schema, contract, or rollout changes; bounded bug fix with tests.

Re-trigger cubic

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Closing — not needed. A DB inspection settled the diagnosis, and it invalidates the premise this PR was opened on.

For catalog a0a4a769 (the one that showed "No valuation found" in my earlier test):

  • song_measurements: 174 rows — measurements are persisted by the funnel.
  • account_catalogs: linked to exactly the claiming account (20f57385), which is also the snapshot owner.

So the claim worked and linked the correct account. My 404 was a cross-account test artifact: I ran the funnel/claim under one account and read the report on the preview logged in as a different fresh account — correct IDOR behavior, not a bug. The re-claim branch requires snapshot.account === accountId, and the snapshot owner is already linked, so the upsertAccountCatalog call here is a no-op in every reachable state. In the real single-account flow (post-#1876), claim and read are the same account, which is linked, so the report already renders.

The insertAccountCatalogupsertAccountCatalog idempotency rename was fine, but there's no reachable bug to justify shipping this. Withdrawing per YAGNI. Tracking correction on recoupable/chat#1867.

@sweetmantech
sweetmantech deleted the fix/link-account-on-reclaim branch July 21, 2026 14:54
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