Files
LWLT-AIBOT/control-plane/migrations/020_leader_task_summary_notifications.sql
2026-09-07 13:01:01 +08:00

132 lines
6.0 KiB
SQL

-- Default-off, organization-wide task-summary subscriptions for team leads.
-- This is a read-only projection: it never grants task mutation or ERP
-- execution authority and it does not reuse employee AgentBus reply rows.
CREATE UNIQUE INDEX IF NOT EXISTS user_channels_organization_id_id_notification_idx
ON user_channels (organization_id, id);
CREATE UNIQUE INDEX IF NOT EXISTS tasks_organization_id_id_notification_idx
ON tasks (organization_id, id);
CREATE INDEX IF NOT EXISTS outbox_events_leader_summary_projection_idx
ON outbox_events (organization_id, aggregate_id, created_at DESC)
WHERE topic = 'task.updated' AND aggregate_type = 'task';
CREATE INDEX IF NOT EXISTS tasks_leader_summary_projection_idx
ON tasks (organization_id, source, status, assigned_user_id, id)
WHERE assigned_user_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS leader_task_summary_subscriptions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
leader_user_id uuid NOT NULL,
channel_id uuid NOT NULL,
scope text NOT NULL DEFAULT 'organization',
include_manual boolean NOT NULL DEFAULT true,
include_agentbus boolean NOT NULL DEFAULT true,
enabled boolean NOT NULL DEFAULT false,
starts_at timestamptz NOT NULL DEFAULT now(),
revision integer NOT NULL DEFAULT 0,
recipient_address_ciphertext text NOT NULL,
recipient_address_fingerprint text NOT NULL,
conversation_id_ciphertext text NOT NULL,
conversation_id_fingerprint text NOT NULL,
target_verified_at timestamptz,
target_verified_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT leader_task_summary_subscriptions_scope_check
CHECK (scope = 'organization'),
CONSTRAINT leader_task_summary_subscriptions_sources_check
CHECK (include_manual OR include_agentbus),
CONSTRAINT leader_task_summary_subscriptions_revision_check
CHECK (revision >= 0),
CONSTRAINT leader_task_summary_subscriptions_recipient_fingerprint_check
CHECK (recipient_address_fingerprint ~ '^[a-f0-9]{64}$'),
CONSTRAINT leader_task_summary_subscriptions_conversation_fingerprint_check
CHECK (conversation_id_fingerprint ~ '^[a-f0-9]{64}$'),
CONSTRAINT leader_task_summary_subscriptions_leader_scope_fkey
FOREIGN KEY (organization_id, leader_user_id)
REFERENCES users (organization_id, id)
ON DELETE CASCADE,
CONSTRAINT leader_task_summary_subscriptions_channel_scope_fkey
FOREIGN KEY (organization_id, channel_id)
REFERENCES user_channels (organization_id, id)
ON DELETE CASCADE,
UNIQUE (organization_id, leader_user_id)
);
CREATE INDEX IF NOT EXISTS leader_task_summary_subscriptions_active_idx
ON leader_task_summary_subscriptions
(organization_id, enabled, starts_at, channel_id)
WHERE enabled = true;
CREATE UNIQUE INDEX IF NOT EXISTS leader_task_summary_subscriptions_organization_id_id_idx
ON leader_task_summary_subscriptions (organization_id, id);
CREATE TABLE IF NOT EXISTS leader_task_summary_deliveries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
subscription_id uuid NOT NULL,
subscription_revision integer NOT NULL,
task_id uuid NOT NULL,
leader_user_id uuid NOT NULL,
channel_id uuid NOT NULL,
source_outbox_event_id bigint NOT NULL,
milestone text NOT NULL,
recipient_address_ciphertext text NOT NULL,
recipient_address_fingerprint text NOT NULL,
conversation_id_ciphertext text NOT NULL,
conversation_id_fingerprint text NOT NULL,
payload_ciphertext text NOT NULL,
payload_fingerprint text NOT NULL,
delivery_status text NOT NULL DEFAULT 'pending',
attempt_count integer NOT NULL DEFAULT 0,
next_attempt_at timestamptz NOT NULL DEFAULT now(),
last_error text,
delivered_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT leader_task_summary_deliveries_milestone_check
CHECK (milestone IN ('final', 'needs_review', 'resolved')),
CONSTRAINT leader_task_summary_deliveries_status_check
CHECK (delivery_status IN ('pending', 'sending', 'delivered', 'failed', 'cancelled')),
CONSTRAINT leader_task_summary_deliveries_attempt_check
CHECK (attempt_count >= 0),
CONSTRAINT leader_task_summary_deliveries_revision_check
CHECK (subscription_revision >= 0),
CONSTRAINT leader_task_summary_deliveries_payload_fingerprint_check
CHECK (payload_fingerprint ~ '^[a-f0-9]{64}$'),
CONSTRAINT leader_task_summary_deliveries_recipient_fingerprint_check
CHECK (recipient_address_fingerprint ~ '^[a-f0-9]{64}$'),
CONSTRAINT leader_task_summary_deliveries_conversation_fingerprint_check
CHECK (conversation_id_fingerprint ~ '^[a-f0-9]{64}$'),
CONSTRAINT leader_task_summary_deliveries_subscription_scope_fkey
FOREIGN KEY (organization_id, subscription_id)
REFERENCES leader_task_summary_subscriptions (organization_id, id)
ON DELETE CASCADE,
CONSTRAINT leader_task_summary_deliveries_task_scope_fkey
FOREIGN KEY (organization_id, task_id)
REFERENCES tasks (organization_id, id)
ON DELETE CASCADE,
CONSTRAINT leader_task_summary_deliveries_leader_scope_fkey
FOREIGN KEY (organization_id, leader_user_id)
REFERENCES users (organization_id, id)
ON DELETE CASCADE,
CONSTRAINT leader_task_summary_deliveries_channel_scope_fkey
FOREIGN KEY (organization_id, channel_id)
REFERENCES user_channels (organization_id, id)
ON DELETE CASCADE,
UNIQUE (subscription_id, subscription_revision, task_id, milestone)
);
CREATE INDEX IF NOT EXISTS leader_task_summary_deliveries_pending_idx
ON leader_task_summary_deliveries
(channel_id, delivery_status, next_attempt_at, created_at, id)
WHERE delivery_status IN ('pending', 'sending', 'failed');
CREATE INDEX IF NOT EXISTS leader_task_summary_deliveries_task_idx
ON leader_task_summary_deliveries
(organization_id, task_id, leader_user_id, created_at DESC);