-- Initiative Recognition System — PostgreSQL schema (architecture_plan.md §4) -- Table order respects FKs (units before users). CREATE EXTENSION IF NOT EXISTS citext; -- ========= ENUMS ========= DO $$ BEGIN CREATE TYPE user_role AS ENUM ('applicant','council_member','editor','admin','viewer'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE initiative_class AS ENUM ('technical','research','textbook'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE research_evidence AS ENUM ('international','domestic','poster'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE eval_level AS ENUM ('high','medium','low'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE submission_status AS ENUM ('draft','submitted','under_review','approved','rejected'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE recognition_tier AS ENUM ('excellent','good'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ========= IDENTITY ========= CREATE TABLE IF NOT EXISTS units ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, parent_id UUID REFERENCES units(id), address TEXT ); CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email CITEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, full_name TEXT NOT NULL, phone TEXT, unit_id UUID REFERENCES units(id), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS user_roles ( user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role user_role NOT NULL, PRIMARY KEY (user_id, role) ); -- System user for anonymous draft saves (no login yet) INSERT INTO users (id, email, password_hash, full_name) VALUES ( '00000000-0000-4000-8000-000000000001', 'system@draft.local', '-', 'System (draft owner)' ) ON CONFLICT (email) DO NOTHING; -- ========= CASE / INITIATIVE ROOT ========= CREATE TABLE IF NOT EXISTS initiatives ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), case_code TEXT UNIQUE NOT NULL, owner_id UUID NOT NULL REFERENCES users(id), status submission_status NOT NULL DEFAULT 'draft', recognition_tier recognition_tier, submitted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_initiatives_owner_status ON initiatives(owner_id, status); -- ========= DRAFT SNAPSHOTS ========= CREATE TABLE IF NOT EXISTS drafts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), draft_code TEXT UNIQUE NOT NULL, initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, payload JSONB NOT NULL, version INTEGER NOT NULL DEFAULT 1, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_drafts_initiative ON drafts(initiative_id); -- ========= ĐƠN (APPLICATION) ========= CREATE TABLE IF NOT EXISTS applications ( initiative_id UUID PRIMARY KEY REFERENCES initiatives(id) ON DELETE CASCADE, initiative_name TEXT NOT NULL, investor_name TEXT, application_field TEXT, first_apply_date DATE, initiative_classification initiative_class, research_evidence_kind research_evidence, international_journal_decl TEXT, content_summary TEXT, confidential_info TEXT, conditions TEXT, author_evaluation TEXT, trial_evaluation TEXT, submission_day SMALLINT, submission_month SMALLINT, submission_year SMALLINT, honesty_confirmed BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT chk_first_apply_window CHECK (first_apply_date IS NULL OR first_apply_date BETWEEN DATE '2025-04-15' AND DATE '2026-04-15') ); CREATE TABLE IF NOT EXISTS authors ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id), ordinal SMALLINT NOT NULL, full_name TEXT NOT NULL, dob DATE, workplace TEXT, title TEXT, qualification TEXT, contribution_percent NUMERIC(5,2) NOT NULL, is_representative BOOLEAN NOT NULL DEFAULT FALSE, CHECK (contribution_percent >= 0 AND contribution_percent <= 100) ); CREATE UNIQUE INDEX IF NOT EXISTS uq_authors_repr ON authors(initiative_id) WHERE is_representative; CREATE TABLE IF NOT EXISTS support_staff ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, full_name TEXT, dob DATE, workplace TEXT, title TEXT, qualification TEXT, support_content TEXT ); CREATE TABLE IF NOT EXISTS evidence_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK (kind IN ('textbook','research','technical')), storage_uri TEXT NOT NULL, original_name TEXT NOT NULL, mime_type TEXT NOT NULL DEFAULT 'application/pdf', byte_size BIGINT NOT NULL, sha256 CHAR(64) NOT NULL, uploaded_by UUID NOT NULL REFERENCES users(id), uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS uq_evidence_kind ON evidence_files(initiative_id, kind); -- ========= BÁO CÁO (REPORT) ========= CREATE TABLE IF NOT EXISTS reports ( initiative_id UUID PRIMARY KEY REFERENCES initiatives(id) ON DELETE CASCADE, introduction TEXT, representative_phone TEXT, representative_email TEXT, current_status TEXT, purpose TEXT, implementation_steps TEXT, first_applied_unit TEXT, achieved_result TEXT, novelty TEXT, effectiveness JSONB NOT NULL DEFAULT '{}'::jsonb, submission_date DATE ); CREATE TABLE IF NOT EXISTS trial_units ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, name TEXT NOT NULL, address TEXT, field TEXT, ordinal SMALLINT ); -- ========= CONTRIBUTION CONFIRMATION ========= CREATE TABLE IF NOT EXISTS contributions ( initiative_id UUID PRIMARY KEY REFERENCES initiatives(id) ON DELETE CASCADE, main_author TEXT NOT NULL, position TEXT, representative_percent NUMERIC(5,2), submission_date TIMESTAMPTZ, digital_signature_confirmed BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS contribution_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, full_name TEXT, work_unit TEXT, contribution_percent NUMERIC(5,2) ); -- ========= PHIẾU ĐÁNH GIÁ ========= CREATE TABLE IF NOT EXISTS evaluations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, council_member_id UUID NOT NULL REFERENCES users(id), position TEXT, evaluation_date DATE NOT NULL, novelty_level eval_level, novelty_score SMALLINT, novelty_comment TEXT, effectiveness_level eval_level, effectiveness_score SMALLINT, effectiveness_comment TEXT, total_score SMALLINT GENERATED ALWAYS AS (COALESCE(novelty_score,0) + COALESCE(effectiveness_score,0)) STORED, conclusion TEXT, status submission_status NOT NULL DEFAULT 'draft', submitted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (novelty_score IS NULL OR (novelty_score BETWEEN 0 AND 40)), CHECK (effectiveness_score IS NULL OR (effectiveness_score BETWEEN 0 AND 60)), UNIQUE (initiative_id, council_member_id) ); CREATE INDEX IF NOT EXISTS idx_eval_initiative ON evaluations(initiative_id); -- ========= ADMIN VERIFY ========= CREATE TABLE IF NOT EXISTS verifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE, field_name TEXT NOT NULL, content_hash CHAR(64) NOT NULL, verified_by UUID NOT NULL REFERENCES users(id), verified_at TIMESTAMPTZ NOT NULL DEFAULT now(), result TEXT ); -- ========= AUDIT TRAIL ========= CREATE TABLE IF NOT EXISTS audit_log ( id BIGSERIAL PRIMARY KEY, actor_id UUID REFERENCES users(id), action TEXT NOT NULL, entity TEXT NOT NULL, entity_id UUID NOT NULL, diff JSONB, occurred_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity, entity_id);