Relatiebeheer, uitnodigingen en gedeelde oefeningen
12.1 Doel en scope
Dit hoofdstuk beschrijft de technische inrichting van relatiebeheer, relatie-uitnodigingen, relatie-events, soft-deactivatie en de technische koppeling met gedeelde oefeningen. Het hoofdstuk werkt de realisatie uit binnen de modulaire monoliet, zonder de functionele regels uit Functioneel Ontwerp, Software Requirements Specification of database-informatie opnieuw als tweede bron te definiëren.
Relatiebeheer is technisch eigendom van de module OefenHub.Relationships. Deze module bepaalt of twee gebruikers binnen een bepaalde rolcontext een geldige relatie of openstaande uitnodiging hebben. Andere modules gebruiken deze informatie via publieke contracts en mogen niet rechtstreeks in relationship-tabellen, entities of DbContexts lezen of schrijven.
Gedeelde oefeningen zijn functioneel afhankelijk van vriendschappen, maar de gedeelde oefening zelf en de daaruit ontstane runs vallen niet onder Relationships. De relatie-module levert alleen de autorisatie- en relatiecheck die nodig is om delen toe te staan.
12.2 Afbakening ten opzichte van andere modules
| Onderdeel | Eigenaar | Toelichting |
|---|---|---|
| Relatietypen, actieve relaties, relatie-uitnodigingen en relatie-events | OefenHub.Relationships | Bron van waarheid voor relatievorming en relatie-lifecycle. |
| Gebruikers, rollen en accountstatus | OefenHub.Identity en OefenHub.Authorization | Relationships verwijst naar gebruikers via soft links en valideert via publieke contracts. |
| Systeemberichten en privéthreads | OefenHub.Communication | Relationships vraagt communicatie aan via contracts; schrijft niet rechtstreeks in communication. |
| Gedeelde oefeningen en runs | OefenHub.Practice | Practice maakt gedeelde oefeningrecords en ontvangersruns aan. |
| Oefening-, categorie- en niveausnapshots | OefenHub.Practice op basis van OefenHub.Catalog | Relationships bewaart geen oefeninhoud. |
| Relatie-uitnodigingen koppelen na provisioning | OefenHub.Identity als workfloweigenaar, via Relationships en Communication contracts | Identity is eigenaar van accountprovisioning. |
| Verlopen uitnodigingen | OefenHub.Scheduling triggert, Relationships voert domeinactie uit | Scheduling beheert job-lifecycle; Relationships blijft domeineigenaar. |
| Geforceerde beheerderontkoppeling | OefenHub.Relationships, aangeroepen vanuit OefenHub.Admin | De relatie-mutatie blijft in Relationships. |
12.3 Project, DbContext en schema
OefenHub.Relationships volgt de standaard moduleprojectstructuur uit het Technisch Ontwerp. Het project heeft maximaal één eigen DbContext en daarmee één databaseschema.
OefenHub.Relationships/
Contracts/
Models/
Enums/
Data/
RelationshipsDbContext.cs
Entities/
Configurations/
Migrations/
Models/
Commands/
Queries/
ReadModels/
Enums/
Services/
Interfaces/
Events/
Helpers/
Extensions/
| Technisch onderdeel | Keuze |
|---|---|
| Project | OefenHub.Relationships |
| DbContext | RelationshipsDbContext |
| Schema | relationships |
| Schema-naamgeving | kleine letters |
| Tabel- en kolomnaamgeving | PascalCase |
| Entity-zichtbaarheid | standaard internal |
| Publieke toegang | uitsluitend via Contracts |
| Cross-module user-verwijzingen | soft link, eventueel met snapshot |
| Cross-module communicatie | via publieke contracts van andere modules |
12.4 Relatietypen
De relatie-module ondersteunt meerdere relatietypen, waarbij dezelfde twee natuurlijke personen meer dan één relatie kunnen hebben wanneer relatietype en rolcontext verschillen.
| Relatietype | Technische code | Initiatie | Kernregel |
|---|---|---|---|
| Vriendschap tussen leerlingen | Friendship | uitnodiging en acceptatie | Alleen tussen leerlingcontexten; nodig voor gedeelde oefeningen. |
| Ouder-/voogdrelatie | GuardianStudent | uitnodiging en acceptatie | Vereist voor ouder-/voogdinzage en live meekijken. |
| Docent-leerlingrelatie | TeacherStudent | docent initieert | Voorwaarde voor niveauautorisaties vanuit docentcontext. |
| Docent-docentrelatie | TeacherTeacher | uitnodiging en acceptatie | Voorwaarde voor collaborators en samenwerking op niveau-laag. |
| Beheerder-beheerderrelatie | AdminAdmin | systeemgestuurd | Wordt automatisch actief gehouden zolang beide gebruikers actieve beheerderrol hebben. |
De technische codes worden niet als vrij beheerbare GUI-data behandeld. Als de waardelijst wijzigt, moet dit via database-informatie, code, seeddata/migratie en impact op het Technisch Ontwerp worden beoordeeld.
12.5 Publieke contracts
Andere modules gebruiken relationship-functionaliteit via publieke contracts. Interne entities, repositories, services en DbContext blijven buiten bereik.
Voorbeelden van publieke contracts:
public interface IRelationshipAccessReader
{
Task<bool> HasActiveFriendshipAsync(RelationshipAccessRequest request, CancellationToken cancellationToken);
Task<bool> HasActiveGuardianStudentRelationAsync(RelationshipAccessRequest request, CancellationToken cancellationToken);
Task<bool> HasActiveTeacherStudentRelationAsync(RelationshipAccessRequest request, CancellationToken cancellationToken);
Task<bool> HasActiveTeacherTeacherRelationAsync(RelationshipAccessRequest request, CancellationToken cancellationToken);
}
public interface IRelationshipInvitationService
{
Task<CreateRelationshipInvitationResult> CreateInvitationAsync(CreateRelationshipInvitationCommand command, CancellationToken cancellationToken);
Task<AcceptRelationshipInvitationResult> AcceptInvitationAsync(AcceptRelationshipInvitationCommand command, CancellationToken cancellationToken);
Task<RejectRelationshipInvitationResult> RejectInvitationAsync(RejectRelationshipInvitationCommand command, CancellationToken cancellationToken);
}
public interface IRelationshipAdministrationService
{
Task<DeactivateRelationshipResult> DeactivateRelationshipAsync(DeactivateRelationshipCommand command, CancellationToken cancellationToken);
Task<ForceDeactivateRelationshipResult> ForceDeactivateRelationshipAsync(ForceDeactivateRelationshipCommand command, CancellationToken cancellationToken);
}
Richtlijnen:
- Command-services wijzigen relationship-brondata.
- Query-services/readers leveren relatie-informatie aan andere modules.
- Publieke DTO's voor deze contracts staan onder
Contracts/Models. - Interne models, readmodels en command handlers blijven buiten
Contracts. - Resulttypes bevatten functionele foutcodes en geen technische database-exceptions.
12.6 Data-eigenaarschap en technische verwijzingen
Relationships bewaart relatie- en uitnodigingsdata, maar is geen eigenaar van gebruikersaccounts. Verwijzingen naar gebruikers uit Identity zijn daarom cross-module verwijzingen.
| Verwijzing | Technische vorm | Reden |
|---|---|---|
FromUserId / ToUserId naar identity-user | soft link | Identity is eigenaar van users; relationships blijft historisch reconstrueerbaar. |
| Rolcontext bij uitnodiging of relatie | soft link + snapshotcode/naam waar nodig | Rolnamen kunnen wijzigen; historie moet leesbaar blijven. |
| E-mailadres bij uitnodiging naar onbekend account | waarde/snapshot in invitation | Er bestaat nog geen intern user-id. |
| Actor bij relatie-event | soft link + actor snapshot waar nodig | Audit moet bruikbaar blijven na anonimisering. |
| Ontstane relatie vanuit uitnodiging | harde FK binnen relationships mogelijk | Beide records horen bij dezelfde module. |
Binnen het schema relationships zijn harde FK's toegestaan wanneer zij de interne consistentie van het relatie-domein beschermen. Over de modulegrens heen geldt soft link of soft link + snapshot als standaard.
Voorbeelden:
relationships.RelationshipInvitations.AcceptedRelationshipId
→ harde FK naar relationships.UserRelationships.Id toegestaan
relationships.UserRelationships.FromUserId
→ soft link naar identity.Users.Id, geen standaard cross-module FK
relationships.RelationshipEvents.ActorUserId
→ soft link + optionele actor snapshot
12.7 Relatie-uitnodigingen
Een uitnodiging is een tijdelijk, auditbaar intentierecord. De uitnodiging maakt nog geen actieve relatie aan. Pas bij acceptatie ontstaat een actieve UserRelationship.
12.7.1 Technische statussen
| Status | Betekenis |
|---|---|
Pending | Uitnodiging is openstaand en nog accepteerbaar. |
Accepted | Uitnodiging is geaccepteerd en heeft geleid tot een actieve relatie. |
Rejected | Uitnodiging is afgewezen en niet langer accepteerbaar. |
Expired | Uitnodiging is verlopen en niet langer accepteerbaar. |
Active is geen uitnodigingsstatus. Actief is de toestand van een ontstane relatie.
12.7.2 Aanmaken van een uitnodiging
Het aanmaken van een uitnodiging bestaat technisch uit preflight-validatie, domeinmutatie en eventueel communicatie.
1. Actorcontext bepalen via server-side sessie en rolcontext.
2. Relatietype en toegestane FromRole/TargetRole valideren.
3. Ontvanger bepalen op basis van e-mailadres of bestaand account.
4. Conflicten controleren.
5. Voor onbekende externe adressen: expliciete consent opvragen en mail-preflight uitvoeren voordat de invitation zichtbaar/pending wordt opgeslagen.
6. Invitation aanmaken in relationships.
7. RelationshipEvent vastleggen.
8. Als de ontvanger een intern account heeft en systeembericht de primaire ingang is: systeembericht atomair aanmaken.
9. Als de ontvanger extern is: MailSendAttempt opslaan en TickerQ-deliveryjob plannen.
10. Resultaat teruggeven aan Web.
12.7.3 Uitnodiging naar onbekend e-mailadres
Wanneer de ontvanger nog geen intern account heeft, wordt niet direct stilzwijgend een uitnodiging opgeslagen. Web toont eerst welke adressen intern zijn verwerkt, welke adressen onbekend zijn en welke adressen geweigerd zijn. Alleen wanneer de uitnodigende gebruiker expliciet bevestigt dat onbekende adressen per externe OefenHub-mail mogen worden uitgenodigd en dat de volledige naam van de uitnodiger in die mail wordt gedeeld, mag de flow doorgaan. De bevestiging wordt persistent vastgelegd met ExternalInviteConsentGiven en ExternalInviteConsentAtUtc zodra het externe verzoek daadwerkelijk wordt aangemaakt.
Vóór opslag voert Web via OefenHub.Mail een synchrone preflight uit voor actieve template, senderconfiguratie en placeholderrendering. Pas daarna wordt de RelationshipInvitation met ToUserId = null aangemaakt, wordt de mail als MailSendAttempts vastgelegd en wordt de TickerQ-deliveryjob gepland. Een externe uitnodiging mag pas zichtbaar/openstaand zijn wanneer deze interne mailqueue-aanvraag duurzaam is geaccepteerd: het mailattempt bestaat en de bijbehorende TickerQ-job is gepland.
Als de preflight faalt, wordt geen invitation opgeslagen en krijgt de gebruiker veilige foutfeedback. Als de queue of jobplanning na het aanmaken alsnog faalt, wordt het zojuist aangemaakte externe verzoek ingetrokken/verborgen en blijft er geen zichtbaar pending extern relatieverzoek achter. Er wordt nog geen systeembericht aangemaakt, omdat er nog geen interne ontvanger bestaat. Als de gebruiker geen externe e-mailuitnodiging bevestigt, wordt voor onbekende adressen niets opgeslagen.
Bij latere provisioning is Identity eigenaar van de workflow:
Identity provisioning
→ zoekt geldige pending invitations voor genormaliseerd e-mailadres
→ roept Relationships aan om ToUserId te koppelen
→ roept Communication aan om systeembericht voor acceptatie aan te maken
Deze koppeling mag verlopen, afgewezen of geaccepteerde uitnodigingen niet heractiveren.
12.7.4 Verzendherinneringen en intrekken
Verzonden openstaande uitnodigingen ondersteunen twee muterende vervolgacties zolang de uitnodiging nog Pending is en door de ingelogde gebruiker is aangemaakt.
Intrekken zet de uitnodiging op Withdrawn, verbergt de uitnodiging uit het openstaande overzicht en registreert een InvitationWithdrawn-event. De actie is destructief vanuit gebruikersperspectief en moet in Web altijd via een bevestigingsmodal lopen. De maildeliveryguard blijft bronstatus controleren, zodat een oude of handmatig opnieuw gestarte deliveryjob geen mail meer verstuurt voor een ingetrokken, verlopen, geaccepteerde of geweigerde uitnodiging.
Herinneren is rate-limited. De wachttijd wordt opgehaald uit admin.SystemSettings via de sleutel RelationshipInvitationReminderCooldownHours, uitgedrukt in uren, met standaardwaarde 24. De server controleert de cooldown op basis van de laatste succesvolle uitnodigings- of herinnernotificatie voor dezelfde invitation. Zolang de cooldown niet is verstreken, mag de UI de actie disabled tonen met een uitleg; de server blijft de definitieve autoriteit.
Een herinnering is altijd een nieuwe notificatiepoging. Voor een bestaande OefenHub-gebruiker maakt Web via OefenHub.Communication een nieuw systeembericht aan en registreert InvitationReminderSystemMessageCreated. Voor een extern e-mailadres maakt Web via OefenHub.Mail een nieuwe mailaanvraag met Purpose = Reminder en dezelfde bronkoppeling SourceEntityType = RelationshipInvitation en SourceEntityId = RelationshipInvitations.Id; bij succesvolle queueing wordt InvitationReminderMailQueued vastgelegd. De oorspronkelijke mailattempt wordt nooit hergebruikt voor een herinnering.
12.8 Conflictpreventie
Conflictpreventie gebeurt server-side binnen Relationships. Clientstate, zichtbare knoppen of oude routeparameters mogen geen uitnodiging of relatie afdwingen.
| Conflict | Afhandeling |
|---|---|
| Zelfde relatietype en rolcontext bestaat al actief | Nieuwe uitnodiging blokkeren. |
| Zelfde relatietype en rolcontext heeft al pending uitnodiging | Nieuwe uitnodiging blokkeren of bestaande pending toestand tonen. |
| Ontvanger heeft vereiste doelrol niet | Bestaand account blokkeren met veilige melding; onbekend e-mailadres alleen na expliciete externe-mailbevestiging als pending op e-mail behandelen. |
| Leerling probeert docentrelatie te initiëren | Blokkeren. |
| Friendship buiten leerling-leerlingcontext | Blokkeren. |
| AdminAdmin handmatig uitnodigen | Blokkeren; systeemgestuurde relatie. |
| Relatie is eerder gedeactiveerd | Nieuwe uitnodiging alleen toestaan volgens geldende domeinregels en conflictcontrole. |
Er is geen functioneel hard maximum per relatietype, behalve waar de combinatie van gebruikers, relatietype en rolcontext uniek actief moet blijven.
12.9 Acceptatie en afwijzing
12.9.1 Acceptatie
Acceptatie verwerkt de uitnodiging en maakt de relatie aan. Dit is één kritieke gebruikersactie en moet atomair worden uitgevoerd voor alle bronmutaties die nodig zijn om een geldige relatie te vormen.
Kritieke stappen bij acceptatie:
- invitation opnieuw ophalen met server-side autorisatiecontrole;
- status en geldigheid controleren;
- target rolcontext controleren;
- conflictcontrole opnieuw uitvoeren;
- UserRelationship aanmaken;
- invitation op Accepted zetten;
- link naar ontstane relatie vastleggen;
- RelationshipEvent vastleggen.
Als één van deze stappen faalt, mag geen half-geaccepteerde uitnodiging achterblijven.
Systeemcommunicatie over acceptatie of afwijzing kan kritisch of retrybaar zijn afhankelijk van de functionele rol. Wanneer het bericht de primaire of enige ingang is voor een vervolgactie, wordt het als kritisch onderdeel van de workflow behandeld. Wanneer het uitsluitend informatief is en de doeltoestand elders zichtbaar blijft, kan het via retrybare communicatie worden verwerkt.
12.9.2 Afwijzing
Afwijzing zet de uitnodiging op Rejected en registreert een relationship-event. De afwijzing maakt geen relatie aan. Wanneer de afwijzingscommunicatie informatief is, mag deze retrybaar zijn. De uitnodigingsstatus zelf blijft de bron van waarheid.
12.10 Verlopen uitnodigingen
Het verlopen van uitnodigingen wordt periodiek verwerkt via OefenHub.Scheduling en TickerQ. Scheduling beheert de technische joblifecycle; Relationships voert de domeinmutatie uit.
Scheduling job
→ Relationships.ExpirePendingInvitationsAsync(...)
→ Relationships zoekt pending invitations waarvan ExpiresAtUtc is verstreken
→ Relationships zet status op Expired
→ Relationships schrijft RelationshipEvent
→ Scheduling registreert jobresultaat, poging en eventuele fout
Deze verwerking is idempotent. Een reeds verlopen, geaccepteerde of afgewezen uitnodiging wordt niet opnieuw gewijzigd.
12.11 Actieve relaties
Een actieve relatie wordt opgeslagen als UserRelationship met IsActive = true. Beëindigen gebeurt via soft-deactivatie en niet via hard delete.
| Eigenschap | Technische regel |
|---|---|
| Relatierecord | Blijft historisch bestaan. |
| Actieve toestand | IsActive = true/false. |
| Deactivering | Actor, rolcontext, tijdstip en reden vastleggen waar vereist. |
| Historie | RelationshipEvent vastleggen. |
| Afhankelijke toegang | Andere modules controleren actuele relatie via contracts. |
| Oude resultaten/runs | Niet verwijderen door relatiebeëindiging. |
12.12 Ontkoppelen en geforceerde beheerderontkoppeling
Ontkoppelen wordt per relatietype verschillend geautoriseerd, maar technisch via dezelfde relationship-lifecycle verwerkt.
| Ontkoppeling | Technische afhandeling |
|---|---|
| Vriendschap | Beide partijen mogen beëindigen; relatie soft-deactiveren; andere partij informeren. |
| Ouder/voogd door ouder/voogd | Directe soft-deactivatie; kind informeren. |
| Ouder/voogd door leerling | Geen directe deactivatie indien functioneel bevestiging vereist is; RelationshipDisconnectRequested vastleggen of actie blokkeren volgens Functioneel Ontwerp en Software Requirements Specification. |
| Docent-leerling door docent | Soft-deactivatie; gekoppelde niveauautorisaties intrekken via daarvoor bestemde eigenaarflow. |
| Docent-leerling door leerling | Geen directe verbreking; RelationshipDisconnectRequested vastleggen of actie blokkeren volgens Functioneel Ontwerp en Software Requirements Specification. |
| Docent-docent | Soft-deactivatie; actieve collaborator-koppelingen moeten door catalog/authorisatie-eigenaar worden gedeactiveerd via contracts. |
| AdminAdmin | Niet handmatig verbreekbaar zolang beide gebruikers actief beheerder zijn. |
| Geforceerd door beheerder | Alleen via beheercontext, met reden, actor, rolcontext, relationship-event en passende systeemcommunicatie. |
Geforceerde beheerderontkoppeling wordt technisch uitgevoerd door Relationships, ook wanneer de actie vanuit Admin wordt gestart. Admin roept dus een publiek administration-contract aan en wijzigt niet rechtstreeks relationship-data.
Wanneer een relatietype een verzoekflow vereist, wordt de bestaande relatie niet gedeactiveerd bij het indienen van het verzoek. De relationship-module legt het verzoek vast als lifecycle-gebeurtenis, bijvoorbeeld RelationshipDisconnectRequested, met actor, rolcontext, relatietype, tijdstip en eventuele reden. Pas een bevoegde vervolgactie mag het verzoek accepteren, afwijzen, laten vervallen of omzetten naar een daadwerkelijke soft-deactivatie. Zolang die vervolgactie niet is afgerond, blijft de relatie functioneel actief en blijft autorisatie afhangen van de bestaande relatie en rolcontext.
12.13 Relatie-afhankelijke toegang
Andere modules mogen relatie-afhankelijke toegang niet zelf reconstrueren uit relationship-tabellen. Zij gebruiken relationship-contracts.
Voorbeelden:
Practice
→ vraagt IRelationshipAccessReader.HasActiveFriendshipAsync(...) voordat delen wordt toegestaan.
LiveMonitoring
→ vraagt IRelationshipAccessReader.HasActiveGuardianStudentRelationAsync(...) voor ouder-/voogd live meekijken.
Catalog/Authorization
→ gebruikt docentcontext en docent-leerlingrelatie voor niveauautorisaties.
Communication
→ gebruikt relatiecontext om toegestane privéberichtontvangers te bepalen.
Een positieve relatiecheck is momentgebonden. Bij iedere gevoelige vervolgactie moet server-side opnieuw worden gecontroleerd of de relatie nog actief is.
12.14 Gedeelde oefeningen
Gedeelde oefeningen zijn geen relationship-data. De bronrecord voor een ontvangen gedeelde oefening hoort bij Practice, omdat de gedeelde oefening verwijst naar oefenruninhoud, snapshots en de daaruit ontstane ontvangersrun.
Relationships levert alleen de voorwaarde:
Mag gebruiker A een afgeronde oefening delen met gebruiker B?
Voor de eerste technische baseline betekent dit minimaal:
- delen is toegestaan voor de afzender;
- afzender heeft een leerlingcontext;
- ontvanger heeft een leerlingcontext;
- er bestaat een actieve Friendship-relatie;
- de bronrun is afgerond en deelbaar;
- de ontvanger is niet dezelfde gebruiker als de afzender.
De precieze run- en deelbaarheidscontrole ligt bij Practice. De relatiecheck ligt bij Relationships.
12.14.1 Technische deel-flow
1. Web vraagt Practice om een afgeronde oefening te delen.
2. Practice valideert run, eigenaar, status en deelbaarheid.
3. Practice vraagt Relationships of actieve Friendship bestaat.
4. Practice maakt SharedExercise/ontvangen gedeelde oefening aan.
5. Practice bewaart snapshots van niveau, categorie, oefening en modulecontext waar nodig.
6. Practice vraagt Communication om ontvanger te informeren.
7. Communication maakt systeembericht of retrybare communicatieactie afhankelijk van workflowkeuze.
Omdat ontvangen gedeelde oefeningen ook via een eigen overzicht beschikbaar zijn, is het systeembericht in de baseline niet de enige gegevensbron voor de ontvanger. Het bericht is wel belangrijk voor gebruikersfeedback en moet betrouwbaar worden verwerkt. Als via een expliciet besluit wordt vastgelegd dat het systeembericht de primaire of enige ingang wordt, moet de transaction boundary opnieuw worden beoordeeld.
12.14.2 Verwijderen uit ontvangen overzicht
Wanneer de ontvanger een gedeelde oefening uit het eigen overzicht verwijdert, is dat een Practice-mutatie. Relationships wordt hierbij niet aangepast. Een vriendschap wordt niet beëindigd en afgeronde runs blijven via geschiedenis beschikbaar volgens de regels van Practice.
12.15 Cross-module transaction boundaries
Voor relatiebeheer wordt niet één generieke transactieregel gebruikt. Per workflow wordt bepaald welke stappen kritisch zijn voor een geldige gebruikersactie.
| Workflow | Kritieke stappen | Mogelijk retrybaar |
|---|---|---|
| Uitnodiging naar bestaand account | Invitation aanmaken; conflictcontrole; RelationshipEvent; systeembericht als primaire ingang | Informatieve vervolgmelding, badge-update |
| Uitnodiging naar onbekend e-mailadres | Invitation aanmaken; e-maildoel en expiry vastleggen | Systeembericht pas na provisioning |
| Acceptatie | Invitation status; UserRelationship; relationship-event; conflictcontrole | Informatieve acceptatiemelding |
| Afwijzing | Invitation status; relationship-event | Informatieve afwijzingsmelding |
| Ontkoppelen | IsActive = false; deactivatiegegevens; relationship-event | Informatieve systeemcommunicatie indien geen primaire ingang |
| Geforceerde beheerderontkoppeling | Deactivatie; reden; actor; role context; relationship-event | Informatieve vervolgcommunicatie indien herstelbaar |
| Gedeelde oefening | SharedExercise in Practice; friendshipcheck; run/snapshotvalidatie | Systeembericht wanneer overzicht de primaire bron blijft |
| Verlopen uitnodigingen | Status naar Expired; event; idempotentie | Geen gebruikerscommunicatie tenzij functioneel vereist |
Bij twijfel wordt een stap als kritisch behandeld totdat expliciet is onderbouwd dat uitgestelde of retrybare verwerking geen ongeldige, onbereikbare of misleidende toestand kan veroorzaken. Deze workflowindeling is onderdeel van de V1.0-transaction-boundarybaseline; wijzigingen in primaire acceptatie-ingangen, communicatiekritikaliteit of relatiebrondata vereisen herbeoordeling volgens hoofdstuk 26.
12.16 Scheduling en retrybare acties
Relationships gebruikt Scheduling voor periodieke of uitgestelde verwerking, maar blijft eigenaar van de domeinactie.
Mogelijke jobs:
| Job | Technische eigenaar lifecycle | Domeineigenaar uitvoering | Doel |
|---|---|---|---|
Relationships.ExpireInvitations | OefenHub.Scheduling | OefenHub.Relationships | Pending uitnodigingen laten verlopen. |
Relationships.SyncAdminRelationships | OefenHub.Scheduling | OefenHub.Relationships eventueel met Identity/Authorization contracts | AdminAdmin-relaties actueel houden. |
Relationships.RetryCommunicationRequest | OefenHub.Scheduling of Communication afhankelijk van eigenaarschap | Communication, aangeroepen vanuit eerder geregistreerde workflow | Mislukte niet-kritieke communicatie herstellen. |
Retrybare acties zijn begrensd en moeten beheerbaar kunnen falen. Zij krijgen minimaal jobtype, correlation-id, pogingnummer, laatste fout en status.
12.17 Systeemcommunicatie
Relatieflows kunnen systeemcommunicatie veroorzaken, maar Relationships schrijft niet rechtstreeks in communication.SystemMessages.
Gebruik:
Relationships
→ ISystemMessageService.CreateSystemMessageAsync(...)
Systeemberichten voor relatie-uitnodigingen gebruiken het toegestane verwijstype:
EntityType = RelationshipInvitation
EntityId = RelationshipInvitation.Id
Systeemcommunicatie moet geen gevoelige payloads, tokens of interne foutdetails bevatten. De frontend bepaalt de vervolgrouting op basis van EntityType, EntityId en actuele autorisatiecontrole.
12.18 Logging, correlation en domeinhistorie
Relatiebeheer gebruikt twee soorten herleidbaarheid:
| Soort | Plaats | Doel |
|---|---|---|
| Domeinhistorie | relationships.RelationshipEvents | Functionele reconstructie van uitnodiging, acceptatie, afwijzing, ontkoppeling en beheeracties. |
| Technische logging | centrale loggingprovider via Web/Scheduling/Infrastructure | Diagnose van technische fouten, correlation en operationele analyse. |
Minimale correlationgegevens bij relatieflows:
CorrelationId
ActorUserId, indien beschikbaar
ActorRoleContext
RelationshipType
InvitationId, indien beschikbaar
RelationshipId, indien beschikbaar
TargetUserId, indien bekend
NormalizedTargetEmail, alleen waar functioneel toegestaan en veilig gelogd
Action
Result
FailureCode
Technische logs mogen geen wachtwoorden, tokens, identity-providerpayloads, vrije berichtinhoud of volledige gevoelige persoonsgegevens bevatten.
12.19 Privacy, anonimisering en account lifecycle
Relatiegegevens bevatten verwijzingen naar personen en moeten daarom aansluiten op accountstatus en anonimisering.
| Situatie | Technische afhandeling |
|---|---|
| Account wordt gedeactiveerd | Nieuwe reguliere relatie-acties blokkeren; historie behouden. |
| Account wordt geanonimiseerd | Actieve relaties van/naar account soft-deactiveren of niet langer autoriserend maken volgens domeinregels. |
| Open uitnodigingen van/naar account | Niet langer accepteerbaar maken waar nodig. |
| Historische events | Behouden, maar zichtbare persoonsgegevens vervangen of beperken volgens anonimiseringbeleid. |
| Privéthreads of systeemberichten | Worden door Communication afgehandeld, niet door Relationships. |
| Oefenruns en gedeelde oefeningen | Blijven bij Practice; relatiebeëindiging verwijdert geen runhistorie. |
Snapshots of tekstuele waarden in relationship-events mogen alleen worden opgeslagen wanneer zij nodig zijn voor auditbaarheid of functionele reconstructie. Bij anonimisering moet duidelijk zijn welke snapshotwaarden worden overschreven met vaste anonimiseringswaarden.
12.20 Autorisatie en server-side contextcontrole
Relatieacties worden altijd server-side geautoriseerd. Webknoppen, routeparameters, oude browserstate of zichtbaar gemaakte relatiegegevens zijn nooit voldoende.
Te controleren per actie:
- ingelogde gebruiker;
- actieve accountstatus;
- actieve rolcontext;
- relatietype;
- actorrol en doelrol;
- bestaande relatie of uitnodiging;
- status van uitnodiging;
- geldigheidstermijn;
- conflictregels;
- beheercontext bij geforceerde acties.
Een gebruiker met meerdere rollen mag niet impliciet relatieacties uitvoeren vanuit een andere rolcontext dan de actieve of expliciet gekozen context.
12.21 Readmodels en queries
Relationship-readmodels staan onder Models/ReadModels binnen OefenHub.Relationships.
Voorbeelden:
Models/ReadModels/
RelationshipOverviewReadModel.cs
SentInvitationReadModel.cs
RelationshipEventReadModel.cs
EligibleMessageRecipientReadModel.cs
Readmodels zijn afgeleid. Zij wijzigen geen relatie, uitnodiging of event. Query-services mogen relationele details verbergen en alleen functioneel relevante gegevens teruggeven aan Web of andere modules.
12.22 Teststrategie
Relatiebeheer krijgt eigen testprojecten onder tests.
tests/OefenHub.Relationships.Tests/
Unit/
Integration/
Contracts/
TestData/
Minimale testdekking:
| Testgebied | Voorbeelden |
|---|---|
| Relatietypen en rolcontext | Friendship alleen leerling-leerling; AdminAdmin niet handmatig. |
| Conflictpreventie | Geen dubbele actieve relatie met zelfde type/rolcontext; pending duplicaten blokkeren. |
| Uitnodigingen | Aanmaken, accepteren, afwijzen, verlopen, onbekend e-mailadres. |
| Cross-module contracts | Identity/Authorization/Communication worden via contracts gebruikt. |
| Ontkoppeling | Soft-deactivatie, events, actor, reden en communicatieaanvraag. |
| Beheerderontkoppeling | Beheercontext, reden verplicht, event vastgelegd. |
| Gedeelde oefeningen | Friendshipcheck voor Practice; geen directe Practice-mutatie vanuit Relationships. |
| Scheduling | Verlopen uitnodigingen idempotent verwerken. |
| Privacy/anonimisering | Relaties niet langer autoriserend, historie blijft zonder actuele persoonsgegevens. |
| Architecture tests | Geen Web-reference; geen directe Data/Entities access vanuit andere modules. |
12.23 Implementatiechecklist
Bij implementatie of wijziging van relatiebeheer moet minimaal worden gecontroleerd:
- Staat nieuwe publieke functionaliteit onder
Contracts? - Zijn entities, DbContext en implementaties
internalwaar mogelijk? - Is de relatie met
Identityeen soft link en geen onbedoelde cross-module FK? - Is de transaction boundary per workflow expliciet bepaald?
- Is systeemcommunicatie kritisch of retrybaar voor deze workflow?
- Zijn conflictregels server-side afgedwongen?
- Is er een relationship-event voor auditbare lifecycle-mutaties?
- Is correlation-id doorgegeven naar Communication, Scheduling en technische logging?
- Zijn retries begrensd en beheerbaar wanneer ze worden gebruikt?
- Worden privacygevoelige gegevens niet onnodig gelogd?
- Zijn tests toegevoegd of aangepast voor relatie-, workflow- en architecture-regels?
12.24 Implementatieverificaties
| Punt | Toelichting |
|---|---|
| Exacte relationship-indexen | Unique indexen voor actieve relaties en pending uitnodigingen moeten worden afgestemd op database-informatie. |
| Cross-module transaction support | Controleer dat relatie-uitnodigingen de V1.0-boundary volgen: kritieke relatiebrondata atomair, afgeleide signalering retrybaar, en systeemberichten alleen kritiek wanneer zij de primaire functionele ingang zijn. |
| AdminAdmin-materialisatie | Beslissen of AdminAdmin-relaties fysiek worden gematerialiseerd of afgeleid worden uit actieve beheerderrollen met optionele readmodelweergave. |
| Uitnodiging naar onbekend e-mailadres | Exacte normalisatie, bewaartermijn en provisioning-koppeling technisch valideren. |
| Systeemcommunicatie bij gedeelde oefeningen | Bevestigen dat het ontvangen gedeelde-oefeningenoverzicht de primaire bron blijft, zodat het systeembericht retrybaar mag zijn. |
| Anonimisering relationship-events | Vastleggen welke snapshots blijven, welke worden vervangen en welke technische identifiers behouden blijven. |
12.x Batch 3B.1 - claimen, onboarding en dynamisch herinnerkanaal
Batch 3B.1 vult de technische kloof tussen externe uitnodigingsmail en expliciete relatieacceptatie. De mail-link is niet de dragende technische claimbron; claimen gebeurt server-side op basis van het geverifieerde e-mailadres dat na identity-providerprovisioning beschikbaar is.
Claimen van externe uitnodigingen
De relationship-module levert een contract dat pending externe uitnodigingen voor een genormaliseerd e-mailadres claimt. De account/provisioninglaag roept dit contract pas aan wanneer het e-mailadres door de identity provider als geverifieerd bekend is.
Technische regels:
- claim alle pending externe uitnodigingen voor het geverifieerde e-mailadres;
- vul de interne ontvangercontext, bijvoorbeeld
ToUserId, of leg een gelijkwaardige claimkoppeling vast; - registreer
InvitationClaimedinRelationshipEvents; - voer geen auto-accept uit;
- claim idempotent, zonder dubbele events of systeemberichten;
- claim nooit verlopen, ingetrokken, afgewezen, geaccepteerde of reeds aan een andere gebruiker gekoppelde uitnodigingen.
Centrale onboarding-gate
De weblaag gebruikt een centrale onboarding-gate in plaats van losse page-level checks. De gate bepaalt de eerstvolgende verplichte stap:
- rolkeuze wanneer de gebruiker nog geen rol heeft;
- relatie-uitnodigingen-onboarding wanneer compatibele geclaimde uitnodigingen nog beoordeeld moeten worden;
- afronden en
identity.Users.OnboardingCompletedAtUtcvullen wanneer alle verplichte stappen klaar zijn.
De gate moet een route-allowlist gebruiken voor onboardingroutes om redirect-loops te voorkomen en moet static/framework/assets expliciet doorlaten, zodat CSS, JavaScript, importmaps en Blazor-resources niet als HTML-onboardingpagina worden teruggegeven. Profielmenu, berichtenicoon en normale appnavigatie blijven verborgen zolang onboarding verplicht is. De shell mag wel centraal een losse Uitloggen-actie tonen als veilige escape.
Rolcompatibiliteit
Rolcompatibiliteit wordt server-side bepaald. Als een gebruiker een rol kiest die niet past bij een geclaimde uitnodiging, sluit de relationship-module die uitnodiging af met een expliciet event/reason, bijvoorbeeld InvitationDeclinedBecauseRoleIncompatible. Zo blijven uitnodigingen niet onzichtbaar pending.
Dynamisch herinnerkanaal
De reminder-commandhandler bepaalt bij uitvoering het actuele kanaal:
ToUserIdbekend en invitation pending: intern systeembericht;- geen
ToUserIden invitation pending: nieuwe mailattempt metPurpose = Reminder; - niet pending of binnen cooldown: weigeren met veilige feedback.
Deze kanaalkeuze wordt niet door de oorspronkelijke uitnodigingsvorm bepaald.