Skip to content

feat: persist catalog valuations + GET /api/catalogs/{catalogId}/valuations - #795

Open
sweetmantech wants to merge 1 commit into
mainfrom
feat/catalog-valuations-api
Open

feat: persist catalog valuations + GET /api/catalogs/{catalogId}/valuations#795
sweetmantech wants to merge 1 commit into
mainfrom
feat/catalog-valuations-api

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Row 15 of chat#1889: the keystone. est_catalog_value was computed on every run and read but never persisted. This PR writes it to the new catalog_valuations history table and exposes the series via GET /api/catalogs/{catalogId}/valuations.

Merge order: docs#279 (contract) → database#52 (table — hard dependency, must merge before this) → this PR.

What it does

Persist (two sites, one helper):

  • lib/catalog/persistCatalogValuation.ts — best-effort (never fails the caller), optional dedupeDaily
  • runValuationHandler — every valuation run writes a row (each run is a fresh measurement)
  • getCatalogMeasurementsHandler — whole-catalog reads write at most one row per catalog per UTC day; artist-scoped reads never write (they value a slice, not the catalog)

Read:

  • GET /api/catalogs/{catalogId}/valuations?limit= — latest-first series, limit 1–100 default 30, limit=1 = current value. Auth via validateAuthContext (bearer or x-api-key); unowned/missing catalog → 404; bad catalogId/limit → 400. Flat { status, valuations } with rows { low, mid, high, measured_song_count, total_streams, measured_at }, matching docs#279 exactly.

Supabase layer: lib/supabase/catalog_valuations/{insertCatalogValuation,selectCatalogValuations}.ts + hand-added catalog_valuations block in types/database.types.ts (regenerates identically once database#52 merges).

Tests (TDD, red→green)

18 new tests across 5 new suites + 2 wiring cases added to getCatalogMeasurementsHandler.test.ts (persists daily-deduped on whole-catalog reads; never on artist-scoped reads). All confirmed RED before implementation.

  • Full suite: 791 files / 4287 tests passed
  • tsc --noEmit: 236 errors — the pre-existing baseline, none in touched files
  • eslint clean on all new/touched files

runValuationHandler has no unit harness (repo convention for that orchestrator); its persist wiring will be verified live on the preview (run a valuation → row appears in the GET).

Closes nothing on its own — tracked in chat#1889 row 15.

🤖 Generated with Claude Code


Summary by cubic

Persist catalog valuation bands to catalog_valuations and add GET /api/catalogs/{catalogId}/valuations to read the history (limit=1 returns the current value). This turns ephemeral est_catalog_value into a stored time series for charts and audits.

  • New Features

    • Persists valuation rows with low/mid/high, measured_song_count, total_streams, measured_at. Writes on every valuation run; daily-deduped on whole-catalog reads; never on artist-scoped reads; best-effort (never fails the caller).
    • Adds GET /api/catalogs/{catalogId}/valuations?limit= (1–100, default 30), latest-first. Auth via bearer or x-api-key; 400 on invalid catalogId/limit; 404 if catalog is missing or not owned. Returns { valuations: [{ low, mid, high, measured_song_count, total_streams, measured_at }] }.
  • Migration

    • Ensure the catalog_valuations table is deployed before releasing this endpoint. No client changes required.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added an API for retrieving catalog valuation history, including configurable result limits.
    • Valuation records are shown latest-first with valuation ranges, measurement dates, song counts, and stream totals.
    • Newly calculated catalog valuations are saved to history automatically.
    • Daily duplicate records are prevented for catalog-wide measurements.
    • Added validation, authentication, access checks, and CORS support for valuation history requests.

@cursor

cursor Bot commented Jul 29, 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 29, 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 29, 2026 11:49pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds catalog valuation-history storage, best-effort persistence from valuation workflows, and an authenticated API route supporting latest-first queries, limits, access checks, and CORS preflight handling.

Changes

Catalog Valuation History

Layer / File(s) Summary
Valuation storage helpers
lib/supabase/catalog_valuations/*
Adds Supabase helpers to insert valuation rows and retrieve catalog history ordered by measured_at descending with a limit.
Best-effort valuation persistence
lib/catalog/persistCatalogValuation.ts
Adds typed persistence with optional per-UTC-day de-duplication, measurement aggregates, and error logging without throwing.
Valuation write integration
lib/valuation/runValuationHandler.ts, lib/catalog/getCatalogMeasurementsHandler.ts
Records completed valuation results and whole-catalog measurement valuations while skipping writes for artist-scoped reads.
Valuation history API
lib/catalog/validateGetCatalogValuationsQuery.ts, lib/catalog/getCatalogValuationsHandler.ts, app/api/catalogs/[catalogId]/valuations/route.ts
Adds authenticated UUID and limit validation, catalog access checks, numeric response mapping, GET handling, and CORS preflight support.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ValuationsRoute
  participant validateGetCatalogValuationsQuery
  participant selectAccountCatalog
  participant selectCatalogValuations
  Client->>ValuationsRoute: GET catalog valuations
  ValuationsRoute->>validateGetCatalogValuationsQuery: Validate auth, catalogId, and limit
  validateGetCatalogValuationsQuery-->>ValuationsRoute: Validated accountId and query
  ValuationsRoute->>selectAccountCatalog: Check catalog-account link
  selectAccountCatalog-->>ValuationsRoute: Catalog access result
  ValuationsRoute->>selectCatalogValuations: Fetch latest-first valuation rows
  selectCatalogValuations-->>ValuationsRoute: Limited valuation history
  ValuationsRoute-->>Client: Success response with valuations
Loading

Possibly related issues

Possibly related PRs

  • recoupable/api#763: Refactors the measurements aggregate used by whole-catalog valuation persistence.
  • recoupable/api#773: Overlaps in valuation-completion side effects within runValuationHandler.
  • recoupable/api#775: Adds catalog ownership links used by valuation-history access checks.

Poem

Valuations gather, rows align,
History blooms by measured time.
Daily echoes softly cease,
Auth guards keep the pathway clean.
GET brings numbers, fresh and bright.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning route.ts exports both GET and OPTIONS, so the file has multiple exported functions and no name match; runValuationHandler is also 192 lines, above the 20-line guideline. Keep route.ts as a thin adapter and move logic into one named module per file; also split runValuationHandler into smaller helpers to reduce responsibility and length.
✅ Passed checks (2 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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/catalog-valuations-api

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.

Actionable comments posted: 1

🧹 Nitpick comments (7)
app/api/catalogs/[catalogId]/valuations/route.ts (1)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Inject the request-path dependencies.

getCatalogValuationsHandler hard-wires validation and Supabase helpers, making this API flow harder to substitute in tests. Compose a handler with injected dependencies at the route boundary.

As per path instructions, “Use dependency injection for services.”

🤖 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 `@app/api/catalogs/`[catalogId]/valuations/route.ts at line 3, Update the route
boundary around getCatalogValuationsHandler to compose the handler with injected
validation and Supabase dependencies instead of importing or relying on
hard-wired helpers. Preserve the existing request and response behavior while
passing the route-specific service implementations into the handler.

Source: Path instructions

lib/supabase/catalog_valuations/selectCatalogValuations.ts (1)

12-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep this helper under 20 lines.

Extract the query-error logging/handling into a small private helper; this function is 21 lines.

As per coding guidelines, “Flag functions longer than 20 lines.”

🤖 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/supabase/catalog_valuations/selectCatalogValuations.ts` around lines 12 -
32, Reduce selectCatalogValuations below 20 lines by extracting the query-error
logging and null-return handling into a small private helper. Keep the existing
query and successful return behavior unchanged, and invoke the helper from the
error branch.

Source: Coding guidelines

lib/catalog/validateGetCatalogValuationsQuery.ts (2)

10-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the valuation-history limit contract.

Move the default, minimum, and maximum into a named configuration object so validation and future consumers cannot drift.

As per coding guidelines, “Use configuration objects instead of hardcoded values.”

🤖 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/validateGetCatalogValuationsQuery.ts` around lines 10 - 21, In
the limit schema within validateGetCatalogValuationsQuery, replace the hardcoded
default, minimum, and maximum values with a named configuration object. Define
and reuse that object for the default, min, and max validation settings so the
valuation-history limit contract is centralized for future consumers.

Source: Coding guidelines


39-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split this validator into smaller focused helpers.

This function is 27 lines. Extract query parsing/error-response construction while preserving auth-first behavior.

As per coding guidelines, “Flag functions longer than 20 lines.”

🤖 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/validateGetCatalogValuationsQuery.ts` around lines 39 - 65, Split
validateGetCatalogValuationsQuery into focused helpers: extract query parameter
parsing and validation, plus construction of the invalid-query NextResponse,
while keeping validateAuthContext as the first operation and preserving the
existing success and error response behavior. Keep
validateGetCatalogValuationsQuery as the orchestration entry point and reuse the
existing schema and CORS symbols.

Source: Coding guidelines

lib/catalog/getCatalogValuationsHandler.ts (1)

23-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the handler below 20 lines.

Extract response mapping and the validated fetch/access flow into private helpers; the handler is currently 36 lines.

As per coding guidelines, “Flag functions longer than 20 lines.”

🤖 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/getCatalogValuationsHandler.ts` around lines 23 - 58, Refactor
getCatalogValuationsHandler to stay under 20 lines by extracting the
row-to-response valuation mapping and the validated catalog access/fetch flow
into private helper functions. Keep validation, authorization, error responses,
success payload shape, and catch behavior unchanged; have the handler delegate
to these helpers.

Source: Coding guidelines

lib/catalog/getCatalogMeasurementsHandler.ts (1)

61-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Keep the measurement handler focused and within the path limit.

getCatalogMeasurementsHandler spans Lines 31-92 and now combines validation, access checks, three reads, valuation, persistence, and response shaping. Extract the new whole-catalog persistence decision into a focused use-case/helper so the handler remains a small request/response orchestrator.

As per coding guidelines, functions longer than 20 lines must be flagged and functions should remain small and focused. As per path instructions, domain functions must remain under 50 lines and follow single responsibility.

🤖 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/getCatalogMeasurementsHandler.ts` around lines 61 - 72, Extract
the whole-catalog persistence decision from getCatalogMeasurementsHandler into a
focused use-case/helper that handles the !artistAccountId condition and
persistCatalogValuation call, then invoke it from the handler after valuation.
Keep validation, reads, valuation, and response shaping in the handler while
preserving daily deduplication and artist-scoped no-write behavior.

Sources: Coding guidelines, Path instructions

lib/catalog/persistCatalogValuation.ts (1)

20-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Keep the changed functions small and focused.

  • lib/catalog/persistCatalogValuation.ts#L20-L52: separate dedupe/atomic-write policy from error handling and the public persistence wrapper.
  • lib/valuation/runValuationHandler.ts#L146-L154: move valuation-history persistence out of the 149-line request orchestrator.
  • lib/catalog/getCatalogMeasurementsHandler.ts#L61-L72: move the whole-catalog persistence decision out of the 62-line measurements handler.

As per coding guidelines, functions longer than 20 lines must be flagged and functions should remain small and focused. As per path instructions, lib/**/*.ts domain functions must remain under 50 lines and follow single responsibility.

🤖 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/persistCatalogValuation.ts` around lines 20 - 52, Keep the
persistence functions small and single-purpose: in
lib/catalog/persistCatalogValuation.ts lines 20-52, extract deduplication and
atomic-write logic from the public error-handling wrapper while preserving
behavior; in lib/valuation/runValuationHandler.ts lines 146-154, move
valuation-history persistence out of the request orchestrator into a focused
helper; in lib/catalog/getCatalogMeasurementsHandler.ts lines 61-72, move the
whole-catalog persistence decision into a focused helper. Ensure each affected
domain function remains under 50 lines and ideally under 20 lines.

Sources: Coding guidelines, 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.

Inline comments:
In `@lib/catalog/persistCatalogValuation.ts`:
- Around line 34-48: Make the deduplication in persistCatalogValuation atomic
rather than relying on the selectCatalogValuations check followed by
insertCatalogValuation. Enforce one valuation per catalog and UTC day in the
database/storage layer using a unique UTC-day key or atomic insert/upsert,
allowing duplicate conflicts to remain harmless under the existing best-effort
behavior.

---

Nitpick comments:
In `@app/api/catalogs/`[catalogId]/valuations/route.ts:
- Line 3: Update the route boundary around getCatalogValuationsHandler to
compose the handler with injected validation and Supabase dependencies instead
of importing or relying on hard-wired helpers. Preserve the existing request and
response behavior while passing the route-specific service implementations into
the handler.

In `@lib/catalog/getCatalogMeasurementsHandler.ts`:
- Around line 61-72: Extract the whole-catalog persistence decision from
getCatalogMeasurementsHandler into a focused use-case/helper that handles the
!artistAccountId condition and persistCatalogValuation call, then invoke it from
the handler after valuation. Keep validation, reads, valuation, and response
shaping in the handler while preserving daily deduplication and artist-scoped
no-write behavior.

In `@lib/catalog/getCatalogValuationsHandler.ts`:
- Around line 23-58: Refactor getCatalogValuationsHandler to stay under 20 lines
by extracting the row-to-response valuation mapping and the validated catalog
access/fetch flow into private helper functions. Keep validation, authorization,
error responses, success payload shape, and catch behavior unchanged; have the
handler delegate to these helpers.

In `@lib/catalog/persistCatalogValuation.ts`:
- Around line 20-52: Keep the persistence functions small and single-purpose: in
lib/catalog/persistCatalogValuation.ts lines 20-52, extract deduplication and
atomic-write logic from the public error-handling wrapper while preserving
behavior; in lib/valuation/runValuationHandler.ts lines 146-154, move
valuation-history persistence out of the request orchestrator into a focused
helper; in lib/catalog/getCatalogMeasurementsHandler.ts lines 61-72, move the
whole-catalog persistence decision into a focused helper. Ensure each affected
domain function remains under 50 lines and ideally under 20 lines.

In `@lib/catalog/validateGetCatalogValuationsQuery.ts`:
- Around line 10-21: In the limit schema within
validateGetCatalogValuationsQuery, replace the hardcoded default, minimum, and
maximum values with a named configuration object. Define and reuse that object
for the default, min, and max validation settings so the valuation-history limit
contract is centralized for future consumers.
- Around line 39-65: Split validateGetCatalogValuationsQuery into focused
helpers: extract query parameter parsing and validation, plus construction of
the invalid-query NextResponse, while keeping validateAuthContext as the first
operation and preserving the existing success and error response behavior. Keep
validateGetCatalogValuationsQuery as the orchestration entry point and reuse the
existing schema and CORS symbols.

In `@lib/supabase/catalog_valuations/selectCatalogValuations.ts`:
- Around line 12-32: Reduce selectCatalogValuations below 20 lines by extracting
the query-error logging and null-return handling into a small private helper.
Keep the existing query and successful return behavior unchanged, and invoke the
helper from the error branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c1e9839e-f33b-4d37-ad4c-1e53caae05de

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb0ca2 and 6bbc7ca.

⛔ Files ignored due to path filters (7)
  • lib/catalog/__tests__/getCatalogMeasurementsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/getCatalogValuationsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/persistCatalogValuation.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/validateGetCatalogValuationsQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/catalog_valuations/__tests__/insertCatalogValuation.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/catalog_valuations/__tests__/selectCatalogValuations.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • types/database.types.ts is excluded by none and included by none
📒 Files selected for processing (8)
  • app/api/catalogs/[catalogId]/valuations/route.ts
  • lib/catalog/getCatalogMeasurementsHandler.ts
  • lib/catalog/getCatalogValuationsHandler.ts
  • lib/catalog/persistCatalogValuation.ts
  • lib/catalog/validateGetCatalogValuationsQuery.ts
  • lib/supabase/catalog_valuations/insertCatalogValuation.ts
  • lib/supabase/catalog_valuations/selectCatalogValuations.ts
  • lib/valuation/runValuationHandler.ts

Comment on lines +34 to +48
if (dedupeDaily) {
const latest = await selectCatalogValuations({ catalogId, limit: 1 });
const newestDay = latest?.[0]?.measured_at?.slice(0, 10);
const today = new Date().toISOString().slice(0, 10);
if (newestDay === today) return;
}

await insertCatalogValuation({
catalog_id: catalogId,
low: valuation.low,
mid: valuation.mid,
high: valuation.high,
measured_song_count: measuredSongCount,
total_streams: totalStreams,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make daily deduplication atomic.

Line 35 and Line 41 form a check-then-insert sequence. Concurrent whole-catalog requests can both observe no row for today and both insert, violating the at-most-one-per-UTC-day objective. Enforce this invariant in the database/storage layer with a unique UTC-day key or an atomic insert/upsert; duplicate conflicts can remain harmless under the best-effort policy.

🤖 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/persistCatalogValuation.ts` around lines 34 - 48, Make the
deduplication in persistCatalogValuation atomic rather than relying on the
selectCatalogValuations check followed by insertCatalogValuation. Enforce one
valuation per catalog and UTC day in the database/storage layer using a unique
UTC-day key or atomic insert/upsert, allowing duplicate conflicts to remain
harmless under the existing best-effort behavior.

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

7 issues found across 15 files

Confidence score: 2/5

  • lib/catalog/getCatalogMeasurementsHandler.ts calls persistCatalogValuation from a GET path, so simple reads can mutate catalog_valuations and create unexpected side effects for retries/caching semantics — move persistence off the read endpoint (or make the write path explicitly idempotent and intentional).
  • lib/catalog/persistCatalogValuation.ts can insert duplicate same-day history rows under concurrent reads, and lib/valuation/runValuationHandler.ts can persist zero-band valuations when aggregates fail, which risks polluting valuation history and downstream reporting — add a DB-enforced daily uniqueness/upsert and short-circuit persistence on aggregate query errors.
  • lib/catalog/getCatalogValuationsHandler.ts maps ownership lookup failures to 404, so Supabase/query outages can look like permanent “not found” responses and suppress client retries — keep an explicit error branch that returns 500 for backend failures.
  • lib/catalog/__tests__/getCatalogValuationsHandler.test.ts, lib/catalog/__tests__/validateGetCatalogValuationsQuery.test.ts, and duplicated query-validation logic in lib/catalog/validateGetCatalogValuationsQuery.ts leave error-envelope and boundary behavior prone to drift — add the missing 500 body-safety and lower-bound tests, then consolidate shared validation/auth handling.
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/catalog/persistCatalogValuation.ts">

<violation number="1" location="lib/catalog/persistCatalogValuation.ts:41">
P2: Concurrent whole-catalog reads can create multiple daily history rows: each request can pass the lookup before either insert commits. Preserve the `dedupeDaily` guarantee with an atomic database-side operation/constraint rather than this read-then-insert check.</violation>
</file>

<file name="lib/catalog/__tests__/validateGetCatalogValuationsQuery.test.ts">

<violation number="1" location="lib/catalog/__tests__/validateGetCatalogValuationsQuery.test.ts:57">
P3: Missing test coverage for the lower limit boundary. The schema rejects limit=0 and limit=-1 with 400, but only the upper boundary (limit=101) and non-numeric input are tested. Add a test for limit=0 or limit=-1 to match the boundary coverage on the lower end.</violation>
</file>

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

<violation number="1" location="lib/catalog/validateGetCatalogValuationsQuery.ts:43">
P3: Catalog query validation flow is now maintained in two near-identical implementations, so auth/error-envelope changes can drift between measurements and valuations. Consider extracting the shared auth/parse/error handling while keeping each endpoint schema local.</violation>
</file>

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

<violation number="1" location="lib/catalog/getCatalogValuationsHandler.ts:35">
P2: A failed ownership lookup is reported as “Catalog not found,” so a Supabase outage/query failure produces a permanent-looking 404 instead of a retryable 500. Preserve an error result distinct from an absent account-catalog link, then return 500 for that case.</violation>
</file>

<file name="lib/catalog/__tests__/getCatalogValuationsHandler.test.ts">

<violation number="1" location="lib/catalog/__tests__/getCatalogValuationsHandler.test.ts:113">
P2: The 500-error test only checks `response.status === 500` and never verifies the response body. Per the team's established convention, tests for 500 responses should assert that raw exception text does not leak into the body (e.g., assert `body.error === "Internal server error"` or that stack traces/DB errors are absent). Without this check, a future change that accidentally returns a dynamic error message would not be caught.</violation>
</file>

<file name="lib/valuation/runValuationHandler.ts">

<violation number="1" location="lib/valuation/runValuationHandler.ts:152">
P2: When `selectCatalogMeasurementsAggregate` returns null (DB error), the persist call silently writes a zero-band valuation (`{low:0, mid:0, high:0}`) with `measuredSongCount: 0, totalStreams: 0` into the valuation history, creating a misleading entry that looks like a legitimate measurement. The sibling handler `getCatalogMeasurementsHandler` guards against null aggregate with an early 500 return, and even the same file's `captureValuationLead` call uses `?? undefined` (meaning "unknown") rather than `?? 0` (which conflates "unknown" with "known zero"). Since the design intent is best-effort, consider skipping the persist when the aggregate query itself failed — that avoids polluting the history with a false zero-band row without adding a hard failure path.</violation>
</file>

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

<violation number="1" location="lib/catalog/getCatalogMeasurementsHandler.ts:64">
P1: Custom agent: **API Design Consistency and Maintainability**

This GET endpoint performs a database write by calling `persistCatalogValuation`, which inserts a row into `catalog_valuations`. The custom API-consistency rule requires GET for retrieval and POST for mutations or actions. Even though the write is idempotent (daily-deduped), it is still a side-effect mutation on a read endpoint, which breaks safe-method semantics and complicates caching. Consider moving the call to the existing POST valuation endpoint or exposing a dedicated POST route for persisting a valuation so that reads remain side-effect free.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant NextAPI as GET valuations Route
    participant AuthV as Auth+Validator
    participant CatDB as account_catalogs (Supabase)
    participant ValDB as catalog_valuations (Supabase)
    participant Persist as persistCatalogValuation
    participant Insert as insertCatalogValuation (Supabase)
    participant Existing as runValuationHandler / getCatalogMeasurementsHandler

    Note over Client,Insert: NEW FLOW: GET /api/catalogs/{catalogId}/valuations?limit=N

    Client->>NextAPI: GET /api/catalogs/{catalogId}/valuations?limit=30
    NextAPI->>AuthV: validateGetCatalogValuationsQuery(request, catalogId)
    AuthV->>AuthV: validateAuthContext (bearer or x-api-key)
    alt Auth failure
        AuthV-->>NextAPI: NextResponse 401
        NextAPI-->>Client: 401
    else Auth success → {accountId}
        AuthV->>AuthV: parse & validate catalogId (UUID), limit (1-100, default 30)
        alt Invalid params
            AuthV-->>NextAPI: NextResponse 400
            NextAPI-->>Client: 400
        else Valid → {accountId, catalogId, limit}
            AuthV-->>NextAPI: validated data
            NextAPI->>CatDB: selectAccountCatalog({accountId, catalogId})
            alt Catalog not owned or missing → null
                CatDB-->>NextAPI: null
                NextAPI-->>Client: 404 { status: "error", error: "Catalog not found" }
            else Owned → link record
                CatDB-->>NextAPI: link
                NextAPI->>ValDB: selectCatalogValuations({catalogId, limit})
                alt DB error → null
                    ValDB-->>NextAPI: null
                    NextAPI-->>Client: 500
                else Success → rows []
                    ValDB-->>NextAPI: rows (latest-first)
                    NextAPI-->>Client: 200 { valuations: [{ low, mid, high, measured_song_count, total_streams, measured_at }] }
                end
            end
        end
    end

    Note over Existing,Insert: NEW PERSIST HOOKS (inside existing flows)

    Existing->>Existing: computeValuationBand() [pre-existing]
    Existing-->>Existing: {low, mid, high, measuredSongCount, totalStreams}
    Existing->>Persist: persistCatalogValuation({catalogId, valuation, measuredSongCount, totalStreams, dedupeDaily?})

    alt dedupeDaily=true (whole-catalog read path)
        Persist->>ValDB: selectCatalogValuations({catalogId, limit:1})
        ValDB-->>Persist: latest row (or empty/null)
        alt Latest row measured_at is today
            Persist-->>Existing: skip (no insert)
        else No row or earlier day
            Persist->>Insert: insertCatalogValuation({catalog_id, low, mid, high, measured_song_count, total_streams})
            Insert-->>Persist: inserted row or null
            Note over Persist: Best-effort: errors logged, never thrown
            Persist-->>Existing: void
        end
    else dedupeDaily=false (valuation run path)
        Persist->>Insert: insertCatalogValuation(...)
        Insert-->>Persist: inserted row or null
        Persist-->>Existing: void
    end
Loading

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

Re-trigger cubic

// Persist the whole-catalog band into the valuation history (chat#1889
// row 15) — daily-deduped so page refreshes don't mint rows. Artist-scoped
// reads value a slice of the catalog, not the catalog, so they never write.
if (!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.

P1: Custom agent: API Design Consistency and Maintainability

This GET endpoint performs a database write by calling persistCatalogValuation, which inserts a row into catalog_valuations. The custom API-consistency rule requires GET for retrieval and POST for mutations or actions. Even though the write is idempotent (daily-deduped), it is still a side-effect mutation on a read endpoint, which breaks safe-method semantics and complicates caching. Consider moving the call to the existing POST valuation endpoint or exposing a dedicated POST route for persisting a valuation so that reads remain side-effect free.

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

<comment>This GET endpoint performs a database write by calling `persistCatalogValuation`, which inserts a row into `catalog_valuations`. The custom API-consistency rule requires GET for retrieval and POST for mutations or actions. Even though the write is idempotent (daily-deduped), it is still a side-effect mutation on a read endpoint, which breaks safe-method semantics and complicates caching. Consider moving the call to the existing POST valuation endpoint or exposing a dedicated POST route for persisting a valuation so that reads remain side-effect free.</comment>

<file context>
@@ -57,6 +58,19 @@ export async function getCatalogMeasurementsHandler(
+    // Persist the whole-catalog band into the valuation history (chat#1889
+    // row 15) — daily-deduped so page refreshes don't mint rows. Artist-scoped
+    // reads value a slice of the catalog, not the catalog, so they never write.
+    if (!artistAccountId) {
+      await persistCatalogValuation({
+        catalogId,
</file context>

if (newestDay === today) return;
}

await insertCatalogValuation({

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: Concurrent whole-catalog reads can create multiple daily history rows: each request can pass the lookup before either insert commits. Preserve the dedupeDaily guarantee with an atomic database-side operation/constraint rather than this read-then-insert check.

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

<comment>Concurrent whole-catalog reads can create multiple daily history rows: each request can pass the lookup before either insert commits. Preserve the `dedupeDaily` guarantee with an atomic database-side operation/constraint rather than this read-then-insert check.</comment>

<file context>
@@ -0,0 +1,52 @@
+      if (newestDay === today) return;
+    }
+
+    await insertCatalogValuation({
+      catalog_id: catalogId,
+      low: valuation.low,
</file context>

const { accountId, catalogId, limit } = validated;

const link = await selectAccountCatalog({ accountId, catalogId });
if (!link) {

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: A failed ownership lookup is reported as “Catalog not found,” so a Supabase outage/query failure produces a permanent-looking 404 instead of a retryable 500. Preserve an error result distinct from an absent account-catalog link, then return 500 for that case.

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

<comment>A failed ownership lookup is reported as “Catalog not found,” so a Supabase outage/query failure produces a permanent-looking 404 instead of a retryable 500. Preserve an error result distinct from an absent account-catalog link, then return 500 for that case.</comment>

<file context>
@@ -0,0 +1,58 @@
+    const { accountId, catalogId, limit } = validated;
+
+    const link = await selectAccountCatalog({ accountId, catalogId });
+    if (!link) {
+      return errorResponse("Catalog not found", 404);
+    }
</file context>


const response = await getCatalogValuationsHandler(makeRequest(), catalogId);

expect(response.status).toBe(500);

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: The 500-error test only checks response.status === 500 and never verifies the response body. Per the team's established convention, tests for 500 responses should assert that raw exception text does not leak into the body (e.g., assert body.error === "Internal server error" or that stack traces/DB errors are absent). Without this check, a future change that accidentally returns a dynamic error message would not be caught.

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

<comment>The 500-error test only checks `response.status === 500` and never verifies the response body. Per the team's established convention, tests for 500 responses should assert that raw exception text does not leak into the body (e.g., assert `body.error === "Internal server error"` or that stack traces/DB errors are absent). Without this check, a future change that accidentally returns a dynamic error message would not be caught.</comment>

<file context>
@@ -0,0 +1,115 @@
+
+    const response = await getCatalogValuationsHandler(makeRequest(), catalogId);
+
+    expect(response.status).toBe(500);
+  });
+});
</file context>

await persistCatalogValuation({
catalogId: catalog.id,
valuation,
measuredSongCount: aggregate?.measuredSongCount ?? 0,

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: When selectCatalogMeasurementsAggregate returns null (DB error), the persist call silently writes a zero-band valuation ({low:0, mid:0, high:0}) with measuredSongCount: 0, totalStreams: 0 into the valuation history, creating a misleading entry that looks like a legitimate measurement. The sibling handler getCatalogMeasurementsHandler guards against null aggregate with an early 500 return, and even the same file's captureValuationLead call uses ?? undefined (meaning "unknown") rather than ?? 0 (which conflates "unknown" with "known zero"). Since the design intent is best-effort, consider skipping the persist when the aggregate query itself failed — that avoids polluting the history with a false zero-band row without adding a hard failure path.

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

<comment>When `selectCatalogMeasurementsAggregate` returns null (DB error), the persist call silently writes a zero-band valuation (`{low:0, mid:0, high:0}`) with `measuredSongCount: 0, totalStreams: 0` into the valuation history, creating a misleading entry that looks like a legitimate measurement. The sibling handler `getCatalogMeasurementsHandler` guards against null aggregate with an early 500 return, and even the same file's `captureValuationLead` call uses `?? undefined` (meaning "unknown") rather than `?? 0` (which conflates "unknown" with "known zero"). Since the design intent is best-effort, consider skipping the persist when the aggregate query itself failed — that avoids polluting the history with a false zero-band row without adding a hard failure path.</comment>

<file context>
@@ -142,6 +143,16 @@ export async function runValuationHandler(request: NextRequest): Promise<NextRes
+    await persistCatalogValuation({
+      catalogId: catalog.id,
+      valuation,
+      measuredSongCount: aggregate?.measuredSongCount ?? 0,
+      totalStreams: aggregate?.totalStreams ?? 0,
+    });
</file context>

expect((result as NextResponse).status).toBe(400);
});

it("rejects limit above 100 with 400", async () => {

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: Missing test coverage for the lower limit boundary. The schema rejects limit=0 and limit=-1 with 400, but only the upper boundary (limit=101) and non-numeric input are tested. Add a test for limit=0 or limit=-1 to match the boundary coverage on the lower end.

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

<comment>Missing test coverage for the lower limit boundary. The schema rejects limit=0 and limit=-1 with 400, but only the upper boundary (limit=101) and non-numeric input are tested. Add a test for limit=0 or limit=-1 to match the boundary coverage on the lower end.</comment>

<file context>
@@ -0,0 +1,74 @@
+    expect((result as NextResponse).status).toBe(400);
+  });
+
+  it("rejects limit above 100 with 400", async () => {
+    okAuth();
+
</file context>

request: NextRequest,
catalogId: string,
): Promise<NextResponse | GetCatalogValuationsQuery> {
const authResult = await validateAuthContext(request);

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: Catalog query validation flow is now maintained in two near-identical implementations, so auth/error-envelope changes can drift between measurements and valuations. Consider extracting the shared auth/parse/error handling while keeping each endpoint schema local.

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

<comment>Catalog query validation flow is now maintained in two near-identical implementations, so auth/error-envelope changes can drift between measurements and valuations. Consider extracting the shared auth/parse/error handling while keeping each endpoint schema local.</comment>

<file context>
@@ -0,0 +1,65 @@
+  request: NextRequest,
+  catalogId: string,
+): Promise<NextResponse | GetCatalogValuationsQuery> {
+  const authResult = await validateAuthContext(request);
+  if (authResult instanceof NextResponse) return authResult;
+
</file context>

…ations

Every valuation run and each first whole-catalog measurements read of the
day now writes a row to catalog_valuations, and the new GET returns the
series latest-first (limit=1 = current value). chat#1889 row 15.

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

@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 (5)
lib/supabase/catalog_valuations/selectCatalogValuations.ts (1)

12-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the inline input shape out of this helper.

This function spans 21 lines solely because its parameter type is inline. Extract it to keep the function below the 20-line limit.

Proposed refactor
+type SelectCatalogValuationsParams = {
+  catalogId: string;
+  limit: number;
+};
+
 export async function selectCatalogValuations({
   catalogId,
   limit,
-}: {
-  catalogId: string;
-  limit: number;
-}): Promise<Tables<"catalog_valuations">[] | null> {
+}: SelectCatalogValuationsParams): Promise<Tables<"catalog_valuations">[] | null> {

As per coding guidelines, “Flag functions longer than 20 lines.”

🤖 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/supabase/catalog_valuations/selectCatalogValuations.ts` around lines 12 -
18, Extract the inline `{ catalogId: string; limit: number }` parameter shape
from `selectCatalogValuations` into a named type or interface, then use that
symbol in the function signature while preserving the existing parameters and
return type.

Source: Coding guidelines

lib/catalog/validateGetCatalogValuationsQuery.ts (1)

39-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split authentication orchestration from query parsing.

This 27-line validator combines authentication, URL extraction, schema parsing, and error rendering. Extract a private query-parsing helper while preserving the current auth-first behavior.

As per coding guidelines, “Flag functions longer than 20 lines.”

🤖 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/validateGetCatalogValuationsQuery.ts` around lines 39 - 65, The
validateGetCatalogValuationsQuery function currently combines authentication
with query extraction, schema validation, and error rendering. Extract the
URL/query parsing and validation logic into a private helper, while keeping
validateGetCatalogValuationsQuery responsible for auth-first orchestration and
returning the same responses and validated data.

Source: Coding guidelines

lib/catalog/getCatalogValuationsHandler.ts (1)

23-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Decompose this handler into focused private helpers.

The 36-line handler performs access lookup, storage retrieval, response mapping, and error translation. Extract the row mapper and catalog-valuation loader so each function stays below the required limit.

As per coding guidelines, “Flag functions longer than 20 lines.”

🤖 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/getCatalogValuationsHandler.ts` around lines 23 - 58, Refactor
getCatalogValuationsHandler so it remains under 20 lines by extracting focused
private helpers: one helper to map a valuation row into the response shape and
another to load catalog valuations after the selectAccountCatalog access check,
including the existing not-found and storage-error responses. Preserve the
handler’s validation, error translation, and response behavior while reusing the
helpers for row mapping and retrieval.

Source: Coding guidelines

lib/valuation/runValuationHandler.ts (1)

149-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Normalize totalStreams once.

aggregate?.totalStreams ?? 0 is now duplicated across valuation, persistence, and lead capture. Store it in a local constant so those paths cannot diverge.

As per coding guidelines, “Use constants for repeated values.”

🤖 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/valuation/runValuationHandler.ts` around lines 149 - 154, Define a local
constant in the valuation flow for the normalized total stream count using
aggregate?.totalStreams ?? 0, then reuse it in the valuation,
persistCatalogValuation, and lead-capture paths. Remove the duplicated inline
expressions while preserving the existing zero fallback.

Source: Coding guidelines

lib/catalog/persistCatalogValuation.ts (1)

20-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split the oversized functions into focused collaborators.

The modified functions exceed the repository’s function-size limits; adding persistence increases their responsibilities.

  • lib/catalog/persistCatalogValuation.ts#L20-L52: extract daily-deduplication lookup from insert/error-handling orchestration.
  • lib/valuation/runValuationHandler.ts#L146-L155: extract valuation-history persistence orchestration from the request workflow.
  • lib/catalog/getCatalogMeasurementsHandler.ts#L61-L72: extract history-write orchestration from measurement retrieval/response construction.

As per coding guidelines, “Flag functions longer than 20 lines.” As per path instructions, “Keep functions under 50 lines.”

🤖 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/persistCatalogValuation.ts` around lines 20 - 52, Split the
oversized persistence responsibilities into focused collaborators: in
lib/catalog/persistCatalogValuation.ts lines 20-52, extract the
daily-deduplication lookup from persistCatalogValuation while keeping insertion
and error handling orchestration intact; in lib/valuation/runValuationHandler.ts
lines 146-155, extract valuation-history persistence from the request workflow;
in lib/catalog/getCatalogMeasurementsHandler.ts lines 61-72, extract
history-write orchestration from measurement retrieval and response
construction. Keep each affected function under the repository’s size limits and
preserve existing behavior.

Sources: Coding guidelines, 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/getCatalogValuationsHandler.ts`:
- Around line 23-58: Refactor getCatalogValuationsHandler so it remains under 20
lines by extracting focused private helpers: one helper to map a valuation row
into the response shape and another to load catalog valuations after the
selectAccountCatalog access check, including the existing not-found and
storage-error responses. Preserve the handler’s validation, error translation,
and response behavior while reusing the helpers for row mapping and retrieval.

In `@lib/catalog/persistCatalogValuation.ts`:
- Around line 20-52: Split the oversized persistence responsibilities into
focused collaborators: in lib/catalog/persistCatalogValuation.ts lines 20-52,
extract the daily-deduplication lookup from persistCatalogValuation while
keeping insertion and error handling orchestration intact; in
lib/valuation/runValuationHandler.ts lines 146-155, extract valuation-history
persistence from the request workflow; in
lib/catalog/getCatalogMeasurementsHandler.ts lines 61-72, extract history-write
orchestration from measurement retrieval and response construction. Keep each
affected function under the repository’s size limits and preserve existing
behavior.

In `@lib/catalog/validateGetCatalogValuationsQuery.ts`:
- Around line 39-65: The validateGetCatalogValuationsQuery function currently
combines authentication with query extraction, schema validation, and error
rendering. Extract the URL/query parsing and validation logic into a private
helper, while keeping validateGetCatalogValuationsQuery responsible for
auth-first orchestration and returning the same responses and validated data.

In `@lib/supabase/catalog_valuations/selectCatalogValuations.ts`:
- Around line 12-18: Extract the inline `{ catalogId: string; limit: number }`
parameter shape from `selectCatalogValuations` into a named type or interface,
then use that symbol in the function signature while preserving the existing
parameters and return type.

In `@lib/valuation/runValuationHandler.ts`:
- Around line 149-154: Define a local constant in the valuation flow for the
normalized total stream count using aggregate?.totalStreams ?? 0, then reuse it
in the valuation, persistCatalogValuation, and lead-capture paths. Remove the
duplicated inline expressions while preserving the existing zero fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e6baf6a0-1115-417e-9994-baee849c8efe

📥 Commits

Reviewing files that changed from the base of the PR and between 6bbc7ca and cc0511d.

⛔ Files ignored due to path filters (7)
  • lib/catalog/__tests__/getCatalogMeasurementsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/getCatalogValuationsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/persistCatalogValuation.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/validateGetCatalogValuationsQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/catalog_valuations/__tests__/insertCatalogValuation.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/catalog_valuations/__tests__/selectCatalogValuations.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • types/database.types.ts is excluded by none and included by none
📒 Files selected for processing (8)
  • app/api/catalogs/[catalogId]/valuations/route.ts
  • lib/catalog/getCatalogMeasurementsHandler.ts
  • lib/catalog/getCatalogValuationsHandler.ts
  • lib/catalog/persistCatalogValuation.ts
  • lib/catalog/validateGetCatalogValuationsQuery.ts
  • lib/supabase/catalog_valuations/insertCatalogValuation.ts
  • lib/supabase/catalog_valuations/selectCatalogValuations.ts
  • lib/valuation/runValuationHandler.ts

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — 2026-07-30, all documented paths live

Preview https://api-ay0k0eaep-recoup.vercel.app, confirmed built from the rebased PR head cc0511d0 (deployment queried by sha). Credential: a fresh agent account minted on the preview itself via POST /api/agents/signup (per the docs recipe), so the walk runs on an isolated account with a preview-salted key. DB cross-checks via SQL against the shared prod database.

# Path Documented Actual Result
1 GET .../valuations bare 401 401, Exactly one of x-api-key or Authorization must be provided, no secrets echoed
2 non-UUID catalogId, bare 401 (auth before validation) 401
3 OPTIONS preflight 200 200
4 non-UUID catalogId, authed 400 400 catalogId must be a valid UUID
5 limit=0 / limit=101 / limit=abc 400 400 / 400 / 400
6 nonexistent uuid 404 404 Catalog not found
7 IDOR: real catalog owned by another account (325a0fa0…) 404, indistinguishable from missing 404, identical envelope to #6
8 Run-persist: POST /api/valuation (Del Water Gap, 67 songs) every run writes a history row run returned band {low: 1,426,019, mid: 2,076,284, high: 2,920,488}; GET .../valuations immediately showed exactly that row (total_streams: 562,418,832, measured_song_count: 67)
9 Read-persist: deleted the row via SQL, then GET .../measurements whole-catalog read persists when no row exists today row re-minted with the identical band
10 Daily dedupe: second measurements read, same UTC day no new row row count stayed 1
11 Day boundary: backdated the row to 2026-07-29 via SQL, read again a new row for today series became 2 rows
12 Ordering + limit=1 latest-first; limit=1 = current value 2026-07-30 row above 2026-07-29; limit=1 returned only the newest
13 DB cross-check writes scoped to the test catalog catalog_valuations total rows = 2, both on the test catalog a22ab197… — no stray writes anywhere else

Docs ↔ API ↔ live reconciliation: response field names, envelope ({status, valuations}), status codes and limit semantics all match docs#279 exactly — no drift found, nothing to patch.

Bonus: rows 8–12 accidentally demo the product story — the series reads $2.00M → $2.08M day-over-day, which is precisely the delta api#799 will put in the report subject.

Test debris: the agent account + its Del Water Gap catalog + 2 valuation rows remain (harmless; happy to clean up post-merge). Merge order: docs#279 ✅ → database#52 ✅ → this.

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