File size: 7,556 Bytes
0aaa5bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
-- ─────────────────────────────────────────────────────────────────────────────
-- LabCard AI β€” Supabase Schema
-- Run this in Supabase SQL Editor: https://supabase.com/dashboard/project/_/sql
-- ─────────────────────────────────────────────────────────────────────────────

-- ── User Profiles ─────────────────────────────────────────────────────────────
-- Extends auth.users β€” created automatically on first report / payment

CREATE TABLE IF NOT EXISTS public.user_profiles (
  id                   UUID         REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
  tier                 TEXT         NOT NULL DEFAULT 'free'
                                    CHECK (tier IN ('free', 'premium')),
  reports_analyzed     INTEGER      NOT NULL DEFAULT 0,
  premium_expires_at   TIMESTAMPTZ,
  created_at           TIMESTAMPTZ  DEFAULT NOW(),
  updated_at           TIMESTAMPTZ  DEFAULT NOW()
);

-- Auto-update updated_at on any change
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER user_profiles_updated_at
  BEFORE UPDATE ON public.user_profiles
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- ── Reports ───────────────────────────────────────────────────────────────────
-- Full LabReport JSON stored in report_json JSONB for future trend analysis.
-- Summary columns (score, grade, etc.) allow fast SQL aggregates without
-- parsing the full JSON every time.

CREATE TABLE IF NOT EXISTS public.reports (
  id                   UUID         DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id              UUID         REFERENCES auth.users(id) ON DELETE SET NULL,
  -- Extracted patient fields (denormalized for fast queries)
  patient_name         TEXT,
  patient_age          TEXT,
  patient_gender       TEXT,
  lab_name             TEXT,
  report_date          TEXT,
  -- Health metrics (all deterministic β€” not from AI)
  health_score         INTEGER      CHECK (health_score >= 0 AND health_score <= 100),
  health_grade         TEXT,
  biological_age       INTEGER,
  chronological_age    INTEGER,
  has_critical_alert   BOOLEAN      DEFAULT FALSE,
  -- Processing metadata
  tier_used            TEXT         DEFAULT 'free',
  biomarker_count      INTEGER,
  abnormal_count       INTEGER,
  processing_time_ms   INTEGER,
  -- Full report blob (enables trend queries + future re-analysis)
  report_json          JSONB        NOT NULL,
  created_at           TIMESTAMPTZ  DEFAULT NOW()
);

-- Indexes for common access patterns
CREATE INDEX IF NOT EXISTS idx_reports_user_id
  ON public.reports(user_id);

CREATE INDEX IF NOT EXISTS idx_reports_created_at
  ON public.reports(created_at DESC);

CREATE INDEX IF NOT EXISTS idx_reports_health_score
  ON public.reports(health_score);

-- GIN index on JSONB for fast biomarker trend queries
CREATE INDEX IF NOT EXISTS idx_reports_json
  ON public.reports USING GIN (report_json);

-- ── Payments ──────────────────────────────────────────────────────────────────
-- Audit trail for all Razorpay transactions.
-- razorpay_order_id is UNIQUE β€” idempotent upsert is safe.

CREATE TABLE IF NOT EXISTS public.payments (
  id                   UUID         DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id              UUID         REFERENCES auth.users(id) ON DELETE SET NULL,
  razorpay_order_id    TEXT         UNIQUE NOT NULL,
  razorpay_payment_id  TEXT         UNIQUE,
  razorpay_signature   TEXT,
  plan                 TEXT         NOT NULL
                                    CHECK (plan IN ('per_report', 'monthly')),
  amount_paise         INTEGER      NOT NULL,
  status               TEXT         NOT NULL DEFAULT 'pending'
                                    CHECK (status IN ('pending', 'success', 'failed')),
  verified_at          TIMESTAMPTZ,
  created_at           TIMESTAMPTZ  DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_payments_user_id
  ON public.payments(user_id);

-- ── Row Level Security ────────────────────────────────────────────────────────
-- Users can only read their own data.
-- Backend uses service role key β€” bypasses RLS for writes.

ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.reports       ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.payments      ENABLE ROW LEVEL SECURITY;

-- user_profiles: users see and update only their own row
CREATE POLICY "Users see own profile"
  ON public.user_profiles FOR ALL
  USING (auth.uid() = id);

-- reports: users can read their own reports
CREATE POLICY "Users see own reports"
  ON public.reports FOR SELECT
  USING (auth.uid() = user_id);

-- reports: backend service role inserts (WITH CHECK true allows service key)
CREATE POLICY "Service role inserts reports"
  ON public.reports FOR INSERT
  WITH CHECK (true);

-- payments: users can only see their own payment records
CREATE POLICY "Users see own payments"
  ON public.payments FOR SELECT
  USING (auth.uid() = user_id);

-- payments: backend service role inserts
CREATE POLICY "Service role inserts payments"
  ON public.payments FOR INSERT
  WITH CHECK (true);

-- ── Helper: increment reports_analyzed counter ────────────────────────────────
-- Called via trigger when a new report is inserted

CREATE OR REPLACE FUNCTION increment_reports_analyzed()
RETURNS TRIGGER AS $$
BEGIN
  IF NEW.user_id IS NOT NULL THEN
    INSERT INTO public.user_profiles (id, reports_analyzed)
      VALUES (NEW.user_id, 1)
      ON CONFLICT (id) DO UPDATE
        SET reports_analyzed = user_profiles.reports_analyzed + 1,
            updated_at = NOW();
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_report_inserted
  AFTER INSERT ON public.reports
  FOR EACH ROW EXECUTE FUNCTION increment_reports_analyzed();

-- ── Trend analysis view (bonus β€” useful for future dashboard) ─────────────────
-- Returns latest health score per user for trend charts

CREATE OR REPLACE VIEW public.user_health_trends AS
SELECT
  r.user_id,
  r.id           AS report_id,
  r.created_at,
  r.health_score,
  r.health_grade,
  r.biological_age,
  r.chronological_age,
  r.abnormal_count,
  r.lab_name
FROM public.reports r
WHERE r.user_id IS NOT NULL
ORDER BY r.user_id, r.created_at DESC;

-- ── Verify setup ──────────────────────────────────────────────────────────────
SELECT
  schemaname,
  tablename,
  tableowner
FROM pg_tables
WHERE schemaname = 'public'
  AND tablename IN ('user_profiles', 'reports', 'payments');