Motorul NU trimite o notificare per eveniment. Decide daca merita notificat, cui, ce severitate, daca se agrega, ce canal si cand. - notifications / notification_preferences / notification_processed_events. Ultimul da idempotency: aceeasi regula nu proceseaza acelasi eveniment de doua ori (unique rule_id + event_id). - Reguli in COD, nu in DB cu condition_expression evaluat dinamic. Un evaluator de expresii e o suprafata de atac si un limbaj in plus, fara ca nimeni sa administreze inca reguli per tenant. Contractul ramane acelasi cand vor migra in DB. - Destinatarii se rezolva server-side din memberships, niciodata din payload. Implicit nu notificam actorul despre propria actiune. - Deduplicare pe cheia tenant+recipient+categorie+entitate+versiune regula. - Agregare: 47 de taskuri intr-o ora devin o notificare cu aggregated_count, nu 47 de notificari. Testul verifica invariantul ca fereastra de agregare <= fereastra de dedup, altfel s-ar crea una noua inainte sa se agrege. - Quiet hours cu fereastra care poate traversa miezul noptii. CRITICAL le depaseste DOAR daca politica userului permite, nu implicit. - Inbox: filtre (unread/action_required/critical/intelligence/system), mark-read, acknowledge (opreste escaladarea), dismiss, snooze. - Preferinte per user+tenant cu UPSERT (NULLS NOT DISTINCT pe category, ca randul implicit "toate categoriile" sa fie unic).
73 lines
3 KiB
SQL
73 lines
3 KiB
SQL
-- Notifications Engine (Sprint 2-3). NU o notificare per eveniment: motorul
|
|
-- decide daca merita, cui, ce severitate, daca se agrega si cand se livreaza.
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE "notification_severity" AS ENUM
|
|
('CRITICAL','ACTION_REQUIRED','WARNING','INFORMATION','INTELLIGENCE','SYSTEM');
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE "notification_status" AS ENUM
|
|
('PENDING','SCHEDULED','DELIVERING','DELIVERED','READ','ACKNOWLEDGED',
|
|
'DISMISSED','FAILED','EXPIRED','ESCALATED');
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
|
CREATE TABLE IF NOT EXISTS "notifications" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"tenant_id" uuid NOT NULL,
|
|
"workspace_id" uuid,
|
|
"recipient_user_id" uuid NOT NULL,
|
|
"category" text NOT NULL,
|
|
"severity" "notification_severity" NOT NULL,
|
|
"title" text NOT NULL,
|
|
"body" text,
|
|
"source_event_id" uuid,
|
|
"source_entity_type" text,
|
|
"source_entity_id" text,
|
|
"action_url" text,
|
|
"action_type" text,
|
|
"status" "notification_status" DEFAULT 'PENDING' NOT NULL,
|
|
"deduplication_key" text NOT NULL,
|
|
"aggregation_key" text,
|
|
"aggregated_count" integer DEFAULT 1 NOT NULL,
|
|
"channels" jsonb DEFAULT '["in_app"]'::jsonb NOT NULL,
|
|
"scheduled_at" timestamp,
|
|
"expires_at" timestamp,
|
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
"read_at" timestamp,
|
|
"acknowledged_at" timestamp,
|
|
"snoozed_until" timestamp,
|
|
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS "notifications_recipient_idx" ON "notifications" ("recipient_user_id","status");
|
|
CREATE INDEX IF NOT EXISTS "notifications_tenant_idx" ON "notifications" ("tenant_id","created_at");
|
|
CREATE INDEX IF NOT EXISTS "notifications_dedup_idx" ON "notifications" ("deduplication_key","created_at");
|
|
|
|
CREATE TABLE IF NOT EXISTS "notification_preferences" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"tenant_id" uuid NOT NULL,
|
|
"user_id" uuid NOT NULL,
|
|
"category" text,
|
|
"enabled" integer DEFAULT 1 NOT NULL,
|
|
"minimum_severity" "notification_severity" DEFAULT 'INFORMATION' NOT NULL,
|
|
"quiet_hours_start" integer,
|
|
"quiet_hours_end" integer,
|
|
"critical_bypasses_quiet_hours" integer DEFAULT 1 NOT NULL,
|
|
"timezone" text DEFAULT 'Europe/Bucharest' NOT NULL,
|
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
);
|
|
-- NULLS NOT DISTINCT: category NULL inseamna "toate categoriile" si trebuie sa
|
|
-- fie unic per (user, tenant), altfel ON CONFLICT n-ar prinde randul implicit.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "notification_prefs_uq"
|
|
ON "notification_preferences" ("user_id","tenant_id","category") NULLS NOT DISTINCT;
|
|
|
|
CREATE TABLE IF NOT EXISTS "notification_processed_events" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"rule_id" text NOT NULL,
|
|
"event_id" uuid NOT NULL,
|
|
"processed_at" timestamp DEFAULT now() NOT NULL
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "notification_processed_uq"
|
|
ON "notification_processed_events" ("rule_id","event_id");
|