4. Communicatie en notificaties
Deze sectie legt de tabellen vast die nodig zijn voor systeemberichten en privéconversaties, inclusief threads, deelnemers, leespositie en systeemachtige thread-events binnen een gesprek.
4.1 SystemMessages
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| SystemMessages | Communicatie | Mailbox-items voor systeemberichten die gebruikers informeren en vanuit de GUI kunnen doorverwijzen naar een concreet domeinobject of vervolghandeling. | Users, RelationshipInvitations, UserRelationships, Tickets, PrivateMessageThreads, SharedExercises |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel van het systeembericht. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| RecipientUserId | uniqueidentifier | - | N | N | Users.Id | N | N | J | Soft link naar Users.Id; geen harde database-FK vanwege modulegrens. Gebruiker die het systeembericht ontvangt. |
| Type | nvarchar(100) | - | N | N | - | N | N | J | Type systeembericht, centraal gestandaardiseerd voor rendering en logica. |
| Title | nvarchar(200) | - | N | N | - | N | N | N | Korte titel van het systeembericht. |
| Body | nvarchar(max) | - | N | N | - | N | N | N | Berichtinhoud zoals zichtbaar in het systeemberichtenoverzicht of detailscherm. |
| CreatedBySystemComponent | nvarchar(100) | - | N | N | - | N | N | J | Broncomponent die het systeembericht heeft aangemaakt, bijvoorbeeld RelationshipService of TicketService. |
| EntityType | nvarchar(50) | - | N | N | - | N | J | J | Gesloten verwijstype voor systeemberichten. Toegestane waarden: RelationshipInvitation, Ticket, PrivateMessageThread, SharedExercise. |
| EntityId | uniqueidentifier | - | N | N | - | N | J | J | Id van het domeinobject waarnaar dit bericht verwijst. |
| CreatedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Aanmaakmoment van het systeembericht. |
| ReadAtUtc | datetime2 | null | N | N | - | N | J | J | Moment waarop de ontvanger het bericht gelezen heeft. |
Validaties / constraints
- CreatedBySystemComponent, Type en EntityType worden centraal gestandaardiseerd. EntityType is null of exact één van de vier toegestane waarden: RelationshipInvitation, Ticket, PrivateMessageThread of SharedExercise.
- Wanneer EntityType gevuld is, moet EntityId ook gevuld zijn en omgekeerd.
Business rules
- Systeemberichten zijn mailbox-items in dezelfde GUI als privéberichten, maar blijven technisch een apart domein.
- Klikbare interactie binnen systeemberichten wordt uitsluitend bepaald via EntityType + EntityId. De frontend vertaalt die combinatie naar de juiste detailweergave of actie voor RelationshipInvitation, Ticket, PrivateMessageThread of SharedExercise.
Lifecycle / gedrag
- Systeemberichten worden niet hard verwijderd.
- ReadAtUtc ondersteunt ongelezen/gelezen weergave.
- Het ontbreken van een losse ActionUrl is een bewuste keuze; navigatie blijft afgeleid van domeinverwijzing.
Designkeuzes
- Aparte notificatietabel houdt systeemberichten los van privéconversaties, terwijl gedeelde mailboxweergave in de GUI behouden blijft.
- CreatedBySystemComponent ondersteunt technische traceerbaarheid zonder UI-logica in de tabel vast te leggen.
- De combinatie EntityType + EntityId vormt binnen SystemMessages een gerichte functionele domeinverwijzing naar exact vier toegestane domeinobjecttypen: RelationshipInvitation, Ticket, PrivateMessageThread en SharedExercise. Hiervoor wordt bewust geen harde foreign key op databaseniveau gebruikt, omdat één record afhankelijk van EntityType naar één van deze domeinobjecten kan verwijzen. De geldigheid van deze verwijzing wordt daarom bewaakt in applicatielogica, enumvalidatie en tests, niet via één databaseconstraint.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: niet van toepassing binnen deze tabel.
Functionele / logische verwijzingen zonder harde FK
RecipientUserIdis een soft link naarUsers.Id; ontvangerbestaan en mailboxautorisatie worden via de identity-/authorizationlaag gecontroleerd.- De combinatie EntityType + EntityId vormt uitsluitend binnen SystemMessages een gerichte polymorfe domeinverwijzing en gebruikt bewust geen harde foreign key, omdat één record afhankelijk van EntityType alleen naar RelationshipInvitations, Tickets, PrivateMessageThreads of SharedExercises kan verwijzen. De velden Type en EntityType functioneren als gesloten code-/enumwaarden en verwijzen bewust niet via een harde foreign key.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
4.2 PrivateMessageThreads
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| PrivateMessageThreads | Communicatie | Bovenliggende conversatie-entiteit voor privéberichten. Bevat threadmetadata zoals onderwerp en laatste activiteit. | Users, PrivateMessageThreadParticipants, PrivateMessages, PrivateMessageThreadEvents, SystemMessages |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel van de thread. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| Subject | nvarchar(200) | - | N | N | - | N | N | J | Onderwerp van de conversatie. |
| CreatedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Moment waarop de thread is aangemaakt. |
| CreatedByUserId | uniqueidentifier | - | N | N | Users.Id | N | N | J | Soft link naar Users.Id; geen harde database-FK vanwege modulegrens. Gebruiker die de thread initieerde. |
| LastMessageAtUtc | datetime2 | - | N | N | - | N | N | J | Laatste moment waarop een normaal privébericht aan de thread is toegevoegd. |
| DisplayColorHex | char(7) | - | N | N | - | N | N | N | Stabiele threadkleur voor mailbox- en detailweergave, bijvoorbeeld #4A90E2. Wordt bij threadaanmaak gekozen uit een veilige palette. |
| IconKey | nvarchar(64) | - | N | N | - | N | N | N | Sleutel naar een beheerde, kleine iconenset voor de thread. Geen vrije HTML/CSS of upload. |
Validaties / constraints
- Subject is verplicht.
- CreatedByUserId moet ook als deelnemer aan de thread voorkomen.
- DisplayColorHex moet een geldige hexkleur uit de toegestane threadpalette zijn.
- IconKey moet voorkomen in de toegestane threadiconallowlist.
Business rules
- Een thread is de container voor een privégesprek en kan later uitbreidbaar zijn naar meerdere deelnemers.
- Verwijderen van een thread door één deelnemer verwijdert de thread niet voor andere deelnemers.
- Wanneer door verwijdering of verlaten van deelnemers geen zinvolle gesprekssituatie meer resteert, raakt de thread functioneel gesloten en kan daarop niet verder worden gereageerd.
Lifecycle / gedrag
- De thread zelf kent geen IsActive-vlag.
- Zichtbaarheid per gebruiker wordt geregeld via de participantlaag.
- LastMessageAtUtc wordt bijgewerkt bij nieuwe privéberichten, niet bij thread-events.
- DisplayColorHex en IconKey worden bij threadaanmaak gezet en blijven stabiel totdat een latere expliciete beheer-/gebruikersactie voor threadpresentatie wordt toegevoegd.
Designkeuzes
- Threadmetadata is bewust losgetrokken van individuele berichten, zodat onderwerp, laatste activiteit en toekomstige groepsgesprekken correct gemodelleerd kunnen worden.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: niet van toepassing binnen deze tabel.
Functionele / logische verwijzingen zonder harde FK
CreatedByUserIdis een soft link naarUsers.Id; threadinitiator wordt functioneel gevalideerd zonder harde database-FK naar identity.- Buiten de expliciete foreign keys bevat deze tabel geen afzonderlijke functionele verwijzingen die als harde foreign key gemodelleerd hoeven te worden; overige velden zijn inhouds-, status- of auditwaarden.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
4.3 PrivateMessageThreadParticipants
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| PrivateMessageThreadParticipants | Communicatie | Deelnemers per privébericht-thread, inclusief mailboxspecifieke zichtbaarheid en leespositie per gebruiker. | PrivateMessageThreads, Users, PrivateMessages |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel van de participantregel. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| ThreadId | uniqueidentifier | - | N | J | PrivateMessageThreads.Id | N | N | J | Verwijzing naar de bijbehorende thread. |
| UserId | uniqueidentifier | - | N | N | Users.Id | N | N | J | Soft link naar Users.Id; geen harde database-FK vanwege modulegrens. Deelnemer aan de thread. |
| JoinedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | N | Moment waarop de gebruiker deelnemer is geworden. |
| DeletedAtUtc | datetime2 | null | N | N | - | N | J | J | Moment waarop deze gebruiker de thread uit zijn of haar eigen overzicht heeft verwijderd of verlaten. |
| LastReadMessageId | uniqueidentifier | null | N | N | PrivateMessages.Id | N | J | J | Soft link naar PrivateMessages.Id; geen harde database-FK zodat berichtretentie en opschoning de participant-readstate niet blokkeren. Laatste normale privébericht dat door deze deelnemer als gelezen geldt. |
| LastReadAtUtc | datetime2 | null | N | N | - | N | J | J | Moment waarop de leespositie voor het laatst is bijgewerkt. |
| DisplayColorHex | char(7) | - | N | N | - | N | N | N | Threadspecifieke kleur voor deze participant binnen deze thread. Wordt gebruikt voor herkenbare berichtballonnen/deelnemeraccenten en is geen globale profielkleur. |
Validaties / constraints
- Actieve combinatie ThreadId + UserId is uniek.
- LastReadMessageId moet, wanneer gevuld, naar een bericht uit dezelfde thread verwijzen.
- DisplayColorHex moet een geldige kleur uit de toegestane participantpalette zijn.
Business rules
- Ongelezenlogica binnen een thread wordt per deelnemer bepaald. Zowel nieuwe privéberichten als thread-events na de laatst bekende leespositie kunnen de thread ongelezen maken.
- Eigen berichten en eigen thread-events tellen voor de actor zelf niet als nieuwe activiteit.
- Soft delete van een gesprek gebeurt per deelnemer via DeletedAtUtc. Een participantregel met
DeletedAtUtcwordt uitgesloten uit mailboxoverzicht, zoeken, filters, paginering en ongelezenafleiding voor die gebruiker, zonder berichten of events voor andere deelnemers te verwijderen. - Wanneer een deelnemer de thread verlaat, moet een thread-event worden vastgelegd zodat andere deelnemers dit kunnen zien.
- LastReadMessageId ondersteunt meerdere ongelezen berichten binnen één thread.
LastReadAtUtcondersteunt daarnaast ongelezenafleiding voor thread-events die geen normaal privébericht zijn.
Lifecycle / gedrag
- Een participant kan later opnieuw aan een thread worden toegevoegd, maar historische deelnameregels blijven behouden.
- DeletedAtUtc verwijdert geen berichten of thread-events.
Designkeuzes
- Mailboxstatus en leespositie zijn bewust niet op thread- of berichtniveau alleen gemodelleerd. Daardoor blijft de structuur correct bij tweepersoonsgesprekken én toekomstige groepsgesprekken.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: ThreadId -> PrivateMessageThreads.Id.
Functionele / logische verwijzingen zonder harde FK
UserIdis een soft link naarUsers.Id; participanttoegang wordt applicatief gevalideerd.LastReadMessageIdis een soft link naarPrivateMessages.Idvanwege berichtretentie en opschoning binnen hetzelfde communicatiedomein.- Buiten de expliciete foreign keys bevat deze tabel geen afzonderlijke functionele verwijzingen die als harde foreign key gemodelleerd hoeven te worden; overige velden zijn inhouds-, status- of auditwaarden.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
4.4 PrivateMessages
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| PrivateMessages | Communicatie | Individuele privéberichten binnen een thread. Ondersteunt reguliere berichten en berichten die namens een andere gebruiker worden verstuurd. | PrivateMessageThreads, PrivateMessageThreadParticipants, Users |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel van het bericht. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| ThreadId | uniqueidentifier | - | N | J | PrivateMessageThreads.Id | N | N | J | Thread waar het bericht onderdeel van is. |
| SenderUserId | uniqueidentifier | - | N | N | Users.Id | N | N | J | Soft link naar Users.Id; geen harde database-FK vanwege modulegrens. Werkelijke afzender van het bericht. |
| SentAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Verzendmoment van het bericht. |
| Body | nvarchar(max) | - | N | N | - | N | N | N | Berichtinhoud. Rich text moet vóór opslag gesanitized worden. Functioneel begrensd op maximaal 4.000 zichtbare tekens en maximaal 12 KiB genormaliseerde veilige HTML. |
| BodyFormat | nvarchar(50) | PlainText | N | N | - | N | N | N | Formaat van de body, bijvoorbeeld PlainText of SanitizedHtml. |
| SendAsUserId | uniqueidentifier | null | N | N | Users.Id | N | J | J | Soft link naar Users.Id; geen harde database-FK vanwege modulegrens. Wanneer gevuld is het bericht functioneel verzonden namens deze gebruiker; de UI toont dan een afwijkende afzenderweergave. |
Validaties / constraints
- Body is verplicht.
- Body bevat na sanitizing maximaal 4.000 zichtbare tekens.
- Wanneer
BodyFormat = SanitizedHtmlwordt gebruikt, is de genormaliseerde opgeslagen body maximaal 12 KiB. - Bij rich text is sanitizing verplicht.
- SendAsUserId wordt uitsluitend gebruikt in expliciet ondersteunde beheerprocessen, zoals het doorzetten van een melding naar een docent namens een gebruiker. Het is geen generieke verzendoptie voor gewone eindgebruikers.
Business rules
- Privéberichten staan los van meldingen, ondanks visuele nabijheid in dezelfde GUI.
- Thread-events zoals onderwerpwijziging of deelnemer verlaten worden niet als gewone PrivateMessages opgeslagen.
- Bijlagen zijn binnen de initiële scope niet opgenomen.
Lifecycle / gedrag
- Privéberichten worden niet hard verwijderd.
- Zichtbaarheid per gebruiker wordt afgeleid via de participantlaag in plaats van via losse delete-vlaggen op het bericht zelf.
- Privéberichten kennen binnen de initiële scope een retentietermijn van drie maanden. Opruiming wordt via geplande cleanup uitgevoerd en raakt alleen privéberichten; systeemberichten volgen bewust niet dezelfde bewaarlijn.
Designkeuzes
- De tabel blijft bewust beperkt tot echte privéberichten.
- Het veld SendAsUserId ondersteunt de speciale weergave “OefenHub Beheerder (namens <gebruiker>)” zonder gewone en systematische thread-events te vermengen.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: ThreadId -> PrivateMessageThreads.Id.
Functionele / logische verwijzingen zonder harde FK
SenderUserIdenSendAsUserIdzijn soft links naarUsers.Id; afzender- en namens-weergave blijven historisch herleidbaar zonder harde database-FK naar identity.- De velden BodyFormat functioneren als code-, enum- of sleutelwaarden en verwijzen bewust niet via een harde foreign key.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
4.5 PrivateMessageThreadEvents
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| PrivateMessageThreadEvents | Communicatie | Systeemachtige gebeurtenissen binnen een privébericht-thread, zoals onderwerpwijzigingen of deelnemers die de thread verlaten. | PrivateMessageThreads, Users |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel van het thread-event. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| ThreadId | uniqueidentifier | - | N | J | PrivateMessageThreads.Id | N | N | J | Thread waarin het event zichtbaar wordt. |
| EventType | nvarchar(100) | - | N | N | - | N | N | J | Type thread-event, bijvoorbeeld SubjectChanged, ParticipantLeft of ParticipantAdded. |
| ActorUserId | uniqueidentifier | null | N | N | Users.Id | N | J | J | Soft link naar Users.Id; geen harde database-FK vanwege modulegrens. Gebruiker die de actie initieerde; null wanneer het event puur systemisch is. |
| AffectedUserId | uniqueidentifier | null | N | N | Users.Id | N | J | J | Soft link naar Users.Id; geen harde database-FK vanwege modulegrens. Optionele betrokken gebruiker op wie het event betrekking heeft, bijvoorbeeld de toegevoegde of vertrokken deelnemer. |
| OccurredAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Moment waarop het event plaatsvond. |
| OldValue | nvarchar(max) | null | N | N | - | N | J | N | Oude waarde bij events met een voor/na-vergelijking, zoals onderwerpwijziging. |
| NewValue | nvarchar(max) | null | N | N | - | N | J | N | Nieuwe waarde bij events met een voor/na-vergelijking, zoals onderwerpwijziging. |
Validaties / constraints
- EventType wordt centraal gestandaardiseerd.
- OldValue en NewValue zijn alleen relevant voor eventtypes die inhoudelijk een wijziging beschrijven.
- Alleen beperkte rich-textopmaak is toegestaan: vet, cursief, onderstreept, drie tekstgroottes, opsommingstekens en genummerde lijsten.
Business rules
- Thread-events zijn geen systeemberichten en ook geen gewone privéberichten.
- Ze worden binnen de thread getoond als systeemachtige regels of ballonnen, bijvoorbeeld bij onderwerpwijziging of het verlaten van een thread.
Lifecycle / gedrag
- Thread-events blijven historisch zichtbaar als onderdeel van de conversatie.
- Privéberichten kennen binnen de initiële scope een bewaartermijn van 3 maanden. Opruimgedrag wordt scheduler-gestuurd uitgevoerd en raakt alleen de zichtbaarheid en beschikbaarheid van privéberichten binnen de daarvoor geldende functionele regels.
- Het verwijderen van een thread voor één deelnemer verwijdert bestaande events niet voor andere deelnemers.
Designkeuzes
Eventinhoud blijft primair technisch en compact. Wanneer eventwaarden renderbare tekst bevatten, moet verwerking server-side veilig en consistent plaatsvinden.
- Aparte eventlaag voorkomt dat gewone berichten vervuild raken met systeemachtige regels.
- Hierdoor blijft de threadweergave semantisch correct en uitbreidbaar.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: ThreadId -> PrivateMessageThreads.Id.
Functionele / logische verwijzingen zonder harde FK
ActorUserIdenAffectedUserIdzijn soft links naarUsers.Id; threadevents blijven leesbaar bij accountlifecycle en anonimisering zonder harde database-FK naar identity.- De velden EventType functioneren als code-, enum- of sleutelwaarden en verwijzen bewust niet via een harde foreign key.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
4.6 Relatiecommunicatie en uitnodigingsberichten
4.6.1 Systeemberichten als ingang voor relatie-uitnodigingen
- Inkomende relatie-uitnodigingen worden altijd via
SystemMessagesaangeboden. - De relatiepagina toont geen aparte inkomende-uitnodigingenlijst.
- Voor
EntityType = RelationshipInvitationverwijstEntityIdnaarRelationshipInvitations.Id. - Wanneer een uitnodiging naar een nog onbekend e-mailadres is verstuurd, kan pas een systeembericht worden aangemaakt nadat het e-mailadres na registratie aan
Users.Idis gekoppeld. - Het openen of lezen van een systeembericht verwerkt de uitnodiging niet automatisch. Acceptatie en afwijzing blijven aparte domeinacties.
- Bij acceptatie en afwijzing ontvangt de uitnodiger een systeembericht of gelijkwaardige notificatie over de uitkomst.
- Wanneer een systeembericht verwijst naar een inmiddels afgehandelde of verlopen uitnodiging, moet de applicatie de actuele domeinstatus tonen en geen verouderde acceptatie- of afwijsactie toestaan.
4.6.2 Scheiding tussen systeemberichten en privéberichten
- Een relatie-uitnodiging is geen privéberichtthread en wordt niet opgeslagen in
PrivateMessages. SystemMessagesblijft het mailboxgerichte systeemberichtdomein; privégesprekken blijven inPrivateMessageThreads,PrivateMessageThreadParticipants,PrivateMessagesenPrivateMessageThreadEvents.- Ongelezenlogica mag in de GUI gecombineerd worden weergegeven, maar de onderliggende domeinen blijven gescheiden.
4.6.3 Relatiecontrole bij privéberichten
- Het starten van een nieuwe privéberichtthread of het verzenden van een privébericht vereist server-side controle op een actieve relatie of systeemrelatie die communicatie toestaat.
- Relatie-ontkoppeling verwijdert bestaande privéberichten of threads niet, maar blokkeert nieuwe relatie-afhankelijke communicatie wanneer geen andere geldige relatiecontext bestaat.
- Frontend-zichtbaarheid van een ontvanger of thread is geen autorisatie; bij verzenden wordt de geldige relatie- of deelnemerscontext opnieuw gecontroleerd.
4.7 Mailbox- en privéberichtregels
4.7.1 Gecombineerde mailboxweergave
- Het berichtenoverzicht is een afgeleid readmodel over
SystemMessagesen privéberichtthreads. - Systeemberichten en privéberichtthreads mogen in dezelfde mailboxweergave worden getoond, maar blijven relationeel gescheiden domeinen.
- Mailboxqueries moeten server-side filteren op de ingelogde gebruiker en mogen geen items tonen waarvoor de gebruiker geen objecttoegang heeft.
- Dynamische aantallen, ongelezenbadges, typefilters, statusfilters, zoekresultaten en paginering zijn afgeleide readmodelwaarden en worden niet als losse mailboxtabel opgeslagen.
- Voor privéthreads toont de preview het eerste nieuwe item voor de ingelogde participant wanneer er ongelezen activiteit is; zonder nieuwe activiteit toont de preview de laatste zichtbare activiteit.
- Het label voor threads wordt afgeleid uit zichtbare actieve deelnemers: twee deelnemers geeft Privégesprek, meer dan twee geeft Groepsgesprek.
- Threadkleur en IconKey komen uit
PrivateMessageThreads; participantaccenten komen uitPrivateMessageThreadParticipants. - Zoeken binnen privéberichtthreads mag worden gebaseerd op zichtbare threadinhoud, waaronder afzendernaam, onderwerp, laatste preview en functioneel doorzoekbare tekstfragmenten binnen de thread.
- Zoeken en filteren veroorzaken geen datamutaties.
4.7.2 Systeemberichten openen
- Het openen van een systeembericht mag
SystemMessages.ReadAtUtczetten wanneer het bericht nog ongelezen is. - Vervolgcontext voor systeemberichten wordt uitsluitend bepaald via
EntityType+EntityId. - Een systeembericht dat verwijst naar een niet-bestaand, verlopen, afgehandeld of niet-toegankelijk domeinobject blijft als mailboxitem bestaan, maar de vervolgactie wordt server-side geblokkeerd.
- Systeemberichten zijn niet participantgebonden verwijderbaar in de berichtenusecases.
4.7.3 Nieuw privébericht en antwoordflow
- Een nieuwe privéberichtthread ontstaat transactioneel met minimaal een threadrecord, participantrecords en het eerste
PrivateMessages-record. - Het beantwoorden van een bestaande thread voegt een nieuw
PrivateMessages-record toe aan dezelfde thread en actualiseertLastMessageAtUtc. - Bij verzenden en beantwoorden wordt de geldige relatie-, systeemrelatie- of deelnemerscontext opnieuw server-side gecontroleerd.
- Een optionele onderwerpwijziging binnen een bestaande thread wordt vastgelegd als
PrivateMessageThreadEvents.EventType = SubjectChangeden niet als gewoon privébericht. - Thread-events na de laatst bekende leespositie kunnen de thread voor deelnemers opnieuw ongelezen maken.
- Tellerupdates en realtime badges worden afgeleid uit systeemberichten, participant-readstate en thread-events; er is geen aparte telleropslag vereist.
- De detailtimeline wordt oud-naar-nieuw opgebouwd binnen een configureerbaar venster. Oudere items worden via een beveiligde opaque cursor geladen, niet via pagina- of datumquerystrings.
- De cursor wordt niet als database-entiteit opgeslagen. Hij bevat protected requestcontext zoals thread-id, user-id, oudste geladen tijdstip en oudste timeline-item-id en wordt bij elk verzoek opnieuw server-side gevalideerd.
- Voor leerlingen kan de zichtbare teller tijdens een actieve oefenrun tijdelijk verborgen blijven zonder dat de onderliggende ongelezenstatus verloren gaat.
4.7.4 Participantgebonden verwijderen
- Een gebruiker verlaat/verwijdert een privéthread uitsluitend uit de eigen mailboxweergave via
PrivateMessageThreadParticipants.DeletedAtUtc. - De eerste geldige verwijder-/verlaatactie schrijft daarnaast één
PrivateMessageThreadEvents-record met eventtypeParticipantLeft, zodat andere deelnemers kunnen zien wie het gesprek heeft verlaten. - Verwijderen is idempotent: herhaling door dezelfde deelnemer mag geen extra domeinrecords of dubbele zichtbaarheidseffecten veroorzaken.
- Andere deelnemers behouden hun eigen participantregel, zichtbaarheid, readstate en toegang tot de thread zolang hun eigen context dat toestaat.
- De thread, private messages en thread-events worden niet hard verwijderd door een mailboxverwijderactie.
- Een participantgebonden verwijderde thread mag niet meer meetellen in mailboxfilters, zoekresultaten, paginering of ongelezenbadges voor die deelnemer.
4.7.5 Niet-beschikbaarheid en retentie
- Privéberichten kennen binnen de initiële scope een bewaartermijn van drie maanden; scheduler-gestuurde cleanup mag privéberichtinhoud na afloop van de termijn functioneel onbeschikbaar maken volgens de geldende retentieregels.
- Systeemberichten volgen niet automatisch dezelfde retentie als privéberichten.
- Wanneer een mailboxitem of thread door autorisatie, participantdelete, domeinstatus of retentie niet meer beschikbaar is, volgt een veilige niet-beschikbaarafhandeling zonder nieuwe domeinmutatie.
4.8 Meldingscommunicatie
- Tickets gebruiken
SystemMessagesals mailboxkanaal voor meldingsupdates, vragen aan de melder, oplossingen, heropenrelevante updates en doorzetinformatie. - Voor meldingen gebruikt
SystemMessages.EntityType = Ticketen verwijstEntityIdnaarTickets.Id. - Voor gedeelde oefeningen gebruikt
SystemMessages.EntityType = SharedExerciseen verwijstEntityIdnaarSharedExercises.Id. Het systeembericht opent de gedeelde-oefeningcontext, maar start of verwerkt de oefening niet automatisch. - Het openen van een ticketgerelateerd systeembericht verwerkt de ticketactie niet automatisch; reageren, accepteren, heropenen of andere ticketacties blijven aparte domeincommands.
- Externe ticketdiscussie wordt opgeslagen in
TicketDiscussionMessagesen niet inPrivateMessages. - Interne ticketdiscussie is uitsluitend beheerdersinformatie en mag nooit via
SystemMessagesaan de melder worden gestuurd. - Doorzetten naar docent is de expliciete uitzondering waarbij de ticketflow ook een privéberichtthread kan genereren. Die privécommunicatie blijft opgeslagen in
PrivateMessageThreadsenPrivateMessages, metSendAsUserIdvoor de functionele afzenderweergave namens de melder.
4.9 Account-lifecycle en berichten
4.9.1 Systeemberichten na accountprovisioning
Een SystemMessages-record vereist altijd een interne ontvanger via RecipientUserId. Voor relatie-uitnodigingen naar onbekende e-mailadressen kan daarom pas een systeembericht voor de ontvanger worden aangemaakt nadat accountprovisioning een intern Users.Id heeft opgeleverd en de uitnodiging aan RelationshipInvitations.ToUserId is gekoppeld.
Het systeembericht verwijst daarna met EntityType = RelationshipInvitation en EntityId = RelationshipInvitations.Id. Reguliere login maakt geen systeemberichten aan; alleen eerste provisioning en uitnodigingskoppeling kunnen dit effect hebben. Het lezen of openen van het systeembericht verwerkt de uitnodiging niet automatisch.
Mailbox-openroutes mogen het bericht vóór detailrendering als gelezen markeren, maar blijven readstate-acties. Het onderliggende object, zoals een relatie-uitnodiging, wordt pas verwerkt door een aparte domeinbeslissing.
4.9.2 Accountanonimisering en communicatie
Bij accountanonimisering blijven systeemberichten, privéberichtthreads en privéberichtinhoud historisch volgens de geldende retentie- en zichtbaarheidregels bestaan. De mailboxzichtbaarheid en actieve privéberichtdeelname van het verwijderde account worden beëindigd of niet langer gebruikt, zonder threadinhoud voor andere deelnemers generiek te verwijderen.
Systeemnotificaties zijn geen mailbox-systeemberichten en worden niet aangemaakt of gewijzigd door login, onvolledige-accountafhandeling of logout. Zij worden pas na een geladen frontpage- of contextweergave via het systeemnotificatiedomein behandeld.
4.10 Beheerder- en ouder-/voogdcommunicatie
| Onderwerp | Aanscherping |
|---|---|
| Systeemberichttemplates | Beheer van systeemberichttemplates wijzigt bestaande template-inhoud, maar maakt geen nieuwe technische referenties aan en verwijdert geen verplichte systeemcommunicatie. |
| Templatehistorie | Wijzigingen aan systeemberichttemplates leggen minimaal actor, tijdstip, veldnaam, oude waarde en nieuwe waarde vast. |
| Runtime systeemberichtinhoud | SystemMessages bewaren de gerenderde titel/body. Templatewijzigingen gelden voor toekomstige berichten en vereisen geen FK van runtimebericht naar template of templatehistorie. |
| Private thread mailboxregel | Het mailboxreadmodel toont één regel per zichtbare private thread, niet per individueel privébericht. De thread- en participantstructuur blijft geschikt voor 2+ deelnemers. |
| Systeemnotificaties | Systeemnotificaties zijn geen mailbox-systeemberichten en geen popupregister-popups. Zij worden via een eigen frontpage-overlay/notificatiecomponent na frontpageload getoond. |
| DisplayRule | Always wordt niet permanent als gezien geregistreerd. OncePerBrowser wordt clientside via browserwaarde op notificatie-id onderdrukt en gebruikt geen server-side seen-tabel. |
| UTC-datums | Start- en eindmomenten van systeemnotificaties worden in UTC opgeslagen en in de UI naar lokale tijd vertaald. |
| Ouder-/voogdontkoppeling | Ontkoppeling van een kind vanuit ouder-/voogdcontext kan systeemcommunicatie aan het kind veroorzaken, maar maakt geen privéberichtthread aan. |
| Beheerpopupfouten | Fout- en validatiemeldingen vanuit beheerformulieren verwijzen naar popupregisterkeys en dupliceren geen popuptekst in usecases of schermdocumentatie. |
4.11 Ouder-/voogdcommunicatie en veilige berichtgrenzen
| Onderwerp | Aanscherping |
|---|---|
| Ontkoppelcommunicatie | Na ouder-/voogdontkoppeling kan een systeembericht aan het kind worden aangemaakt. Dit bericht is informatief en voert bij openen geen relatieherstel, acceptatie of nieuwe uitnodiging uit. |
| Geen privéthread | Ontkoppeling vanuit ouder-/voogdcontext maakt geen privéberichtthread aan en gebruikt geen threadgebeurtenis als vervanging voor systeemcommunicatie. |
| Resultaattoegang geweigerd | Geweigerde resultaat-, geschiedenis- of PDF-verzoeken tonen veilige feedback zonder kindnaam, runinhoud of gedeeltelijke resultaatdata te lekken. |
| Live foutmeldingen | Live-beschikbaarheid, toegang geweigerd, sessie-einde en verbindingsverlies verwijzen via PopupKey naar het popupregister; popupteksten worden niet in usecases of schermdocumentatie gedupliceerd. |
4.12 Applicatieschil tijdens actieve leerling-oefenrun
Tijdens een actieve leerling-oefenrun mogen berichtenbadges, meldingenindicaties, systeemnotificatie-overlays en vergelijkbare communicatie-terugkoppelingen de leerling niet visueel afleiden. De onderliggende SystemMessages, privébericht-readstates, ticketactie-indicaties en notificatiedata blijven server-side correct bestaan. Alleen de zichtbaarheid of timing van de UI-indicatie wordt tijdelijk onderdrukt of uitgesteld tot de oefencontext is verlaten of afgerond.