Survey-campagnes (datamodel en API)

Feiten over het survey-campagne-stelsel: collections, velden, validators, processor-flow, endpoints, bron-bestanden.

Voor begrip: zie de explanation. Voor stappen: zie de how-to.

Datamodel

SurveyCampaign

Collection: surveycampaigns op de tenant-connectie.

Indexen:

  • Unieke index op uuid.
  • Niet-unieke indexen op status, createdBy, survey, inviteSendDate.

Velden (zie apps/api/src/api/survey-campaign/surveyCampaignModel.ts):

Veld Type Default Toelichting
uuid string random UUID Unieke campagne-id voor de UI
name string (verplicht) Interne naam
description string optioneel Interne beschrijving
survey ObjectId (verplicht) Ref naar Survey (tenant-DB)
tags ObjectId[] (verplicht, min 1) Refs naar Tag (tenant-DB), bepaalt doelgroep
template ObjectId (verplicht) Ref naar MessageTemplate (admin-DB), één voor de hele campagne
inviteSendDate date (verplicht) Drempelmoment voor invite-tick
inviteSubject string (verplicht) Onderwerp uitnodiging, ondersteunt en
inviteBody string (verplicht) HTML-body uitnodiging, ondersteunt dezelfde placeholders
reminder1SendDate date optioneel Verzendmoment reminder 1
reminder1Subject string optioneel Verplicht zodra reminder1SendDate gezet is
reminder1Body string optioneel Verplicht zodra reminder1SendDate gezet is
reminder2SendDate / reminder2Subject / reminder2Body idem optioneel Idem voor fase 2
reminder3SendDate / reminder3Subject / reminder3Body idem optioneel Idem voor fase 3
status enum scheduled scheduled / running / completed / cancelled
inviteSentAt date optioneel Door processor gezet bij invite-fase
reminder1SentAt / reminder2SentAt / reminder3SentAt date optioneel Door processor gezet per fase
audienceSnapshotAt date optioneel Tijdstip waarop deelnemerssnapshot is opgeslagen
audienceSize number optioneel Aantal deelnemers in de snapshot
createdBy ObjectId (verplicht) Ref naar User (tenant-DB)
createdAt / updatedAt date auto Mongoose timestamps

Vergrendelde velden in status running (RUNNING_LOCKED_FIELDS in surveyCampaignService.ts):

['survey', 'tags', 'inviteSendDate', 'template', 'inviteSubject', 'inviteBody']

Reminder-content blijft bewerkbaar zolang die fase nog niet is verstuurd.

SurveyCampaignParticipant

Collection: surveycampaignparticipants op de tenant-connectie.

Indexen:

  • Unieke composite index op (campaign, user).
  • Niet-unieke index op (campaign, respondedAt).

Velden (zie apps/api/src/api/survey-campaign-participant/surveyCampaignParticipantModel.ts):

Veld Type Toelichting
campaign ObjectId Ref naar SurveyCampaign
user ObjectId Ref naar User
inviteMessageId / reminderXMessageId ObjectId Optioneel, gereserveerd voor toekomstige per-bericht referenties
respondedAt date Optioneel, gezet zodra de gebruiker een respons indient
surveyCode string Optioneel, unieke 6-char [A-Z0-9] code per (campagne, ontvanger). Door invite-fase gegenereerd, hergebruikt voor reminders
createdAt / updatedAt date Mongoose timestamps

PublicSurveyCode (al bestaand)

Door de campagne-flow nieuw aangemaakt via publicSurveyCodeService.issueCodeForRecipient. Records komen terecht op de admin-connectie in publicSurveyCodes.

Veld Waarde bij campagne-issuance
code 6-char [A-Z0-9] string, retry tot 5x bij collision
publicId Van de PublicSurveyLink van de gekoppelde survey
surveyId String-id van campaign.survey
tenantUuid Van mongooseConnectionManager.tenant.uuid
status 'active'
usesCount 0 (wordt door validateCode opgehoogd bij elke landing)
maxUses null (geen click-limiet, hergebruikbaar over reminders)

Validators

apps/api/src/api/survey-campaign/surveyCampaignSchemaValidators.ts

baseCampaignFields valideert via Zod:

  • template, inviteSubject (min 1), inviteBody (min 1) verplicht.
  • reminderXSubject en reminderXBody optioneel.

applyCampaignRules voegt cross-field-regels toe:

Regel Pad Foutmelding
rejectPastInvite (alleen bij create) inviteSendDate Uitnodigingsdatum mag niet in het verleden liggen
Date zonder subject reminderXSubject reminderXSubject is verplicht zodra reminderXSendDate is gezet
Date zonder body reminderXBody reminderXBody is verplicht zodra reminderXSendDate is gezet
Subject of body zonder date reminderXSendDate reminderXSendDate is verplicht zodra reminderX-content is ingevuld
Niet-oplopende fasedata reminderXSendDate reminderXSendDate moet na de vorige reminder liggen (of na invite voor reminder 1)

CreateSurveyCampaignBodySchema past de regels met rejectPastInvite: true toe. UpdateSurveyCampaignBodySchema is een .partial() zonder die regel.

Service-flow

apps/api/src/api/survey-campaign/surveyCampaignService.ts

processDuePhases(now, actor?)

Loopt over alle SurveyCampaign-documenten met status Scheduled of Running en roept per campagne processCampaign aan binnen een try/catch (één falende campagne breekt de tick niet af).

processCampaign(campaign, now, actor?) — invite-fase

Triggert wanneer !campaign.inviteSentAt && campaign.inviteSendDate <= now:

  1. resolveCampaignSendContext(campaign). Returnt null als template of PublicSurveyLink (status 'active') ontbreekt, of als mongooseConnectionManager.tenant.uuid niet leesbaar is. Bij null: status = Cancelled en return.
  2. buildSnapshot(campaign): query actieve gebruikers met de campagne-tags, persisteer als SurveyCampaignParticipant. Audience 0 → status = Completed, inviteSentAt = now, return.
  3. Per deelnemer: hergebruik bestaande surveyCode of roep issueCodeForRecipient aan en sla op via setSurveyCode.
  4. messageService.create met templateUuid, campaign.inviteSubject, campaign.inviteBody en een perRecipientContext die per ontvanger { data: { surveyUrl } } levert.
  5. inviteSentAt = now, status = Running.

processCampaign(campaign, now, actor?) — reminder-fases

Voor elke duePhase (waar date && subject && body && !sentAt && date <= now):

  1. Eenmaal per tick: resolveCampaignSendContext. Bij null: log error, return (campagne blijft Running).
  2. Per fase: laad non-respondents, splits in eligibleUserIds (met surveyCode) en missingCodeUserIds (zonder code). Loggen waarschuwing voor de tweede groep.
  3. messageService.create met fase-specifieke subject/body en perRecipientContext op basis van codeByUser.
  4. Markeer [reminderXSentAt] = now.

Completion-check

Aan het eind van elke processCampaign-tick: als inviteSentAt gezet is en alle reminders óf geen sendDate óf een sentAt hebben, dan status = Completed.

Andere methods

  • create(data): forceert status: Scheduled.
  • update(id, patch): weigert wijzigingen op vergrendelde velden in Running, weigert alle wijzigingen in Completed/Cancelled. Whitelist via _.omit van read-only velden.
  • cancel(id): zet status: Cancelled.
  • recordResponse(surveyId, userId): zet respondedAt op alle SurveyCampaignParticipant-records voor lopende campagnes waar deze user aan deelneemt.
  • getCampaignAnalytics(id): telt totale deelnemers, respondenten, response-rate, en bouwt een dag-buckets-tijdlijn met fase-markers.

API-endpoints

apps/api/src/api/survey-campaign/surveyCampaignRouter.ts, prefix /survey-campaigns. Auth: authenticationService.isAuthorized.

Method Path Body / Query Response
GET /survey-campaigns FilterAndPaginateOptions (skip, limit, sort, query) { data: SurveyCampaign[], ...pagination }
GET /survey-campaigns/:id - SurveyCampaign
POST /survey-campaigns CreateSurveyCampaignBodySchema Aangemaakt SurveyCampaign (status scheduled)
PATCH /survey-campaigns/:id UpdateSurveyCampaignBodySchema Bijgewerkt SurveyCampaign
POST /survey-campaigns/:id/cancel - SurveyCampaign met status: cancelled
GET /survey-campaigns/:id/analytics - { audienceSize, respondedCount, responseRate, timeline[], markers[] }

OpenAPI-registratie (surveyCampaignRegistry) volgt automatisch het Mongoose-schema, geen losse onderhoudslijst.

Mail-pipeline-uitbreiding

apps/api/src/api/message/messageService.ts heeft één nieuw veld op IMessageCreate:

perRecipientContext?: (recipient: User) => {
  subject?: string;
  body?: string;
  data?: Partial<IMessageCreate['data']> & { surveyUrl?: string };
};

Verwerking in zowel createMessageForRecipient als createBulk:

  1. resolveRecipientContext(recipient, message) mergt: subject = ctx?.subject ?? message.subject, body = ctx?.body ?? message.body, data = { ...message.data, ...ctx?.data }.
  2. substitutionHelper(..., { surveyUrl: ctx.data.surveyUrl }) als 8e arg, alleen als aanwezig.
  3. resolveSubject(rawSubject, substitutions) (private helper) past tweepas dot-notation resolutie toe, gedeeld door de single- en bulk-paden.

apps/api/src/common/utils/substitutionHelper.ts:

  • 8e parameter extra?: { surveyUrl?: string }.
  • Indien aanwezig: top-level surveyUrl op het returned-object zodat `` door Handlebars opgelost wordt.
  • De bestaande data: { ..., surveyUrl: undefined } regel blijft staan zodat callers die `` gebruiken (bijv. surveyService reminder-pad) niet breken.

apps/api/src/api/public-survey/publicSurveyCodeService.ts:

issueCodeForRecipient(params: {
  surveyId: string;
  publicId: string;
  tenantUuid: string;
  maxUses?: number | null; // default null
}): Promise<string>

Genereert via crypto.randomInt over alfabet ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789. Retry tot 5x bij err.code === 11000 (duplicate code). Throwt na 5 mislukkingen met Kon geen unieke survey-code genereren na 5 pogingen.

Frontend

Feature: apps/frontend/src/app/features/backoffice/survey-campaigns/.

Onderdeel Pad Doel
Routes routes/survey-campaigns.routes.ts List / detail / create / edit
Lijst pages/list/survey-campaign-list.component.ts Overzicht met fase-marker en status
Edit pages/edit/survey-campaign-edit.component.ts Reactive Form, applyLocks voor running-vergrendelingen
Detail pages/detail/survey-campaign-detail.component.ts Overzicht, fases, template, per-fase content, analytics
Body-editor components/campaign-body-editor/campaign-body-editor.component.ts Wrapper rond WysiwygEditorComponent, voegt insert-knoppen toe
Types data-access/survey-campaign.types.ts SurveyCampaign, SurveyCampaignTemplateRef, SurveyCampaignCreateInput, SurveyCampaignAnalytics
HTTP-client data-access/survey-campaign.service.ts list, getById, create, update, cancel, analytics

Editor-shortcut-strings staan als component-properties (SURVEY_URL_PLACEHOLDER, RECIPIENT_NAME_PLACEHOLDER) zodat Angular ze niet als template-interpolatie probeert te resolven binnen event-bindings.

Bron-bestanden

  • Service en model: apps/api/src/api/survey-campaign/
  • Participant: apps/api/src/api/survey-campaign-participant/
  • Code-issuance: apps/api/src/api/public-survey/publicSurveyCodeService.ts
  • Mail-pipeline: apps/api/src/api/message/messageService.ts, apps/api/src/common/utils/substitutionHelper.ts
  • Processor: apps/api/src/common/services/bullmq-service/processors/survey-campaign-tick.ts
  • Frontend: apps/frontend/src/app/features/backoffice/survey-campaigns/