11/**
2- * Finance watchlist (PRD §3.1, §5) — profile-scoped tickers.
2+ * Finance watchlist items (PRD §3.1, §5) — tickers within a named list .
33 *
4- * GET /api/finance/watchlist — list the active profile 's tickers
5- * POST /api/finance/watchlist {symbol} — add a ticker
6- * DELETE /api/finance/watchlist?symbol= — remove a ticker
4+ * GET /api/finance/watchlist?watchlistId= — list a list 's tickers
5+ * POST /api/finance/watchlist {symbol|symbols, watchlistId? } — add ticker(s)
6+ * DELETE /api/finance/watchlist?symbol=&watchlistId= — remove a ticker
77 *
8- * Paid-gated. Rows are filtered by the active profile id; the table also has
9- * RLS as defense in depth.
8+ * Paid-gated. When `watchlistId` is omitted we fall back to the profile's
9+ * default (oldest) list, creating one if needed — so legacy single-list callers
10+ * keep working. Every query is scoped to the active profile (RLS in depth).
1011 */
1112
1213import { NextRequest , NextResponse } from 'next/server' ;
@@ -15,11 +16,23 @@ import { getActiveProfileId } from '@/lib/profiles/profile-utils';
1516import { getServerClient } from '@/lib/supabase' ;
1617import { normalizeSymbol } from '@/lib/finance/market-data/stooq' ;
1718import { parseSymbolList } from '@/lib/finance/watchlist' ;
19+ import { getOrCreateDefaultWatchlistId , ownsWatchlist } from '@/lib/finance/watchlist-db' ;
1820
1921export const dynamic = 'force-dynamic' ;
2022
2123const SYMBOL_RE = / ^ [ A - Z ] [ A - Z 0 - 9 . \- ] { 0 , 9 } $ / ;
2224
25+ /**
26+ * Resolve the target list for a profile. Returns the verified list id, or null
27+ * when an explicit (but unowned/unknown) id was supplied.
28+ */
29+ async function resolveWatchlistId ( profileId : string , requested : string | null ) : Promise < string | null > {
30+ if ( requested ) {
31+ return ( await ownsWatchlist ( profileId , requested ) ) ? requested : null ;
32+ }
33+ return getOrCreateDefaultWatchlistId ( profileId ) ;
34+ }
35+
2336export async function GET ( request : NextRequest ) : Promise < NextResponse > {
2437 const gate = await requireActiveSubscription ( request ) ;
2538 if ( gate ) return gate ;
@@ -29,18 +42,23 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
2942 return NextResponse . json ( { error : 'No active profile' } , { status : 400 } ) ;
3043 }
3144
45+ const watchlistId = await resolveWatchlistId ( profileId , request . nextUrl . searchParams . get ( 'watchlistId' ) ) ;
46+ if ( ! watchlistId ) {
47+ return NextResponse . json ( { error : 'watchlist not found' } , { status : 404 } ) ;
48+ }
49+
3250 const { data, error } = await getServerClient ( )
3351 . from ( 'finance_watchlist' )
3452 . select ( 'id, symbol, exchange, added_at' )
35- . eq ( 'profile_id ' , profileId )
53+ . eq ( 'watchlist_id ' , watchlistId )
3654 . order ( 'added_at' , { ascending : false } ) ;
3755
3856 if ( error ) {
3957 console . error ( '[finance/watchlist] list error:' , error ) ;
4058 return NextResponse . json ( { error : 'failed to load watchlist' } , { status : 500 } ) ;
4159 }
4260
43- return NextResponse . json ( { watchlist : data ?? [ ] } ) ;
61+ return NextResponse . json ( { watchlistId , watchlist : data ?? [ ] } ) ;
4462}
4563
4664export async function POST ( request : NextRequest ) : Promise < NextResponse > {
@@ -53,21 +71,28 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
5371 }
5472
5573 const body = ( await request . json ( ) . catch ( ( ) => null ) ) as
56- | { symbol ?: string ; symbols ?: string | string [ ] ; exchange ?: string }
74+ | { symbol ?: string ; symbols ?: string | string [ ] ; exchange ?: string ; watchlistId ?: string }
5775 | null ;
5876
77+ const watchlistId = await resolveWatchlistId ( profileId , body ?. watchlistId ?? null ) ;
78+ if ( ! watchlistId ) {
79+ return NextResponse . json ( { error : 'watchlist not found' } , { status : 404 } ) ;
80+ }
81+
82+ const supabase = getServerClient ( ) ;
83+
5984 // Bulk add: `symbols` may be a comma/space/newline-separated string or array.
6085 if ( body ?. symbols !== undefined ) {
6186 const { valid, invalid } = parseSymbolList ( body . symbols ) ;
6287 if ( valid . length === 0 ) {
6388 return NextResponse . json ( { error : 'no valid symbols' , invalid } , { status : 400 } ) ;
6489 }
6590
66- const { data, error } = await getServerClient ( )
91+ const { data, error } = await supabase
6792 . from ( 'finance_watchlist' )
6893 . upsert (
69- valid . map ( ( symbol ) => ( { profile_id : profileId , symbol, exchange : null } ) ) ,
70- { onConflict : 'profile_id ,symbol' } ,
94+ valid . map ( ( symbol ) => ( { profile_id : profileId , watchlist_id : watchlistId , symbol, exchange : null } ) ) ,
95+ { onConflict : 'watchlist_id ,symbol' } ,
7196 )
7297 . select ( 'id, symbol, exchange, added_at' ) ;
7398
@@ -76,7 +101,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
76101 return NextResponse . json ( { error : 'failed to add symbols' } , { status : 500 } ) ;
77102 }
78103
79- return NextResponse . json ( { added : data ?? [ ] , count : data ?. length ?? 0 , invalid } , { status : 201 } ) ;
104+ return NextResponse . json ( { watchlistId , added : data ?? [ ] , count : data ?. length ?? 0 , invalid } , { status : 201 } ) ;
80105 }
81106
82107 // Single add.
@@ -85,11 +110,11 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
85110 return NextResponse . json ( { error : 'invalid symbol' } , { status : 400 } ) ;
86111 }
87112
88- const { data, error } = await getServerClient ( )
113+ const { data, error } = await supabase
89114 . from ( 'finance_watchlist' )
90115 . upsert (
91- { profile_id : profileId , symbol, exchange : body ?. exchange ?? null } ,
92- { onConflict : 'profile_id ,symbol' } ,
116+ { profile_id : profileId , watchlist_id : watchlistId , symbol, exchange : body ?. exchange ?? null } ,
117+ { onConflict : 'watchlist_id ,symbol' } ,
93118 )
94119 . select ( 'id, symbol, exchange, added_at' )
95120 . single ( ) ;
@@ -99,7 +124,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
99124 return NextResponse . json ( { error : 'failed to add symbol' } , { status : 500 } ) ;
100125 }
101126
102- return NextResponse . json ( { item : data } , { status : 201 } ) ;
127+ return NextResponse . json ( { watchlistId , item : data } , { status : 201 } ) ;
103128}
104129
105130export async function DELETE ( request : NextRequest ) : Promise < NextResponse > {
@@ -116,10 +141,15 @@ export async function DELETE(request: NextRequest): Promise<NextResponse> {
116141 return NextResponse . json ( { error : 'invalid symbol' } , { status : 400 } ) ;
117142 }
118143
144+ const watchlistId = await resolveWatchlistId ( profileId , request . nextUrl . searchParams . get ( 'watchlistId' ) ) ;
145+ if ( ! watchlistId ) {
146+ return NextResponse . json ( { error : 'watchlist not found' } , { status : 404 } ) ;
147+ }
148+
119149 const { error } = await getServerClient ( )
120150 . from ( 'finance_watchlist' )
121151 . delete ( )
122- . eq ( 'profile_id ' , profileId )
152+ . eq ( 'watchlist_id ' , watchlistId )
123153 . eq ( 'symbol' , symbol ) ;
124154
125155 if ( error ) {
0 commit comments