ADR 0006: Partnership verlengen en verloopnotificatie

  • Status: geaccepteerd
  • Datum: 2026-08-07
  • Beslissers: Thomas van Minnen
  • Branch: feat/partnership-verloop-notificatie-verlengen

Context

Een partnership koppelt twee organisaties zodat activiteiten uit de agenda van de organiserende partij zichtbaar worden bij de ontvangende. Het heeft een vaste periode, maar er was geen manier om die te verlengen en niemand werd gewaarschuwd als hij afliep. Zodra de einddatum passeerde stopte het delen stil: geen mail, geen statuswissel, geen zichtbaar spoor in de backoffice. In de praktijk merkte een organisatie het pas als deelnemers de activiteiten van de partner kwijt waren.

Twee eigenschappen van het domein maken dit lastiger dan het klinkt. Ten eerste staan partnerships in de admin-database en worden ze door twee tenants gedeeld, terwijl eventService.create() een event hangt aan de tenant die op dat moment in de context staat. Ten tweede zijn de twee organisaties niet gelijk: de organisator stelt de periode vast, de ontvanger tekent ervoor. Elke notificatie moet dus weten wie er aan zet is en in wiens omgeving hij hoort.

Beslissing

Verlengen op hetzelfde document

Een verlenging is een pendingRenewal-veld naast de lopende periode, niet een nieuw partnership. Zolang de huidige einddatum niet gepasseerd is blijft het partnership gewoon werken terwijl de tegenpartij nog moet tekenen. pendingRenewal.acceptedAt markeert dat beide organisaties akkoord zijn.

Gaat de nieuwe periode in, dan verhuist de oude naar history[], inclusief de volledige accepted-entries. Zo is achteraf te herleiden welke datums er golden en wie namens welke organisatie wanneer akkoord ging op de DPIA en de verwerkersovereenkomst (AVG). De acceptatie-entry heeft één schemadefinitie, gedeeld door de lopende periode, de aanvraag en het archief, zodat die drie niet uit elkaar kunnen lopen.

De rolverdeling volgt het bestaande configureer-model: de organisator stelt de periode vast (PUT /tenant-partnerships/{id}/renew), de ontvanger tekent er alleen voor en kan de datums niet wijzigen. Verlengen kan vanuit active én expired; een verlopen partnership blijft daarom in het overzicht staan, want dat is precies wat je wilt verlengen.

Delen wacht niet op de nachtelijke job

buildActivePeriodFilter is de enige plek die bepaalt of er nu gedeeld mag worden, en heeft twee takken: de lopende periode omvat vandaag, of er ligt een door beiden getekende verlenging waarvan de periode vandaag omvat. Die tweede tak haalt de afhankelijkheid van de job weg. Zonder hem zat er tussen het aflopen van de oude periode en de eerstvolgende job-run een gat waarin niet gedeeld werd terwijl niemand daarom had gevraagd.

Het omklappen zelf blijft aan de dagelijkse job (05:00, tenant-aware, cron: 0 5 * * *) en is daarmee pure boekhouding: de datums in de UI kloppen daarna weer, maar het delen wachtte er niet op. Een gat dat de organisatie zélf kiest, doordat de nieuwe startdatum later ligt dan de oude einddatum, blijft wél een gat. Daar waarschuwt de verlengdialog voor en het overzicht toont het.

Annuleren is een harde grens

Annuleren zet de status op canceled en verwijdert pendingRenewal in dezelfde schrijfactie. Daarnaast weigeren isRenewalDue, de filter van promoteRenewal en findWithPendingRenewalForTenant allemaal een geannuleerd partnership. Die gordel-en-bretels is bewust: zonder die grenzen kon een opgezegde samenwerking met een getekende verlenging later alsnog ingaan en weer activiteiten delen tussen organisaties die uit elkaar zijn, terwijl de rij uit het overzicht verdwenen was.

Notificaties in de omgeving van de ontvanger

Twee event-types, met in code vastgelegde uuid’s (tenantPartnershipEventTypes.ts). Ze worden door eventService.create() bij eerste gebruik zelf aangemaakt; een beheerder koppelt er daarna in de admin-UI een template en ontvangers aan.

Event-type Wanneer Waar
Partnership verloopt binnenkort 30, 14 en 7 dagen voor de einddatum Per tenant apart, via de tenant-aware processor
Partnership wacht op akkoord Bij configureren en bij een verlengingsaanvraag door de organisator In de omgeving van de ontvanger, via runInTenantContext

Dat de verloopjob een tenant-aware processor is, is hier geen implementatiedetail maar de kern: doordat BullMQ over alle actieve tenants fan-out, ziet elke deelnemer hetzelfde partnership vanuit de eigen omgeving en gaat de mail eruit met de eigen huisstijl, afzender en beheerders. Eén partnership levert zo twee onafhankelijke notificaties op, precies één per organisatie. De melding over een wachtend akkoord kent maar één geadresseerde en gebruikt daarom runInTenantContext naar de ontvangende tenant, dezelfde manier waarop externalActivityService een externe inschrijving afhandelt. TenantPartnership is toegevoegd aan de onModelRegistry, met de admin-connectie als bron, zodat het event zijn brondocument kan terugvinden.

Drempels en dubbelingen

De drempels zijn [30, 14, 7]. Gekozen wordt de kleinste drempel die de resterende dagen al gepasseerd zijn en die voor déze tenant nog niet verstuurd is. Daardoor schuift een gemiste dagrun de herinnering door — op 27 dagen gaat alsnog de 30-dagen-herinnering uit — zonder dat er later dubbel gemaild wordt. Twee onafhankelijke vangnetten bewaken dat: een marker per partnership, tenant en drempel (expiryRemindersSent) en een idempotencyKey op de taak.

Zodra beide organisaties een verlenging getekend hebben, houden de herinneringen op. Er valt dan niets meer te doen en een mail die zegt dat de samenwerking verloopt zou simpelweg onwaar zijn. Een aangevraagde maar nog ongetekende verlenging houdt ze juist níét tegen: zonder handtekening verloopt de samenwerking gewoon. In dat geval gaat wel mee wie er aan zet is, als losse gegevens, zodat de template de tekst kan kiezen.

Gevolgen

  • Positief: één partnership blijft één rij over de jaren, met een volledig acceptatie-archief; het delen hangt niet meer aan een geslaagde job-run; een aflopende samenwerking is niet langer stil; de verlopen-status wordt eindelijk echt weggeschreven en het partnership blijft zichtbaar om te verlengen.
  • Negatief: het document draagt nu drie periodes (lopend, aangevraagd, archief) en de meeste queries moeten weten welke ze bedoelen. Wie een filter op startdate/enddate schrijft, moet zich afvragen of pendingRenewal meetelt. Daarom staat de periode-conditie op precies één plek.
  • Risico’s: de melding is best-effort en mag de handeling niet meeslepen — mislukt hij, dan is de verlenging alsnog opgeslagen en valt de ontvanger terug op de verloopherinnering. En zolang een beheerder in de admin-UI geen template en ontvangers aan een event-type koppelt, wordt het event wel vastgelegd maar gaat er geen mail uit; dat is stil te missen.

Alternatieven

  • Een nieuw partnership-document per periode — afgewezen: dan is de historie een verzameling losse rijen die je zelf aan elkaar moet knopen, verdubbelt het overzicht bij elke verlenging en moet elke lezer weten welke rij de actuele is.
  • De verlenging de lopende periode meteen laten overschrijven — afgewezen: dan zou het delen stoppen op het moment van aanvragen in plaats van na de einddatum, terwijl de tegenpartij nog moet tekenen.
  • Het omklappen door de job als enige waarheid voor delen — afgewezen om het gat tussen de einddatum en de eerstvolgende run; zie de tweede tak van buildActivePeriodFilter.
  • De mails vanuit de admin-context versturen — afgewezen: dan is er geen tenant om afzender, huisstijl en ontvangers uit af te leiden, en zou één partnership één mail opleveren in plaats van één per organisatie.
  • Twee losse event-types voor configureren en verlengen — afgewezen: de ontvanger en de gevraagde handeling zijn identiek (rond het partnership af in de backoffice), alleen de aanleiding verschilt. Die gaat als gegeven mee, zodat één configuratie volstaat.
  • pendingRenewal bewaren bij annuleren — overwogen voor de audittrail, bewust niet gedaan: een aanvraag die nooit meer kan ingaan hoort niet als los veld achter te blijven op een document waar niets meer mee gebeurt.

Huidige status (2026-08-07)

Af, op de branch, nog geen PR. Vijf commits: e93c43d6e (job, verlopen-status, verlengen), 3e3983218 (overzicht: verlengingsperiode en het gat), 6dca975c7 (geannuleerd laten liggen), 609f1b22c (zwijgen zodra de verlenging rond is), d40262ee0 (melding over een wachtend akkoord). Groen: 183 tests over de negen partnership- en processorsuites in apps/api, 78 over de agenda-specs in apps/frontend.

Openstaand:

  • De mailteksten van beide event-types moeten in de admin gekoppeld worden, inclusief ontvangers.
  • Onbeslist: of de verloopmail de persoonlijke notificatievoorkeur mag negeren. Dat zou de eerste niet-uitzetbare notificatie van het product zijn; voorstel is een vlag op de notificatie in plaats van hardgecodeerde ontvangers.