Sari la conținut

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.

CanalMecanismCum ajunge la user
Card TelegramFirmEventsConsumer (group telegram-worker)mesaj/card în chat-ul legat de firmă
Web pushWebPushConsumer (group webpush-worker), VAPIDnotificare de browser via service worker
In-app bellpolling REST (/api/notifications)clopoțel + drawer în UI
Domain event → outbox_events → Kafka topic `firm.events`
├─ FirmEventsConsumer (group telegram-worker) → card Telegram
└─ WebPushConsumer (group webpush-worker) → web push browser

WebPushConsumer (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.

src/Asy.Domain/Notifications/Notification.cs. Convenția de Type e "{domain}.{action}".

CâmpNotă
UserIddestinatarul (tipat)
FirmIdnullable — null = notificare globală per-user
Typeex. "efactura.incoming", "reminder.payment_due"
Title, Bodytext afișat
Linkrută SPA relativă pentru click (ex. /facturi/{id})
Iconmapat în frontend; null → Bell
MetadataJsonJSONB cu date structurate (invoiceId, partnerName…)
ReadAt, CreatedAtcitit 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).

Subscripțiile de browser se țin în push_subscriptions (src/Asy.Domain/Notifications/PushSubscription.cs):

CâmpNotă
UserIdscope per-user, nu per-firmă
EndpointURL-ul serviciului de push — cheie naturală unică
P256dh, Authcheile ECDH/secret ale clientului (base64url)
UserAgentsnapshot browser (identificare device la revocare)
FailureCount, LastNotifiedAtlivrare; 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/*.

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 secunde
staleTime: 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.

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.