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).
65 lines
2 KiB
TypeScript
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);
|
|
}
|
|
}
|