Skip to main content

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

OnderdeelEigenaarToelichting
Relatietypen, actieve relaties, relatie-uitnodigingen en relatie-eventsOefenHub.RelationshipsBron van waarheid voor relatievorming en relatie-lifecycle.
Gebruikers, rollen en accountstatusOefenHub.Identity en OefenHub.AuthorizationRelationships verwijst naar gebruikers via soft links en valideert via publieke contracts.
Systeemberichten en privéthreadsOefenHub.CommunicationRelationships vraagt communicatie aan via contracts; schrijft niet rechtstreeks in communication.
Gedeelde oefeningen en runsOefenHub.PracticePractice maakt gedeelde oefeningrecords en ontvangersruns aan.
Oefening-, categorie- en niveausnapshotsOefenHub.Practice op basis van OefenHub.CatalogRelationships bewaart geen oefeninhoud.
Relatie-uitnodigingen koppelen na provisioningOefenHub.Identity als workfloweigenaar, via Relationships en Communication contractsIdentity is eigenaar van accountprovisioning.
Verlopen uitnodigingenOefenHub.Scheduling triggert, Relationships voert domeinactie uitScheduling beheert job-lifecycle; Relationships blijft domeineigenaar.
Geforceerde beheerderontkoppelingOefenHub.Relationships, aangeroepen vanuit OefenHub.AdminDe 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 onderdeelKeuze
ProjectOefenHub.Relationships
DbContextRelationshipsDbContext
Schemarelationships
Schema-naamgevingkleine letters
Tabel- en kolomnaamgevingPascalCase
Entity-zichtbaarheidstandaard internal
Publieke toeganguitsluitend via Contracts
Cross-module user-verwijzingensoft link, eventueel met snapshot
Cross-module communicatievia 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.

RelatietypeTechnische codeInitiatieKernregel
Vriendschap tussen leerlingenFriendshipuitnodiging en acceptatieAlleen tussen leerlingcontexten; nodig voor gedeelde oefeningen.
Ouder-/voogdrelatieGuardianStudentuitnodiging en acceptatieVereist voor ouder-/voogdinzage en live meekijken.
Docent-leerlingrelatieTeacherStudentdocent initieertVoorwaarde voor niveauautorisaties vanuit docentcontext.
Docent-docentrelatieTeacherTeacheruitnodiging en acceptatieVoorwaarde voor collaborators en samenwerking op niveau-laag.
Beheerder-beheerderrelatieAdminAdminsysteemgestuurdWordt 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.

VerwijzingTechnische vormReden
FromUserId / ToUserId naar identity-usersoft linkIdentity is eigenaar van users; relationships blijft historisch reconstrueerbaar.
Rolcontext bij uitnodiging of relatiesoft link + snapshotcode/naam waar nodigRolnamen kunnen wijzigen; historie moet leesbaar blijven.
E-mailadres bij uitnodiging naar onbekend accountwaarde/snapshot in invitationEr bestaat nog geen intern user-id.
Actor bij relatie-eventsoft link + actor snapshot waar nodigAudit moet bruikbaar blijven na anonimisering.
Ontstane relatie vanuit uitnodigingharde FK binnen relationships mogelijkBeide 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

StatusBetekenis
PendingUitnodiging is openstaand en nog accepteerbaar.
AcceptedUitnodiging is geaccepteerd en heeft geleid tot een actieve relatie.
RejectedUitnodiging is afgewezen en niet langer accepteerbaar.
ExpiredUitnodiging 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.

ConflictAfhandeling
Zelfde relatietype en rolcontext bestaat al actiefNieuwe uitnodiging blokkeren.
Zelfde relatietype en rolcontext heeft al pending uitnodigingNieuwe uitnodiging blokkeren of bestaande pending toestand tonen.
Ontvanger heeft vereiste doelrol nietBestaand account blokkeren met veilige melding; onbekend e-mailadres alleen na expliciete externe-mailbevestiging als pending op e-mail behandelen.
Leerling probeert docentrelatie te initiërenBlokkeren.
Friendship buiten leerling-leerlingcontextBlokkeren.
AdminAdmin handmatig uitnodigenBlokkeren; systeemgestuurde relatie.
Relatie is eerder gedeactiveerdNieuwe 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.

EigenschapTechnische regel
RelatierecordBlijft historisch bestaan.
Actieve toestandIsActive = true/false.
DeactiveringActor, rolcontext, tijdstip en reden vastleggen waar vereist.
HistorieRelationshipEvent vastleggen.
Afhankelijke toegangAndere modules controleren actuele relatie via contracts.
Oude resultaten/runsNiet verwijderen door relatiebeëindiging.

12.12 Ontkoppelen en geforceerde beheerderontkoppeling

Ontkoppelen wordt per relatietype verschillend geautoriseerd, maar technisch via dezelfde relationship-lifecycle verwerkt.

OntkoppelingTechnische afhandeling
VriendschapBeide partijen mogen beëindigen; relatie soft-deactiveren; andere partij informeren.
Ouder/voogd door ouder/voogdDirecte soft-deactivatie; kind informeren.
Ouder/voogd door leerlingGeen directe deactivatie indien functioneel bevestiging vereist is; RelationshipDisconnectRequested vastleggen of actie blokkeren volgens Functioneel Ontwerp en Software Requirements Specification.
Docent-leerling door docentSoft-deactivatie; gekoppelde niveauautorisaties intrekken via daarvoor bestemde eigenaarflow.
Docent-leerling door leerlingGeen directe verbreking; RelationshipDisconnectRequested vastleggen of actie blokkeren volgens Functioneel Ontwerp en Software Requirements Specification.
Docent-docentSoft-deactivatie; actieve collaborator-koppelingen moeten door catalog/authorisatie-eigenaar worden gedeactiveerd via contracts.
AdminAdminNiet handmatig verbreekbaar zolang beide gebruikers actief beheerder zijn.
Geforceerd door beheerderAlleen 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.

WorkflowKritieke stappenMogelijk retrybaar
Uitnodiging naar bestaand accountInvitation aanmaken; conflictcontrole; RelationshipEvent; systeembericht als primaire ingangInformatieve vervolgmelding, badge-update
Uitnodiging naar onbekend e-mailadresInvitation aanmaken; e-maildoel en expiry vastleggenSysteembericht pas na provisioning
AcceptatieInvitation status; UserRelationship; relationship-event; conflictcontroleInformatieve acceptatiemelding
AfwijzingInvitation status; relationship-eventInformatieve afwijzingsmelding
OntkoppelenIsActive = false; deactivatiegegevens; relationship-eventInformatieve systeemcommunicatie indien geen primaire ingang
Geforceerde beheerderontkoppelingDeactivatie; reden; actor; role context; relationship-eventInformatieve vervolgcommunicatie indien herstelbaar
Gedeelde oefeningSharedExercise in Practice; friendshipcheck; run/snapshotvalidatieSysteembericht wanneer overzicht de primaire bron blijft
Verlopen uitnodigingenStatus naar Expired; event; idempotentieGeen 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:

JobTechnische eigenaar lifecycleDomeineigenaar uitvoeringDoel
Relationships.ExpireInvitationsOefenHub.SchedulingOefenHub.RelationshipsPending uitnodigingen laten verlopen.
Relationships.SyncAdminRelationshipsOefenHub.SchedulingOefenHub.Relationships eventueel met Identity/Authorization contractsAdminAdmin-relaties actueel houden.
Relationships.RetryCommunicationRequestOefenHub.Scheduling of Communication afhankelijk van eigenaarschapCommunication, aangeroepen vanuit eerder geregistreerde workflowMislukte 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:

SoortPlaatsDoel
Domeinhistorierelationships.RelationshipEventsFunctionele reconstructie van uitnodiging, acceptatie, afwijzing, ontkoppeling en beheeracties.
Technische loggingcentrale loggingprovider via Web/Scheduling/InfrastructureDiagnose 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.

SituatieTechnische afhandeling
Account wordt gedeactiveerdNieuwe reguliere relatie-acties blokkeren; historie behouden.
Account wordt geanonimiseerdActieve relaties van/naar account soft-deactiveren of niet langer autoriserend maken volgens domeinregels.
Open uitnodigingen van/naar accountNiet langer accepteerbaar maken waar nodig.
Historische eventsBehouden, maar zichtbare persoonsgegevens vervangen of beperken volgens anonimiseringbeleid.
Privéthreads of systeemberichtenWorden door Communication afgehandeld, niet door Relationships.
Oefenruns en gedeelde oefeningenBlijven 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:

TestgebiedVoorbeelden
Relatietypen en rolcontextFriendship alleen leerling-leerling; AdminAdmin niet handmatig.
ConflictpreventieGeen dubbele actieve relatie met zelfde type/rolcontext; pending duplicaten blokkeren.
UitnodigingenAanmaken, accepteren, afwijzen, verlopen, onbekend e-mailadres.
Cross-module contractsIdentity/Authorization/Communication worden via contracts gebruikt.
OntkoppelingSoft-deactivatie, events, actor, reden en communicatieaanvraag.
BeheerderontkoppelingBeheercontext, reden verplicht, event vastgelegd.
Gedeelde oefeningenFriendshipcheck voor Practice; geen directe Practice-mutatie vanuit Relationships.
SchedulingVerlopen uitnodigingen idempotent verwerken.
Privacy/anonimiseringRelaties niet langer autoriserend, historie blijft zonder actuele persoonsgegevens.
Architecture testsGeen 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 internal waar mogelijk?
  • Is de relatie met Identity een 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

PuntToelichting
Exacte relationship-indexenUnique indexen voor actieve relaties en pending uitnodigingen moeten worden afgestemd op database-informatie.
Cross-module transaction supportControleer 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-materialisatieBeslissen of AdminAdmin-relaties fysiek worden gematerialiseerd of afgeleid worden uit actieve beheerderrollen met optionele readmodelweergave.
Uitnodiging naar onbekend e-mailadresExacte normalisatie, bewaartermijn en provisioning-koppeling technisch valideren.
Systeemcommunicatie bij gedeelde oefeningenBevestigen dat het ontvangen gedeelde-oefeningenoverzicht de primaire bron blijft, zodat het systeembericht retrybaar mag zijn.
Anonimisering relationship-eventsVastleggen 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 InvitationClaimed in RelationshipEvents;
  • 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:

  1. rolkeuze wanneer de gebruiker nog geen rol heeft;
  2. relatie-uitnodigingen-onboarding wanneer compatibele geclaimde uitnodigingen nog beoordeeld moeten worden;
  3. afronden en identity.Users.OnboardingCompletedAtUtc vullen 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:

  • ToUserId bekend en invitation pending: intern systeembericht;
  • geen ToUserId en invitation pending: nieuwe mailattempt met Purpose = Reminder;
  • niet pending of binnen cooldown: weigeren met veilige feedback.

Deze kanaalkeuze wordt niet door de oorspronkelijke uitnodigingsvorm bepaald.