ceo-api/drizzle/0006_notifications_engine.sql
valentinbvro 80e9ede875 feat: Sprint 2-3 -- Notifications Engine
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).
2026-07-29 14:36:29 +02:00

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");