Activiteiten-herinneringen

Dit document legt uit hoe het systeem bepaalt wanneer en aan wie herinneringsmails worden verstuurd voor activiteiten met een online kamer, en welke ontwerpkeuzes daarachter zitten. Voor de concrete configuratie van kamer-tijden, zie de tenant-instelling wherebyRoomSettings.

Probleem

Een activiteit met een online kamer (Whereby) heeft een natuurlijk moment waarop deelnemers een herinnering nodig hebben: het moment dat de kamer opengaat. Dat is niet de starttijd van de activiteit, maar de kamertijd — standaard 30 minuten eerder.

Het oude systeem werkte met een tijdsvenster van één tot twee uur vóór de starttijd. Dat leverde twee problemen op:

  • Ontkoppeling van de kamertijd: de herinnering ging niet op het logische moment (kamer open), maar op een vast, willekeurig venster.
  • Geen trackbaar bewijs: het systeem had geen geheugen van wat al verstuurd was. Idempotency keys in de event-engine voorkwamen duplicaten technisch, maar je kon op de activiteit zelf niet zien of een herinnering was verstuurd.

Concept

Het nieuwe systeem koppelt de herinnering direct aan de kamertijd. De processor draait elke vijf minuten en vraagt: “welke activiteiten zijn de afgelopen vijf minuten opengegaan en hebben nog geen herinnering ontvangen?”

kamertijd = activity.start − openTime

Het tijdsvenster van de query is dan:

start ≥ nu + openTime − 5min
start ≤ nu + openTime

Dat venster loopt in de pas met de cron-frequentie: elke activiteit valt in precies één run.

Tracking via roomReminderSent

De staat wordt bijgehouden op de activiteit zelf via een boolean veld roomReminderSent. Dat veld is standaard afwezig (gelijkwaardig aan false) en wordt op true gezet nadat alle herinneringen voor die activiteit succesvol zijn verstuurd.

Waarom op de activiteit en niet uitsluitend via idempotency keys?

  • Zichtbaarheid: je kunt in de database direct zien of een activiteit al verwerkt is.
  • Querybare toestand: de query filtert op roomReminderSent: { $ne: true }, wat efficiënter en leesbaarder is dan een lookup in de event-engine.
  • Herstart-veilig: als de processor crasht na het versturen maar vóór het markeren, wordt de activiteit in de volgende run opnieuw opgepakt. De idempotency keys in de event-aanmaak vormen dan het vangnet dat dubbele mails voorkomt.

De twee lagen vullen elkaar dus aan:

Laag Mechanisme Wanneer actief
Primair roomReminderSent op de activiteit Normale flow
Vangnet Idempotency key in de event-engine Bij gedeeltelijke mislukking

Verwerking per activiteit

De processor verwerkt activiteiten één voor één. Na elke geslaagde verwerking wordt de activiteit direct gemarkeerd. Mislukt een activiteit, dan gaat de processor door met de volgende — de fout wordt gelogd op de BullMQ-job.

Deze keuze voorkomt dat een probleem bij één activiteit (bijvoorbeeld een ontbrekende gebruiker of een Whereby-fout) de rest van de batch blokkeert.

Herinneringen worden verstuurd naar drie groepen:

  • Interne gebruikers (activity.users) — leden die direct op de activiteit staan
  • Externe gebruikers (activity.externalUsers) — deelnemers van andere tenants, gefilterd op de huidige tenant-UUID zodat iedere tenant alleen zijn eigen externe deelnemers bereikt
  • Hosts (activity.hosts) — de begeleiders van de activiteit, met een eigen mail-template

Gedeelde communityManager

De community manager — de systeemgebruiker die als afzender van de event-aanmaak fungeert — wordt éénmalig opgehaald vóór de loop en doorgegeven aan elke verzendmethode. Zo wordt voor een batch van tien activiteiten één database-aanroep gedaan in plaats van dertig (drie per activiteit × tien activiteiten).

Handmatige herinneringen

Naast de automatische cron bestaat er een API-endpoint voor handmatige verzending:

POST /activities/:id/remind-participant
Body: { userId: string, type: "user" | "external" }

Handmatige herinneringen slaan roomReminderSent bewust over: ze zijn bedoeld als extra nudge en mogen ook verstuurd worden nadat de automatische herinnering al is gegaan. Om botsing met de automatische flow te voorkomen bevatten hun idempotency keys een timestamp, waardoor ze altijd uniek zijn.