File size: 10,556 Bytes
18b9531
ffc5e7c
18b9531
ffc5e7c
18b9531
 
416c2e2
 
 
 
 
 
 
 
 
05035ed
 
 
416c2e2
 
 
 
 
 
ffc5e7c
18b9531
ffc5e7c
18b9531
ffc5e7c
18b9531
 
 
 
 
 
7c6a236
 
18b9531
 
 
 
 
 
 
 
 
 
 
 
7c6a236
 
18b9531
dbe3108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18b9531
 
 
 
 
 
 
 
 
 
 
7c6a236
 
18b9531
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f411df
18b9531
 
 
 
 
 
 
 
 
 
 
7c6a236
 
 
9f411df
18b9531
ffc5e7c
b24c2ae
 
ffc5e7c
 
 
 
 
 
b24c2ae
ffc5e7c
 
 
 
 
 
b24c2ae
 
 
ffc5e7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18b9531
 
 
f565e50
 
 
 
 
18b9531
 
 
 
f565e50
18b9531
 
7c6a236
18b9531
 
 
7c6a236
18b9531
 
 
 
 
 
 
dbe3108
18b9531
 
 
ffc5e7c
 
 
18b9531
 
ffc5e7c
 
 
 
 
 
 
 
 
18b9531
ffc5e7c
 
 
 
 
 
 
 
18b9531
 
ffc5e7c
 
18b9531
ffc5e7c
18b9531
 
ffc5e7c
 
18b9531
ffc5e7c
18b9531
 
ffc5e7c
 
 
18b9531
ffc5e7c
18b9531
 
ffc5e7c
 
18b9531
dbe3108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffc5e7c
 
18b9531
 
ffc5e7c
18b9531
 
 
ffc5e7c
 
 
 
 
 
 
18b9531
 
 
ffc5e7c
 
18b9531
 
ffc5e7c
18b9531
 
 
ffc5e7c
 
 
 
 
 
 
18b9531
 
ffc5e7c
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
-- Portfolio Intelligence Platform Database Schema
-- For Supabase PostgreSQL with Auth Integration

-- Enable UUID extension (if not already enabled)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Helper function to convert strings to UUIDs (for demo portfolio IDs)
CREATE OR REPLACE FUNCTION string_to_portfolio_uuid(input_string TEXT)
RETURNS UUID
LANGUAGE plpgsql
IMMUTABLE
SECURITY INVOKER
SET search_path = ''
AS $$
BEGIN
    -- Use URL namespace UUID constant (6ba7b811-9dad-11d1-80b4-00c04fd430c8)
    -- uuid_ns_url() is a constant in PostgreSQL, not a function
    RETURN public.uuid_generate_v5('6ba7b811-9dad-11d1-80b4-00c04fd430c8'::uuid, input_string);
END;
$$;

-- Grant execute to authenticated and anon roles
GRANT EXECUTE ON FUNCTION string_to_portfolio_uuid TO authenticated, anon;

-- Users table (linked to Supabase Auth - CRITICAL FIX)
CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
    email VARCHAR(255) UNIQUE NOT NULL,
    username VARCHAR(100),
    is_active BOOLEAN DEFAULT true,
    is_demo BOOLEAN DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_users_id ON users(id);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

-- Portfolios table
CREATE TABLE IF NOT EXISTS portfolios (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    risk_tolerance VARCHAR(20) DEFAULT 'moderate',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_portfolios_user_id ON portfolios(user_id);
CREATE INDEX IF NOT EXISTS idx_portfolios_created_at ON portfolios(created_at DESC);

-- Portfolio inputs table (for auto-save history - keeps last 3 per user)
CREATE TABLE IF NOT EXISTS portfolio_inputs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    description TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_portfolio_inputs_user_created ON portfolio_inputs(user_id, created_at DESC);

-- Trigger function to limit portfolio inputs to 3 per user
CREATE OR REPLACE FUNCTION limit_portfolio_inputs_per_user()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = ''
AS $$
BEGIN
    -- Keep only the 3 most recent entries per user
    DELETE FROM public.portfolio_inputs
    WHERE id IN (
        SELECT id
        FROM (
            SELECT id,
                   ROW_NUMBER() OVER (
                       PARTITION BY user_id
                       ORDER BY created_at DESC
                   ) AS row_num
            FROM public.portfolio_inputs
            WHERE user_id = NEW.user_id
        ) ranked
        WHERE row_num > 3
    );

    RETURN NULL;
END;
$$;

-- Create trigger for automatic cleanup
DROP TRIGGER IF EXISTS trigger_limit_portfolio_inputs ON portfolio_inputs;
CREATE TRIGGER trigger_limit_portfolio_inputs
    AFTER INSERT ON portfolio_inputs
    FOR EACH ROW EXECUTE FUNCTION limit_portfolio_inputs_per_user();

-- Portfolio holdings table
CREATE TABLE IF NOT EXISTS portfolio_holdings (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
    ticker VARCHAR(20) NOT NULL,
    quantity NUMERIC(20, 8) NOT NULL,
    cost_basis NUMERIC(20, 2),
    asset_type VARCHAR(20) DEFAULT 'stock',
    added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_holdings_portfolio_id ON portfolio_holdings(portfolio_id);
CREATE INDEX IF NOT EXISTS idx_holdings_ticker ON portfolio_holdings(ticker);

-- Portfolio analyses table
CREATE TABLE IF NOT EXISTS portfolio_analyses (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE,
    analysis_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

    -- Analysis results (JSONB for flexibility)
    holdings_snapshot JSONB NOT NULL,
    market_data JSONB,
    risk_metrics JSONB,
    optimisation_results JSONB,
    ai_synthesis TEXT,
    recommendations JSONB,

    -- Metadata
    reasoning_steps JSONB,
    mcp_calls JSONB,
    sentiment_data JSONB,
    execution_time_ms INTEGER,
    model_version VARCHAR(50),

    -- Computed fields
    total_value NUMERIC(20, 2),
    health_score INTEGER CHECK (health_score >= 0 AND health_score <= 10),
    risk_level VARCHAR(20),

    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_analyses_portfolio_id ON portfolio_analyses(portfolio_id);
CREATE INDEX IF NOT EXISTS idx_analyses_date ON portfolio_analyses(analysis_date DESC);
CREATE INDEX IF NOT EXISTS idx_analyses_health_score ON portfolio_analyses(health_score);
CREATE INDEX IF NOT EXISTS idx_analyses_sentiment_data ON portfolio_analyses USING GIN (sentiment_data);

-- Function to automatically create user profile on signup (CRITICAL FOR AUTH)
-- Uses SECURITY DEFINER with empty search_path to bypass RLS on INSERT
-- Fully qualifies table name (public.users) to work with empty search_path
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = ''
AS $$
BEGIN
    INSERT INTO public.users (id, email, username, is_active, is_demo)
    VALUES (
        NEW.id,
        NEW.email,
        COALESCE(
            NEW.raw_user_meta_data ->> 'username',
            split_part(NEW.email, '@', 1)
        ),
        true,
        false
    );
    RETURN NEW;
EXCEPTION
    WHEN OTHERS THEN
        -- Log error but don't block signup
        RAISE WARNING 'Failed to create user profile: %', SQLERRM;
        RETURN NEW;
END;
$$;

-- Create trigger for automatic profile creation
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
    AFTER INSERT ON auth.users
    FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = ''
AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$;

-- Triggers for updated_at
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

DROP TRIGGER IF EXISTS update_portfolios_updated_at ON portfolios;
CREATE TRIGGER update_portfolios_updated_at BEFORE UPDATE ON portfolios
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

-- Grant permissions (Supabase uses RLS - Row Level Security)
-- These are basic permissions; adjust based on your auth setup
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolios ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolio_inputs ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolio_holdings ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolio_analyses ENABLE ROW LEVEL SECURITY;

-- RLS policies with performance optimizations (wrapped auth.uid() = 20x faster)
-- Users table policies
DROP POLICY IF EXISTS users_read_own ON users;
CREATE POLICY users_read_own ON users
    FOR SELECT
    TO authenticated
    USING ((SELECT auth.uid()) = id);

DROP POLICY IF EXISTS users_update_own ON users;
CREATE POLICY users_update_own ON users
    FOR UPDATE
    TO authenticated
    USING ((SELECT auth.uid()) = id)
    WITH CHECK ((SELECT auth.uid()) = id);

DROP POLICY IF EXISTS users_insert_own ON users;
CREATE POLICY users_insert_own ON users
    FOR INSERT
    TO authenticated
    WITH CHECK ((SELECT auth.uid()) = id);

-- Portfolios table policies
DROP POLICY IF EXISTS portfolios_read_own ON portfolios;
CREATE POLICY portfolios_read_own ON portfolios
    FOR SELECT
    TO authenticated
    USING ((SELECT auth.uid()) = user_id);

DROP POLICY IF EXISTS portfolios_insert_own ON portfolios;
CREATE POLICY portfolios_insert_own ON portfolios
    FOR INSERT
    TO authenticated
    WITH CHECK ((SELECT auth.uid()) = user_id);

DROP POLICY IF EXISTS portfolios_update_own ON portfolios;
CREATE POLICY portfolios_update_own ON portfolios
    FOR UPDATE
    TO authenticated
    USING ((SELECT auth.uid()) = user_id)
    WITH CHECK ((SELECT auth.uid()) = user_id);

DROP POLICY IF EXISTS portfolios_delete_own ON portfolios;
CREATE POLICY portfolios_delete_own ON portfolios
    FOR DELETE
    TO authenticated
    USING ((SELECT auth.uid()) = user_id);

-- Portfolio inputs table policies
DROP POLICY IF EXISTS portfolio_inputs_read_own ON portfolio_inputs;
CREATE POLICY portfolio_inputs_read_own ON portfolio_inputs
    FOR SELECT
    TO authenticated
    USING ((SELECT auth.uid()) = user_id);

DROP POLICY IF EXISTS portfolio_inputs_insert_own ON portfolio_inputs;
CREATE POLICY portfolio_inputs_insert_own ON portfolio_inputs
    FOR INSERT
    TO authenticated
    WITH CHECK ((SELECT auth.uid()) = user_id);

DROP POLICY IF EXISTS portfolio_inputs_delete_own ON portfolio_inputs;
CREATE POLICY portfolio_inputs_delete_own ON portfolio_inputs
    FOR DELETE
    TO authenticated
    USING ((SELECT auth.uid()) = user_id);

-- Holdings policies (optimized subquery pattern)
DROP POLICY IF EXISTS holdings_access_own ON portfolio_holdings;
CREATE POLICY holdings_access_own ON portfolio_holdings
    FOR ALL
    TO authenticated
    USING (
        portfolio_id IN (
            SELECT id FROM portfolios
            WHERE user_id = (SELECT auth.uid())
        )
    )
    WITH CHECK (
        portfolio_id IN (
            SELECT id FROM portfolios
            WHERE user_id = (SELECT auth.uid())
        )
    );

-- Analyses policies
DROP POLICY IF EXISTS analyses_access_own ON portfolio_analyses;
CREATE POLICY analyses_access_own ON portfolio_analyses
    FOR ALL
    TO authenticated
    USING (
        portfolio_id IN (
            SELECT id FROM portfolios
            WHERE user_id = (SELECT auth.uid())
        )
    )
    WITH CHECK (
        portfolio_id IN (
            SELECT id FROM portfolios
            WHERE user_id = (SELECT auth.uid())
        )
    );

-- Grant permissions
GRANT USAGE ON SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated;