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.reminderXSubjectenreminderXBodyoptioneel.
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:
resolveCampaignSendContext(campaign). Returntnullals template ofPublicSurveyLink(status'active') ontbreekt, of alsmongooseConnectionManager.tenant.uuidniet leesbaar is. Bijnull:status = Cancelleden return.buildSnapshot(campaign): query actieve gebruikers met de campagne-tags, persisteer alsSurveyCampaignParticipant. Audience 0 →status = Completed,inviteSentAt = now, return.- Per deelnemer: hergebruik bestaande
surveyCodeof roepissueCodeForRecipientaan en sla op viasetSurveyCode. messageService.createmettemplateUuid,campaign.inviteSubject,campaign.inviteBodyen eenperRecipientContextdie per ontvanger{ data: { surveyUrl } }levert.inviteSentAt = now,status = Running.
processCampaign(campaign, now, actor?) — reminder-fases
Voor elke duePhase (waar date && subject && body && !sentAt && date <= now):
- Eenmaal per tick:
resolveCampaignSendContext. Bijnull: log error, return (campagne blijftRunning). - Per fase: laad non-respondents, splits in
eligibleUserIds(metsurveyCode) enmissingCodeUserIds(zonder code). Loggen waarschuwing voor de tweede groep. messageService.createmet fase-specifiekesubject/bodyenperRecipientContextop basis vancodeByUser.- 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): forceertstatus: Scheduled.update(id, patch): weigert wijzigingen op vergrendelde velden inRunning, weigert alle wijzigingen inCompleted/Cancelled. Whitelist via_.omitvan read-only velden.cancel(id): zetstatus: Cancelled.recordResponse(surveyId, userId): zetrespondedAtop alleSurveyCampaignParticipant-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:
resolveRecipientContext(recipient, message)mergt:subject = ctx?.subject ?? message.subject,body = ctx?.body ?? message.body,data = { ...message.data, ...ctx?.data }.substitutionHelper(..., { surveyUrl: ctx.data.surveyUrl })als 8e arg, alleen als aanwezig.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
surveyUrlop het returned-object zodat `` door Handlebars opgelost wordt. - De bestaande
data: { ..., surveyUrl: undefined }regel blijft staan zodat callers die `` gebruiken (bijv.surveyServicereminder-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/