ALTER TABLE users ADD COLUMN deleted_by uuid REFERENCES users(id) ON DELETE SET NULL, ADD COLUMN deletion_reason text NOT NULL DEFAULT '', ADD COLUMN restored_at timestamptz; ALTER TABLE card_batches ADD COLUMN membership_plan_id uuid REFERENCES membership_plans(id) ON DELETE SET NULL, ADD COLUMN edition text NOT NULL DEFAULT '' CHECK (char_length(edition) <= 64), ADD COLUMN max_uses integer NOT NULL DEFAULT 1 CHECK (max_uses > 0); ALTER TABLE card_batches DROP CONSTRAINT card_batches_card_type_check; ALTER TABLE card_batches ADD CONSTRAINT card_batches_card_type_check CHECK (card_type IN ('registration','renewal','membership')); ALTER TABLE cards ADD COLUMN label text NOT NULL DEFAULT '' CHECK (char_length(label) <= 128); CREATE TABLE notification_channel_configs ( channel text PRIMARY KEY CHECK (channel IN ('smtp','telegram')), enabled boolean NOT NULL DEFAULT false, settings jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(settings)='object'), secret_cipher bytea, updated_by uuid REFERENCES users(id) ON DELETE SET NULL, updated_at timestamptz NOT NULL DEFAULT now() ); INSERT INTO notification_channel_configs(channel) VALUES ('smtp'),('telegram'); CREATE TABLE notification_templates ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), template_key text NOT NULL CHECK (template_key ~ '^[a-z0-9][a-z0-9._-]{0,127}$'), channel text NOT NULL CHECK (channel IN ('email','telegram')), subject text NOT NULL DEFAULT '' CHECK (char_length(subject) <= 300), body text NOT NULL CHECK (char_length(body) BETWEEN 1 AND 20000), enabled boolean NOT NULL DEFAULT true, updated_by uuid REFERENCES users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(template_key,channel) ); CREATE TABLE notification_delivery_attempts ( id bigserial PRIMARY KEY, delivery_id uuid NOT NULL REFERENCES notification_deliveries(id) ON DELETE CASCADE, attempt integer NOT NULL CHECK (attempt > 0), status text NOT NULL CHECK (status IN ('sent','failed','skipped')), error_code text NOT NULL DEFAULT '', error_summary text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), UNIQUE(delivery_id,attempt) ); CREATE TABLE telegram_runtime_state ( singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton), update_offset bigint NOT NULL DEFAULT 0, updated_at timestamptz NOT NULL DEFAULT now() ); INSERT INTO telegram_runtime_state(singleton) VALUES(true); CREATE TABLE user_ip_history ( id bigserial PRIMARY KEY, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, ip_hash text NOT NULL CHECK (char_length(ip_hash)=64), country text NOT NULL DEFAULT '', region text NOT NULL DEFAULT '', carrier text NOT NULL DEFAULT '', asn bigint, first_seen_at timestamptz NOT NULL DEFAULT now(), last_seen_at timestamptz NOT NULL DEFAULT now(), seen_count bigint NOT NULL DEFAULT 1 CHECK (seen_count > 0), UNIQUE(user_id,ip_hash) ); CREATE INDEX user_ip_history_user_time_idx ON user_ip_history(user_id,last_seen_at DESC); CREATE TABLE security_list_entries ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), subject_type text NOT NULL CHECK (subject_type IN ('ip','cidr','device','user')), mode text NOT NULL CHECK (mode IN ('allow','deny')), subject_value text NOT NULL CHECK (char_length(subject_value) BETWEEN 1 AND 512), reason text NOT NULL CHECK (char_length(btrim(reason)) BETWEEN 1 AND 1000), expires_at timestamptz, created_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), revoked_at timestamptz, revoked_by uuid REFERENCES users(id) ON DELETE SET NULL, UNIQUE(subject_type,mode,subject_value) ); CREATE INDEX security_list_active_idx ON security_list_entries(subject_type,mode,expires_at) WHERE revoked_at IS NULL; CREATE TABLE user_appeals ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, risk_event_id uuid REFERENCES risk_events(id) ON DELETE SET NULL, body text NOT NULL CHECK (char_length(btrim(body)) BETWEEN 1 AND 5000), status text NOT NULL DEFAULT 'submitted' CHECK (status IN ('submitted','reviewing','accepted','rejected')), 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 TABLE media_request_callbacks ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL CHECK (char_length(btrim(name)) BETWEEN 1 AND 128), callback_url text NOT NULL CHECK (callback_url ~ '^https://'), secret_cipher bytea NOT NULL, 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 media_request_callback_events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), callback_id uuid NOT NULL REFERENCES media_request_callbacks(id) ON DELETE CASCADE, external_event_id text NOT NULL CHECK (char_length(external_event_id) BETWEEN 1 AND 256), request_id uuid REFERENCES media_requests(id) ON DELETE SET NULL, payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'), signature_valid boolean NOT NULL, applied boolean NOT NULL DEFAULT false, error_summary text NOT NULL DEFAULT '', received_at timestamptz NOT NULL DEFAULT now(), UNIQUE(callback_id,external_event_id) );