-- ============================================================================= -- 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();