CREATE TABLE IF NOT EXISTS scheduled_notifications ( id BIGSERIAL PRIMARY KEY, channel TEXT NOT NULL CHECK (channel IN ('email', 'sms', 'push')), title TEXT, message TEXT NOT NULL, html TEXT, scheduled_at TIMESTAMPTZ NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'sent', 'failed', 'cancelled')), target_user_ids BIGINT[], target_role TEXT, target_raw JSONB, attempt_count INT NOT NULL DEFAULT 0, last_error TEXT, processing_started_at TIMESTAMPTZ, sent_at TIMESTAMPTZ, cancelled_at TIMESTAMPTZ, created_by BIGINT NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT scheduled_notifications_target_required CHECK ( (target_user_ids IS NOT NULL AND array_length(target_user_ids, 1) > 0) OR target_role IS NOT NULL OR target_raw IS NOT NULL ) ); CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_due ON scheduled_notifications (scheduled_at) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_status ON scheduled_notifications (status, scheduled_at);