From 76c03c01b10c66698239d16ad02c469546f838cd Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 29 Jul 2026 14:29:17 -0500 Subject: [PATCH] feat: catalog_valuations - persisted valuation history per catalog chat#1889 row 15 (keystone). One row per band computation; read is always latest-first per catalog, hence the (catalog_id, measured_at DESC) index. FK CASCADE to catalogs: a valuation row is meaningless without its catalog. Contract: docs#279. Co-Authored-By: Claude Opus 5 (1M context) --- ...260729230000_create_catalog_valuations.sql | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 supabase/migrations/20260729230000_create_catalog_valuations.sql diff --git a/supabase/migrations/20260729230000_create_catalog_valuations.sql b/supabase/migrations/20260729230000_create_catalog_valuations.sql new file mode 100644 index 0000000..29c9923 --- /dev/null +++ b/supabase/migrations/20260729230000_create_catalog_valuations.sql @@ -0,0 +1,35 @@ +-- Create catalog_valuations: persisted valuation history per catalog +-- (recoupable/chat#1889 row 15 — the keystone of the 2026-07-29 reorder). +-- +-- The estimated catalog value is currently recomputed live on every read +-- (GET /api/catalogs/{id}/measurements derives the band at read time) and +-- stored nowhere. That costs the product its retention hook — a weekly report +-- can never say "your number moved" without two numbers to compare — and costs +-- sales a sortable Attio Catalog Value field (today hand-priced per lead). +-- +-- One row per band computation, at most one per catalog per day from the +-- read path (valuation runs always insert). History is the point: columns on +-- catalogs would only ever hold the latest value. +-- +-- Written by api: runValuationHandler (funnel + onboarding seeding) and the +-- measurements read path (daily-deduped). Read by +-- GET /api/catalogs/{catalogId}/valuations (contract: docs#279). +-- +-- FK to catalogs with ON DELETE CASCADE: a valuation row is meaningless +-- without its catalog, unlike the loose-id bookkeeping tables. + +CREATE TABLE IF NOT EXISTS public.catalog_valuations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + catalog_id UUID NOT NULL REFERENCES public.catalogs(id) ON DELETE CASCADE, + low NUMERIC NOT NULL, -- band low, USD + mid NUMERIC NOT NULL, -- band midpoint, USD + high NUMERIC NOT NULL, -- band high, USD + measured_song_count INTEGER NOT NULL, -- songs measured in the underlying capture + total_streams BIGINT NOT NULL, -- whole-catalog lifetime streams at measurement + measured_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- The read is always "latest rows for one catalog". +CREATE INDEX IF NOT EXISTS catalog_valuations_catalog_measured_idx + ON public.catalog_valuations (catalog_id, measured_at DESC);