Files
sciagent/be0/migrations/010_user_staff_profiles.sql
T
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

115 lines
4.2 KiB
SQL

-- User staff profiles (1:1 with users) — HR / verification workflow
-- Apply: docker exec -i initiative-postgres psql -U initiative -d initiatives < be0/migrations/010_user_staff_profiles.sql
DO $$ BEGIN
CREATE TYPE profile_verification_status AS ENUM ('draft', 'pending', 'verified', 'rejected');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
CREATE TABLE IF NOT EXISTS academic_titles (
code TEXT PRIMARY KEY,
label_vi TEXT NOT NULL,
label_en TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT TRUE
);
INSERT INTO academic_titles (code, label_vi, label_en, sort_order) VALUES
('professor', 'Giáo sư', 'Professor', 10),
('associate_professor', 'Phó Giáo sư', 'Associate Professor', 20),
('doctor_sc', 'Tiến sĩ', 'Doctor of Science', 30),
('bsckii', 'BSCKII', 'Specialist level II', 35),
('bscki', 'BSCKI', 'Specialist level I', 36),
('master', 'Thạc sĩ', 'Master', 40),
('doctor_md', 'Bác sĩ', 'Physician', 45),
('pharmacist', 'Dược sĩ', 'Pharmacist', 46),
('bachelor', 'Cử nhân', 'Bachelor', 50),
('other', 'Khác (ghi rõ)', 'Other (specify)', 100)
ON CONFLICT (code) DO NOTHING;
CREATE TABLE IF NOT EXISTS user_staff_profiles (
user_id UUID PRIMARY KEY
REFERENCES users(id) ON DELETE CASCADE,
employee_id TEXT,
academic_title_code TEXT REFERENCES academic_titles(code),
academic_title_other TEXT,
unit_name_freetext TEXT,
job_title TEXT,
profile_verification_status profile_verification_status
NOT NULL DEFAULT 'draft',
verification_submitted_at TIMESTAMPTZ,
verified_at TIMESTAMPTZ,
verified_by_user_id UUID REFERENCES users(id),
rejection_reason TEXT,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT employee_id_shape
CHECK (employee_id IS NULL OR employee_id ~ '^[A-Z0-9-]{3,32}$'),
CONSTRAINT academic_title_other_invariant CHECK (
CASE
WHEN academic_title_code IS NULL THEN academic_title_other IS NULL
WHEN academic_title_code = 'other' THEN
academic_title_other IS NOT NULL AND length(trim(academic_title_other)) > 0
ELSE academic_title_other IS NULL
END
),
CONSTRAINT verified_requires_metadata CHECK (
profile_verification_status <> 'verified'
OR (verified_at IS NOT NULL AND verified_by_user_id IS NOT NULL)
),
CONSTRAINT rejected_requires_reason CHECK (
profile_verification_status <> 'rejected'
OR (rejection_reason IS NOT NULL AND length(trim(rejection_reason)) > 0)
),
CONSTRAINT non_terminal_clears_verification CHECK (
profile_verification_status NOT IN ('draft', 'pending')
OR (verified_at IS NULL AND verified_by_user_id IS NULL)
),
CONSTRAINT rejected_clears_verification_metadata CHECK (
profile_verification_status <> 'rejected'
OR (verified_at IS NULL AND verified_by_user_id IS NULL)
),
CONSTRAINT verified_clears_rejection CHECK (
profile_verification_status <> 'verified'
OR rejection_reason IS NULL
),
CONSTRAINT job_title_length CHECK (
job_title IS NULL OR length(job_title) <= 120
)
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_usp_employee_id_unique
ON user_staff_profiles (employee_id)
WHERE employee_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS ix_usp_pending_queue
ON user_staff_profiles (verification_submitted_at)
WHERE profile_verification_status = 'pending';
CREATE INDEX IF NOT EXISTS ix_usp_verifier_activity
ON user_staff_profiles (verified_by_user_id, verified_at DESC)
WHERE verified_by_user_id IS NOT NULL;
-- Backfill one row per existing user (draft, NULL fields)
INSERT INTO user_staff_profiles (user_id, profile_verification_status)
SELECT u.id, 'draft'::profile_verification_status
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM user_staff_profiles p WHERE p.user_id = u.id
);
COMMENT ON TABLE user_staff_profiles IS
'Institutional staff profile and verification state; scalars only — no MinIO.';