ceo-api/src/notifications/notifications.controller.ts
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

65 lines
2 KiB
TypeScript

import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common';
import { CurrentSession } from '../auth/session.decorator';
import type { AuthenticatedSession } from '../auth/tenant.guard';
import { MarkReadDto, SnoozeDto, UpdatePreferencesDto } from './dto';
import { NotificationsService, type InboxFilter } from './notifications.service';
const FILTERS: InboxFilter[] = [
'all',
'unread',
'action_required',
'critical',
'intelligence',
'system',
];
@Controller('notifications')
export class NotificationsController {
constructor(private readonly notificationsService: NotificationsService) {}
@Get()
list(@CurrentSession() session: AuthenticatedSession, @Query('filter') filter?: string) {
const safe = FILTERS.includes(filter as InboxFilter) ? (filter as InboxFilter) : 'all';
return this.notificationsService.list(session, safe);
}
@Post('mark-read')
markRead(@CurrentSession() session: AuthenticatedSession, @Body() dto: MarkReadDto) {
return this.notificationsService.markRead(session, dto.ids);
}
@Post(':id/acknowledge')
acknowledge(
@CurrentSession() session: AuthenticatedSession,
@Param('id', ParseUUIDPipe) id: string,
) {
return this.notificationsService.acknowledge(session, id);
}
@Post(':id/dismiss')
dismiss(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) {
return this.notificationsService.dismiss(session, id);
}
@Post(':id/snooze')
snooze(
@CurrentSession() session: AuthenticatedSession,
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: SnoozeDto,
) {
return this.notificationsService.snooze(session, id, dto.minutes);
}
@Get('preferences')
getPreferences(@CurrentSession() session: AuthenticatedSession) {
return this.notificationsService.getPreferences(session);
}
@Patch('preferences')
updatePreferences(
@CurrentSession() session: AuthenticatedSession,
@Body() dto: UpdatePreferencesDto,
) {
return this.notificationsService.updatePreferences(session, dto);
}
}