ALTER TABLE users DROP CONSTRAINT users_role_check; ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('user','owner','admin','operator','support','auditor')); ALTER TABLE user_permissions DROP CONSTRAINT user_permissions_permission_check; ALTER TABLE user_permissions ADD CONSTRAINT user_permissions_permission_check CHECK (permission IN ( 'users.read','users.write','memberships.manage','cards.manage','invites.manage', 'requests.manage','tickets.manage','notifications.manage','security.manage', 'operations.manage','e.manage','audit.read','logs.read','backups.manage', 'updates.manage','settings.manage','licenses.manage' )); CREATE TABLE membership_plans ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), code text NOT NULL UNIQUE CHECK (code ~ '^[a-z0-9][a-z0-9._-]{0,63}$'), name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128), duration_days integer NOT NULL CHECK (duration_days > 0), max_playbacks integer NOT NULL DEFAULT 0 CHECK (max_playbacks >= 0), max_devices integer NOT NULL DEFAULT 0 CHECK (max_devices >= 0), max_bitrate_kbps integer NOT NULL DEFAULT 0 CHECK (max_bitrate_kbps >= 0), enabled boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE memberships DROP CONSTRAINT memberships_status_check; ALTER TABLE memberships ADD CONSTRAINT memberships_status_check CHECK (status IN ('active','paused','expired','revoked')); ALTER TABLE memberships ADD COLUMN plan_id uuid REFERENCES membership_plans(id) ON DELETE SET NULL, ADD COLUMN paused_at timestamptz, ADD COLUMN pause_reason text NOT NULL DEFAULT '', ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE media_requests DROP CONSTRAINT media_requests_status_check; UPDATE media_requests SET status='submitted' WHERE status='open'; UPDATE media_requests SET status='accepted' WHERE status='planned'; ALTER TABLE media_requests ADD CONSTRAINT media_requests_status_check CHECK (status IN ( 'submitted','reviewing','accepted','existing','downloading','fulfilled','failed','rejected','cancelled' )); ALTER TABLE media_requests ADD COLUMN media_type text NOT NULL DEFAULT 'other' CHECK (media_type IN ('movie','series','season','episode','other')), ADD COLUMN release_year integer CHECK (release_year BETWEEN 1800 AND 3000), ADD COLUMN priority text NOT NULL DEFAULT 'normal' CHECK (priority IN ('low','normal','high','urgent')), ADD COLUMN assignee_user_id uuid REFERENCES users(id) ON DELETE SET NULL, ADD COLUMN public_reply text NOT NULL DEFAULT '', ADD COLUMN internal_note text NOT NULL DEFAULT '', ADD COLUMN merged_into uuid REFERENCES media_requests(id) ON DELETE SET NULL, ADD COLUMN closed_at timestamptz; CREATE TABLE media_request_messages ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), request_id uuid NOT NULL REFERENCES media_requests(id) ON DELETE CASCADE, author_user_id uuid REFERENCES users(id) ON DELETE SET NULL, visibility text NOT NULL CHECK (visibility IN ('user','internal')), body text NOT NULL CHECK (length(btrim(body)) BETWEEN 1 AND 5000), created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX media_request_messages_request_idx ON media_request_messages(request_id,created_at); ALTER TABLE tickets DROP CONSTRAINT tickets_status_check; UPDATE tickets SET status='in_progress' WHERE status='pending'; ALTER TABLE tickets ADD CONSTRAINT tickets_status_check CHECK (status IN ('open','in_progress','waiting_user','resolved','closed')); ALTER TABLE tickets DROP CONSTRAINT tickets_priority_check; ALTER TABLE tickets ADD CONSTRAINT tickets_priority_check CHECK (priority IN ('low','normal','high','urgent')); ALTER TABLE tickets ADD COLUMN sla_due_at timestamptz, ADD COLUMN last_user_reply_at timestamptz, ADD COLUMN last_staff_reply_at timestamptz, ADD COLUMN reopened_at timestamptz; ALTER TABLE ticket_replies ADD COLUMN visibility text NOT NULL DEFAULT 'user' CHECK (visibility IN ('user','internal')); CREATE TABLE announcements ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), title text NOT NULL CHECK (length(btrim(title)) BETWEEN 1 AND 200), body text NOT NULL CHECK (length(btrim(body)) BETWEEN 1 AND 20000), status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','scheduled','published','withdrawn')), audience text NOT NULL DEFAULT 'all' CHECK (audience IN ('all','active_members','staff')), pinned boolean NOT NULL DEFAULT false, publish_at timestamptz, expires_at timestamptz, created_by uuid NOT NULL REFERENCES users(id), updated_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CHECK (expires_at IS NULL OR publish_at IS NULL OR expires_at > publish_at) ); CREATE INDEX announcements_status_publish_idx ON announcements(status,publish_at DESC); CREATE TABLE announcement_reads ( announcement_id uuid NOT NULL REFERENCES announcements(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, read_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (announcement_id,user_id) ); CREATE TABLE notification_events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), event_type text NOT NULL, target_user_id uuid REFERENCES users(id) ON DELETE CASCADE, title text NOT NULL CHECK (length(btrim(title)) BETWEEN 1 AND 200), body text NOT NULL CHECK (length(body) <= 10000), link_path text NOT NULL DEFAULT '' CHECK (link_path='' OR link_path ~ '^/'), related_type text NOT NULL DEFAULT '', related_id text NOT NULL DEFAULT '', created_by uuid REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX notification_events_target_idx ON notification_events(target_user_id,created_at DESC); CREATE TABLE notification_deliveries ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), event_id uuid NOT NULL REFERENCES notification_events(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, channel text NOT NULL CHECK (channel IN ('in_app','email','telegram')), status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sending','sent','failed','cancelled')), attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0), next_attempt_at timestamptz, last_error text NOT NULL DEFAULT '', read_at timestamptz, deleted_at timestamptz, sent_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(event_id,user_id,channel) ); CREATE INDEX notification_deliveries_user_idx ON notification_deliveries(user_id,channel,created_at DESC) WHERE deleted_at IS NULL; CREATE INDEX notification_deliveries_pending_idx ON notification_deliveries(next_attempt_at,created_at) WHERE status IN ('pending','failed'); CREATE TABLE risk_rules ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 128), event_type text NOT NULL CHECK (event_type IN ('concurrency','multi_region','device_churn','auth_failures','manual')), window_seconds integer NOT NULL CHECK (window_seconds BETWEEN 60 AND 2592000), threshold integer NOT NULL CHECK (threshold > 0), action text NOT NULL DEFAULT 'review' CHECK (action IN ('observe','review','revoke_sessions','disable_user')), enabled boolean NOT NULL DEFAULT true, created_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE risk_events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), rule_id uuid REFERENCES risk_rules(id) ON DELETE SET NULL, user_id uuid REFERENCES users(id) ON DELETE SET NULL, event_type text NOT NULL, severity text NOT NULL CHECK (severity IN ('info','low','medium','high','critical')), status text NOT NULL DEFAULT 'open' CHECK (status IN ('open','reviewing','resolved','dismissed','appealed')), evidence jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(evidence)='object'), resolution text NOT NULL DEFAULT '', resolved_by uuid REFERENCES users(id) ON DELETE SET NULL, resolved_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX risk_events_status_idx ON risk_events(status,severity,created_at DESC); ALTER TABLE refresh_token_families ADD COLUMN device_name text NOT NULL DEFAULT '', ADD COLUMN client_name text NOT NULL DEFAULT '', ADD COLUMN platform text NOT NULL DEFAULT '', ADD COLUMN ip_hash text NOT NULL DEFAULT '', ADD COLUMN last_seen_at timestamptz NOT NULL DEFAULT now(); ALTER TABLE devices ADD COLUMN device_name text NOT NULL DEFAULT '', ADD COLUMN client_name text NOT NULL DEFAULT '', ADD COLUMN platform text NOT NULL DEFAULT '', ADD COLUMN client_version text NOT NULL DEFAULT '', ADD COLUMN last_ip_hash text NOT NULL DEFAULT '', ADD COLUMN trusted boolean NOT NULL DEFAULT false, ADD COLUMN blocked_at timestamptz, ADD COLUMN blocked_by uuid REFERENCES users(id) ON DELETE SET NULL; CREATE TABLE configuration_versions ( revision bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, domain text NOT NULL CHECK (domain IN ('s','e')), values jsonb NOT NULL CHECK (jsonb_typeof(values)='object'), state text NOT NULL DEFAULT 'draft' CHECK (state IN ('draft','published','superseded','failed')), source_revision bigint REFERENCES configuration_versions(revision), created_by uuid NOT NULL REFERENCES users(id), published_at timestamptz, failure_reason text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX configuration_versions_published_domain_idx ON configuration_versions(domain) WHERE state='published'; CREATE TABLE operation_jobs ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), job_type text NOT NULL, resource_type text NOT NULL DEFAULT '', resource_id text NOT NULL DEFAULT '', idempotency_key text NOT NULL UNIQUE, state text NOT NULL DEFAULT 'queued' CHECK (state IN ('queued','running','pausing','paused','cancelling','cancelled','succeeded','partial_failed','failed')), progress_total bigint NOT NULL DEFAULT 0 CHECK (progress_total >= 0), progress_completed bigint NOT NULL DEFAULT 0 CHECK (progress_completed >= 0), progress_failed bigint NOT NULL DEFAULT 0 CHECK (progress_failed >= 0), error_code text NOT NULL DEFAULT '', error_summary text NOT NULL DEFAULT '', created_by uuid REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), started_at timestamptz, updated_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz, CHECK (progress_completed + progress_failed <= progress_total OR progress_total=0) ); CREATE INDEX operation_jobs_state_idx ON operation_jobs(state,created_at DESC); CREATE TABLE api_idempotency_keys ( actor_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, operation text NOT NULL, idempotency_key text NOT NULL, resource_id text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL DEFAULT now() + interval '24 hours', PRIMARY KEY (actor_user_id,operation,idempotency_key) ); CREATE INDEX api_idempotency_keys_expiry_idx ON api_idempotency_keys(expires_at);