Files
pragativeer-v2/migrations/001_full_schema_sync.sql

382 lines
15 KiB
SQL

-- =============================================================================
-- Pragativeer Full Schema Sync + Demo User Seed Migration
-- File: migrations/001_full_schema_sync.sql
--
-- HOW TO RUN:
-- Windows: .\migrate.bat migrations\001_full_schema_sync.sql
-- Linux/Mac (server): ./migrate.sh migrations/001_full_schema_sync.sql
--
-- This script is 100% idempotent (safe to run multiple times).
-- Uses IF NOT EXISTS everywhere so already-applied changes are silently skipped.
-- Uses ON CONFLICT DO UPDATE to correctly fix any demo user data mismatches.
-- =============================================================================
-- ── ENUM TYPES ─────────────────────────────────────────────────────────────────
-- All enum values in DB are UPPERCASE (e.g. FREE, PRO, AI_COACH, JEE_MAIN)
DO $$ BEGIN
CREATE TYPE classenum AS ENUM ('ELEVENTH', 'TWELFTH', 'DROPPER');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE examenum AS ENUM ('JEE_MAIN', 'JEE_ADVANCED', 'NEET', 'JEE_NEET');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TYPE examenum ADD VALUE IF NOT EXISTS 'JEE_NEET';
EXCEPTION WHEN others THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE mediumenum AS ENUM ('ENGLISH', 'HINDI', 'BILINGUAL');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE tierenum AS ENUM ('FREE', 'PRO', 'AI_COACH');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE questiontypeenum AS ENUM ('MCQ_SINGLE', 'MCQ_MULTIPLE', 'NUMERICAL');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE sourcetypeenum AS ENUM ('DPS', 'TEST', 'MICROTEST');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE errortypeenum AS ENUM ('CONCEPTUAL', 'CALCULATION', 'READING', 'TIME_PRESSURE');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- ── TABLE: user ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS "user" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE,
phone TEXT UNIQUE,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
student_class classenum NOT NULL,
target_exam examenum NOT NULL,
medium mediumenum NOT NULL,
school TEXT,
city TEXT NOT NULL,
state TEXT NOT NULL,
tier tierenum NOT NULL DEFAULT 'FREE',
trial_end TIMESTAMP WITHOUT TIME ZONE,
subscription_end TIMESTAMP WITHOUT TIME ZONE,
profile_image_url TEXT,
streak_count INTEGER NOT NULL DEFAULT 0,
longest_streak INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
last_active TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
active_session_token TEXT
);
ALTER TABLE "user"
ADD COLUMN IF NOT EXISTS trial_end TIMESTAMP WITHOUT TIME ZONE,
ADD COLUMN IF NOT EXISTS subscription_end TIMESTAMP WITHOUT TIME ZONE,
ADD COLUMN IF NOT EXISTS profile_image_url TEXT,
ADD COLUMN IF NOT EXISTS streak_count INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS longest_streak INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS active_session_token TEXT,
ADD COLUMN IF NOT EXISTS school TEXT;
-- ── TABLE: parent ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS parent (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
phone TEXT NOT NULL,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
language TEXT NOT NULL DEFAULT 'English',
linked_students JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
ALTER TABLE parent
ADD COLUMN IF NOT EXISTS language TEXT NOT NULL DEFAULT 'English',
ADD COLUMN IF NOT EXISTS linked_students JSONB NOT NULL DEFAULT '[]';
-- ── TABLE: question ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS question (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
subject TEXT NOT NULL,
topic TEXT NOT NULL,
subtopic TEXT NOT NULL,
difficulty TEXT NOT NULL,
type questiontypeenum NOT NULL,
question_text TEXT NOT NULL,
question_text_hi TEXT,
options JSONB,
options_hi JSONB,
correct_answer JSONB NOT NULL,
solution_text TEXT NOT NULL,
solution_text_hi TEXT,
language mediumenum NOT NULL,
year INTEGER,
exam examenum,
tags TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by UUID NOT NULL,
verified_by UUID
);
ALTER TABLE question
ADD COLUMN IF NOT EXISTS question_text_hi TEXT,
ADD COLUMN IF NOT EXISTS options_hi JSONB,
ADD COLUMN IF NOT EXISTS solution_text_hi TEXT,
ADD COLUMN IF NOT EXISTS tags TEXT,
ADD COLUMN IF NOT EXISTS verified_by UUID;
-- ── TABLE: dps ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS dps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
date DATE NOT NULL,
title TEXT NOT NULL,
questions JSONB NOT NULL DEFAULT '[]',
difficulty TEXT NOT NULL,
time_limit INTEGER NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by UUID NOT NULL
);
-- ── TABLE: test ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS test (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
type TEXT NOT NULL,
exam_pattern examenum NOT NULL,
sections JSONB NOT NULL,
total_time INTEGER NOT NULL,
difficulty TEXT NOT NULL,
scheduled_date TIMESTAMP WITHOUT TIME ZONE,
end_date TIMESTAMP WITHOUT TIME ZONE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by UUID NOT NULL
);
ALTER TABLE test
ADD COLUMN IF NOT EXISTS end_date TIMESTAMP WITHOUT TIME ZONE;
-- ── TABLE: transaction ────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS transaction (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "user"(id),
amount DOUBLE PRECISION NOT NULL,
tier TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
-- ── TABLE: coupon ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS coupon (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
discount_percent DOUBLE PRECISION NOT NULL,
max_discount_amount DOUBLE PRECISION NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
expires_at TIMESTAMP WITHOUT TIME ZONE,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
-- ── TABLE: attempt ────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS attempt (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "user"(id),
source_type sourcetypeenum NOT NULL,
source_id UUID NOT NULL,
started_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
submitted_at TIMESTAMP WITHOUT TIME ZONE,
time_taken INTEGER,
answers JSONB,
score INTEGER NOT NULL DEFAULT 0,
total_questions INTEGER NOT NULL,
accuracy DOUBLE PRECISION NOT NULL DEFAULT 0.0,
status TEXT NOT NULL DEFAULT 'In_Progress',
is_retake BOOLEAN NOT NULL DEFAULT FALSE
);
ALTER TABLE attempt
ADD COLUMN IF NOT EXISTS is_retake BOOLEAN NOT NULL DEFAULT FALSE;
-- ── TABLE: mistake ────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS mistake (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "user"(id),
question_id UUID NOT NULL REFERENCES question(id),
attempt_id UUID NOT NULL REFERENCES attempt(id),
error_type errortypeenum NOT NULL,
topic TEXT NOT NULL,
subtopic TEXT NOT NULL,
next_review TIMESTAMP WITHOUT TIME ZONE NOT NULL,
review_count INTEGER NOT NULL DEFAULT 0,
consecutive_correct INTEGER NOT NULL DEFAULT 0,
is_mastered BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
ALTER TABLE mistake
ADD COLUMN IF NOT EXISTS consecutive_correct INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS is_mastered BOOLEAN NOT NULL DEFAULT FALSE;
-- ── TABLE: studyplan ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS studyplan (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "user"(id),
date DATE NOT NULL,
tasks JSONB NOT NULL,
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
generated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
feedback TEXT
);
ALTER TABLE studyplan
ADD COLUMN IF NOT EXISTS feedback TEXT;
-- ── TABLE: subscription ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS subscription (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES "user"(id),
tier tierenum NOT NULL,
billing_cycle TEXT NOT NULL,
start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
end_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
amount INTEGER NOT NULL,
payment_id TEXT NOT NULL,
status TEXT NOT NULL,
auto_renew BOOLEAN NOT NULL DEFAULT TRUE,
coupon_code TEXT
);
ALTER TABLE subscription
ADD COLUMN IF NOT EXISTS coupon_code TEXT,
ADD COLUMN IF NOT EXISTS auto_renew BOOLEAN NOT NULL DEFAULT TRUE;
-- ── TABLE: systemsettings ─────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS systemsettings (
id TEXT PRIMARY KEY DEFAULT 'global',
maintenance_mode BOOLEAN NOT NULL DEFAULT FALSE,
allow_new_registrations BOOLEAN NOT NULL DEFAULT TRUE,
enable_free_trial BOOLEAN NOT NULL DEFAULT TRUE,
trial_duration_days INTEGER NOT NULL DEFAULT 15,
trial_tier tierenum NOT NULL DEFAULT 'AI_COACH',
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
);
ALTER TABLE systemsettings
ADD COLUMN IF NOT EXISTS enable_free_trial BOOLEAN NOT NULL DEFAULT TRUE,
ADD COLUMN IF NOT EXISTS trial_duration_days INTEGER NOT NULL DEFAULT 15,
ADD COLUMN IF NOT EXISTS trial_tier tierenum NOT NULL DEFAULT 'AI_COACH';
INSERT INTO systemsettings (id, maintenance_mode, allow_new_registrations, enable_free_trial, trial_duration_days, trial_tier, updated_at)
VALUES ('global', FALSE, TRUE, TRUE, 15, 'AI_COACH', now())
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- DEMO USER SEED / FIX
-- These 4 rows are the canonical demo accounts used by the frontend switcher.
-- ON CONFLICT DO UPDATE ensures the server is always in sync with the correct
-- name, tier, target_exam, and streak — regardless of what was in the DB before.
-- password_hash below is bcrypt of "PragatiVeer@Demo2026" (12 rounds).
-- =============================================================================
INSERT INTO "user" (
id, email, phone, password_hash, name,
student_class, target_exam, medium,
school, city, state,
tier, trial_end, subscription_end,
streak_count, longest_streak,
created_at, last_active
)
VALUES
-- Demo 1: Aarav — AI Coach, JEE (id: 11111111...)
(
'11111111-1111-1111-1111-111111111111',
'aarav.demo@pragativeer.com',
'9911111111',
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMoJ8zYTvNiG0HGkY0P6YOHRV6',
'Aarav',
'TWELFTH', 'JEE_MAIN', 'ENGLISH',
'DPS RK Puram', 'New Delhi', 'Delhi',
'AI_COACH', NULL, NULL,
7, 7,
now(), now()
),
-- Demo 2: Neha — AI Coach, NEET (id: 22222222...)
(
'22222222-2222-2222-2222-222222222222',
'neha.demo@pragativeer.com',
'9922222222',
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMoJ8zYTvNiG0HGkY0P6YOHRV6',
'Neha',
'TWELFTH', 'NEET', 'ENGLISH',
'St. Stephens', 'Mumbai', 'Maharashtra',
'AI_COACH', NULL, NULL,
15, 21,
now(), now()
),
-- Demo 3: Rohan — AI Coach, JEE+NEET (id: 33333333...)
(
'33333333-3333-3333-3333-333333333333',
'rohan.demo@pragativeer.com',
'9933333333',
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMoJ8zYTvNiG0HGkY0P6YOHRV6',
'Rohan',
'DROPPER', 'JEE_NEET', 'ENGLISH',
'Allen Kota', 'Kota', 'Rajasthan',
'AI_COACH', NULL, NULL,
1, 1,
now(), now()
),
-- Demo 4: Amit — Free Plan, JEE (id: 44444444...)
-- Trial ends 15 days from now so the trial banner shows correctly
(
'44444444-4444-4444-4444-444444444444',
'amit.demo@pragativeer.com',
'9944444444',
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMoJ8zYTvNiG0HGkY0P6YOHRV6',
'Amit',
'ELEVENTH', 'JEE_MAIN', 'ENGLISH',
'Resonance', 'Patna', 'Bihar',
'FREE', now() + INTERVAL '15 days', NULL,
1, 1,
now(), now()
)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email,
tier = EXCLUDED.tier,
target_exam = EXCLUDED.target_exam,
student_class = EXCLUDED.student_class,
medium = EXCLUDED.medium,
school = EXCLUDED.school,
city = EXCLUDED.city,
state = EXCLUDED.state,
trial_end = EXCLUDED.trial_end,
streak_count = EXCLUDED.streak_count,
longest_streak = EXCLUDED.longest_streak,
last_active = now();