Notificări (in-app + web push)
ASYDATA livrează notificări pe trei canale: cardul Telegram, web push în browser (VAPID) și
clopoțelul in-app (bell + drawer) cu polling. Toate sunt alimentate event-driven din topicul Kafka
firm.events, prin consumeri independenți. Backend-ul stă în src/Asy.Application/Notifications/ și
src/Asy.Domain/Notifications/, consumerul de web push în
src/Workers/Asy.Workers.Telegram/WebPushConsumer.cs, iar frontend-ul în
asydata-app-frontend/src/features/notifications/.
Arhitectura de decuplare e aceeași ca la bot — vezi Flux event-driven și Telegram.
Canalele
Secțiune intitulată „Canalele”| Canal | Mecanism | Cum ajunge la user |
|---|---|---|
| Card Telegram | FirmEventsConsumer (group telegram-worker) | mesaj/card în chat-ul legat de firmă |
| Web push | WebPushConsumer (group webpush-worker), VAPID | notificare de browser via service worker |
| In-app bell | polling REST (/api/notifications) | clopoțel + drawer în UI |
Arhitectura event-driven
Secțiune intitulată „Arhitectura event-driven”Domain event → outbox_events → Kafka topic `firm.events` ├─ FirmEventsConsumer (group telegram-worker) → card Telegram └─ WebPushConsumer (group webpush-worker) → web push browserWebPushConsumer (src/Workers/Asy.Workers.Telegram/WebPushConsumer.cs, group webpush-worker) citește
event-type din header, extrage firmId din partition key, construiește payload-ul, interoghează membrii
activi ai firmei și cheamă IPushSubscriptionService.SendToUsersAsync(...). Pentru
InvoiceReceivedEvent trimite payload { title: "Factură nouă: {supplier}", body: "{ref} · {amount} {currency}", url: "/facturi/{id}", icon: "efactura", tag: "invoice-{id}" }. E un no-op dacă
Kafka:BootstrapServers lipsește sau VAPID nu e configurat.
Entitatea Notification
Secțiune intitulată „Entitatea Notification”src/Asy.Domain/Notifications/Notification.cs. Convenția de Type e "{domain}.{action}".
| Câmp | Notă |
|---|---|
UserId | destinatarul (tipat) |
FirmId | nullable — null = notificare globală per-user |
Type | ex. "efactura.incoming", "reminder.payment_due" |
Title, Body | text afișat |
Link | rută SPA relativă pentru click (ex. /facturi/{id}) |
Icon | mapat în frontend; null → Bell |
MetadataJson | JSONB cu date structurate (invoiceId, partnerName…) |
ReadAt, CreatedAt | citit la / creat la |
Notificarea se creează prin INotificationService.CreateAsync(CreateNotificationRequest). Worker-ul de
e-Factură o emite pentru fiecare factură nou importată (Type: "efactura.incoming") — vezi
e-Factură (SFS).
Web push (VAPID)
Secțiune intitulată „Web push (VAPID)”Subscripțiile de browser se țin în push_subscriptions (src/Asy.Domain/Notifications/PushSubscription.cs):
| Câmp | Notă |
|---|---|
UserId | scope per-user, nu per-firmă |
Endpoint | URL-ul serviciului de push — cheie naturală unică |
P256dh, Auth | cheile ECDH/secret ale clientului (base64url) |
UserAgent | snapshot browser (identificare device la revocare) |
FailureCount, LastNotifiedAt | livrare; subscripția se șterge la 404/410 |
Trimiterea folosește IWebPushSender (src/Asy.Infrastructure/Notifications/WebPushSender.cs, librăria
WebPush). Cheile VAPID vin din config: WebPush:Subject, WebPush:PublicKey, WebPush:PrivateKey. Dacă
nu sunt setate, IsConfigured = false și trimiterile sunt no-op silent. Pe frontend, cheia publică vine
din VITE_VAPID_PUBLIC_KEY; hook-ul usePushNotifications (src/features/notifications/usePushNotifications.ts)
gestionează subscribe / unsubscribe / sendTest via /api/push/*.
Clopoțelul in-app (polling)
Secțiune intitulată „Clopoțelul in-app (polling)”Frontend-ul nu folosește realtime; clopoțelul face polling prin React Query. În
asydata-app-frontend/src/features/notifications/hooks.ts:
refetchInterval: 60_000, // ~60 secundestaleTime: 30_000,useNotificationsCount() apelează /api/notifications/count la ~60 secunde și alimentează badge-ul de
pe NotificationsBell. Drawer-ul (NotificationsDrawer) listează prin /api/notifications și permite
mark-read / mark-all-read / delete. Endpoint-urile sunt în src/Asy.WebApi/Endpoints/NotificationsEndpoints.cs
și PushEndpoints.cs.
Toggle per chat
Secțiune intitulată „Toggle per chat”Pe canalul Telegram, ce evenimente ajung în fiecare chat e controlat de TelegramChat.EventsEnabledJson
(ex. { "invoiceNew": true, "syncError": false }); cheia lipsă = implicit activată pentru tipurile noi.
Vezi Telegram.
- Flux event-driven — outbox → Kafka
firm.events - Telegram — celălalt consumer al aceluiași topic
- e-Factură (SFS) — sursa notificărilor
efactura.incoming - Domenii — contextul Notifications
- Schema vizuală pe Figma