placementt-core 1.400.985 → 1.400.987
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/constants.d.ts +0 -22
- package/lib/constants.js +8 -53
- package/lib/constants.js.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/surveys/registry.d.ts +212 -0
- package/lib/surveys/registry.js +443 -0
- package/lib/surveys/registry.js.map +1 -0
- package/lib/surveys/schedule.d.ts +102 -0
- package/lib/surveys/schedule.js +148 -0
- package/lib/surveys/schedule.js.map +1 -0
- package/lib/surveys/schedule.test.d.ts +1 -0
- package/lib/surveys/schedule.test.js +240 -0
- package/lib/surveys/schedule.test.js.map +1 -0
- package/lib/surveys/types.d.ts +204 -0
- package/lib/surveys/types.js +25 -0
- package/lib/surveys/types.js.map +1 -0
- package/lib/typeDefinitions.d.ts +13 -25
- package/package.json +1 -1
- package/src/constants.ts +6 -54
- package/src/index.ts +3 -0
- package/src/surveys/registry.ts +589 -0
- package/src/surveys/schedule.test.ts +281 -0
- package/src/surveys/schedule.ts +168 -0
- package/src/surveys/types.ts +223 -0
- package/src/typeDefinitions.ts +13 -21
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
/**
|
|
3
|
+
* surveys/registry.ts — what makes each survey kind different.
|
|
4
|
+
*
|
|
5
|
+
* The engine (functions/src/surveys.ts) and the web hooks are kind-agnostic: they
|
|
6
|
+
* read behaviour from here. Everything a kind needs that isn't the data shape lives
|
|
7
|
+
* in one SurveyKindDef — auth mode, who can answer, thread position, message types,
|
|
8
|
+
* URL builders and email copy.
|
|
9
|
+
*
|
|
10
|
+
* Copy rule: the student/parent invite and reminder wording of a MIGRATED kind must
|
|
11
|
+
* not change. Every string below marked "(migrated)" is lifted character-for-character
|
|
12
|
+
* from the notifications file it replaces:
|
|
13
|
+
* aspirations ← notifications/aspirations.ts
|
|
14
|
+
* leaver* ← notifications/leavers.ts (incl. CHECKPOINT_COPY)
|
|
15
|
+
* alumniCheckIn ← notifications/alumni.ts
|
|
16
|
+
* skills ← notifications/skillsAssessments.ts
|
|
17
|
+
* Strings marked "(new)" have no pre-migration equivalent — parent tracks on kinds
|
|
18
|
+
* that never had one, and the staff heads-up email.
|
|
19
|
+
*
|
|
20
|
+
* Core stays UI-free: form components and UniversalAnalyticsPage schemas are
|
|
21
|
+
* registered web-side, keyed by the same SurveyKind.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type {SurveyKind, SurveyRaterType} from "./types";
|
|
25
|
+
import {SURVEY_KINDS_LIST} from "./types";
|
|
26
|
+
|
|
27
|
+
/** Where survey links point. The notifications files each hardcoded this. */
|
|
28
|
+
export const SURVEY_SITE_BASE_URL = "https://careerthread.co.uk";
|
|
29
|
+
|
|
30
|
+
/** Prefix a survey path with the site origin. */
|
|
31
|
+
export const absoluteSurveyUrl = (path: string): string => `${SURVEY_SITE_BASE_URL}${path}`;
|
|
32
|
+
|
|
33
|
+
/** Who an individual email is addressed to (distinct from SurveyRaterType, which is
|
|
34
|
+
* about which tracks a cycle asks for). */
|
|
35
|
+
export type SurveyAudience = "student"|"parent"|"employer"|"trustedContact";
|
|
36
|
+
|
|
37
|
+
/** Everything the copy builders can interpolate. */
|
|
38
|
+
export type SurveyCopyContext = {
|
|
39
|
+
/** The institute's branding name, as getImageColorInstituteForEmail returns it. */
|
|
40
|
+
instituteName: string,
|
|
41
|
+
/** The RECIPIENT's forename — who the salutation greets. */
|
|
42
|
+
forename: string,
|
|
43
|
+
/** The subject of the survey, when that isn't the recipient (parent/employer/
|
|
44
|
+
* trusted-contact emails). */
|
|
45
|
+
studentForename?: string,
|
|
46
|
+
studentSurname?: string,
|
|
47
|
+
/** The kind's staff-facing label, for the copy shared across kinds. */
|
|
48
|
+
surveyLabel?: string,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type SurveyEmailCopy = {
|
|
52
|
+
subject: (ctx: SurveyCopyContext) => string,
|
|
53
|
+
preheader: (ctx: SurveyCopyContext) => string,
|
|
54
|
+
title: (ctx: SurveyCopyContext) => string,
|
|
55
|
+
body: (ctx: SurveyCopyContext) => string,
|
|
56
|
+
button: string,
|
|
57
|
+
/** Second CTA — only the alumni check-in has one (its one-click "nothing's
|
|
58
|
+
* changed" reply, which appends the fragment to the same form URL). */
|
|
59
|
+
secondaryButton?: {title: string, urlFragment: string},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Copy variants a kind can define. "invite"/"reminder" are the chain; anything
|
|
63
|
+
* else is a kind-specific one-off (e.g. the alumni fresh-link email). */
|
|
64
|
+
export type SurveyCopyVariant = "invite"|"reminder";
|
|
65
|
+
|
|
66
|
+
type CopySet = Partial<Record<SurveyAudience, SurveyEmailCopy>>;
|
|
67
|
+
|
|
68
|
+
/** Args for the URL builders. Kinds use the subset they need. */
|
|
69
|
+
export type SurveyPathArgs = {
|
|
70
|
+
kind: SurveyKind,
|
|
71
|
+
requestId: string,
|
|
72
|
+
/** Magic-link token, from SurveyRequest.token / parentToken. */
|
|
73
|
+
token?: string,
|
|
74
|
+
role?: "student"|"parent",
|
|
75
|
+
oId?: string,
|
|
76
|
+
alumniId?: string,
|
|
77
|
+
/** Legacy skills links: the skillAssessments doc id and its externalKey. */
|
|
78
|
+
assessmentId?: string,
|
|
79
|
+
externalKey?: string,
|
|
80
|
+
rater?: SurveyRaterType,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Which field on a request/cycle the legacy emailInteractions docs were keyed by.
|
|
84
|
+
* Needed by the migration-window dual-read: old interaction docs carry a different
|
|
85
|
+
* collectionLink + collectionItemId to the ones the engine writes. */
|
|
86
|
+
export type SurveyLegacyItemIdSource = "requestId"|"threadKey"|"uid"|"alumniId";
|
|
87
|
+
|
|
88
|
+
export type SurveyLegacyRef = {
|
|
89
|
+
/** emailInteractions.collectionLink used before the cutover. */
|
|
90
|
+
collectionLink: string,
|
|
91
|
+
itemIdSource: SurveyLegacyItemIdSource,
|
|
92
|
+
messageTypes: {invite?: string, reminder?: string, [variant: string]: string|undefined},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type SurveyKindDef = {
|
|
96
|
+
kind: SurveyKind,
|
|
97
|
+
/** Staff-facing name, singular. */
|
|
98
|
+
label: string,
|
|
99
|
+
/** Card shown on the student dashboard for an outstanding request. Absent for
|
|
100
|
+
* kinds with no in-app student surface (alumni check-ins are magic-link only). */
|
|
101
|
+
studentCard?: {title: string, description: string},
|
|
102
|
+
/** "account" — the person logs in with their CareerThread account and the form
|
|
103
|
+
* reads their uid. "magicLink" — no account (or a dead school email), so the
|
|
104
|
+
* request carries a token. */
|
|
105
|
+
authMode: "account"|"magicLink",
|
|
106
|
+
/** Which rater tracks this kind supports. `students` means the subject in
|
|
107
|
+
* person — for alumniCheckIn that is the alumnus, not a pupil. */
|
|
108
|
+
raters: Record<SurveyRaterType, boolean>,
|
|
109
|
+
/** How a parent/guardian participates. "rater" — a parallel response track on
|
|
110
|
+
* the request, offered when the slot's `parents` toggle is on. "fallback" — only
|
|
111
|
+
* contacted after the subject ignores their whole chain (alumni trusted contact).
|
|
112
|
+
* "none" — never contacted. */
|
|
113
|
+
parentMode: "rater"|"fallback"|"none",
|
|
114
|
+
/** Staff can submit on the person's behalf. */
|
|
115
|
+
staffCanCompleteOnBehalf: boolean,
|
|
116
|
+
/** Which thread this kind belongs to, if any. Cycles in a thread share a
|
|
117
|
+
* threadKey and supersede each other in threadOrder. */
|
|
118
|
+
thread?: string,
|
|
119
|
+
threadOrder?: number,
|
|
120
|
+
/** Where responses are stored. Skills keeps writing skillAssessments docs
|
|
121
|
+
* (plan decision D4) so its aggregates and analytics stay untouched; every other
|
|
122
|
+
* kind stores the payload on the request. */
|
|
123
|
+
responseStore: "surveyRequests"|"skillAssessments",
|
|
124
|
+
/** Days after send that the automatic reminder fires. Absent = no cron reminder:
|
|
125
|
+
* skills has never had one, and alumni check-ins chase via the emailInteractions
|
|
126
|
+
* reminder chain rather than a per-cycle reminder. */
|
|
127
|
+
reminderAfterDays?: number,
|
|
128
|
+
/** Default window before the form link expires. Only aspirations close today. */
|
|
129
|
+
defaultCloseAfterDays?: number,
|
|
130
|
+
/** emailInteractions message types the engine writes. */
|
|
131
|
+
messageTypes: {invite: string, reminder: string, parentInvite: string, parentReminder: string},
|
|
132
|
+
/** Pre-cutover interaction identity, for the dual-read supersession shim.
|
|
133
|
+
* Delete with the shim in Phase E. */
|
|
134
|
+
legacy: SurveyLegacyRef,
|
|
135
|
+
/** The engine's form URL. */
|
|
136
|
+
formPath: (args: SurveyPathArgs) => string,
|
|
137
|
+
/** The URL live emails already point at. Kept working as a redirect forever. */
|
|
138
|
+
legacyFormPath?: (args: SurveyPathArgs) => string,
|
|
139
|
+
copy: {invite: CopySet, reminder: CopySet, extra?: {[variant: string]: SurveyEmailCopy}},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ─── Shared copy ──────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const studentFullName = (ctx: SurveyCopyContext) =>
|
|
145
|
+
`${ctx.studentForename ?? ""} ${ctx.studentSurname ?? ""}`.trim();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* (new) The parent track's copy, shared by every kind that doesn't override it —
|
|
149
|
+
* one template family across kinds, parameterised by the kind's label (D7). Skills
|
|
150
|
+
* overrides it, since its parent wording predates the engine and must not change.
|
|
151
|
+
*/
|
|
152
|
+
const GENERIC_PARENT_COPY: {invite: SurveyEmailCopy, reminder: SurveyEmailCopy} = {
|
|
153
|
+
invite: {
|
|
154
|
+
subject: (ctx) => `${ctx.instituteName} — ${ctx.surveyLabel ?? "a short survey"} for ${ctx.studentForename}`,
|
|
155
|
+
preheader: () => "It takes a few minutes and helps your child's school plan their next steps.",
|
|
156
|
+
title: () => "Your view, in a few minutes",
|
|
157
|
+
body: (ctx) => `${ctx.instituteName} is asking parents and carers to complete a short form about ${ctx.studentForename}.\n\nThere are no right or wrong answers — your view sits alongside ${ctx.studentForename}'s own, and helps the school support them. Tap the button below to start; no login needed.`,
|
|
158
|
+
button: "Complete the form",
|
|
159
|
+
},
|
|
160
|
+
reminder: {
|
|
161
|
+
subject: (ctx) => `Reminder — ${ctx.surveyLabel ?? "a short survey"} for ${ctx.studentForename}`,
|
|
162
|
+
preheader: () => "The form closes soon — it only takes a few minutes.",
|
|
163
|
+
title: () => "Just a reminder",
|
|
164
|
+
body: (ctx) => `We noticed the short form ${ctx.instituteName} sent about ${ctx.studentForename} hasn't been completed yet.\n\nIt takes a few minutes and there's no login needed — just tap the button below.`,
|
|
165
|
+
button: "Complete the form",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** Context for the staff heads-up email, which is about a cycle rather than a person. */
|
|
170
|
+
export type SurveyHeadsUpContext = {
|
|
171
|
+
instituteName: string,
|
|
172
|
+
/** Staff member's forename. */
|
|
173
|
+
forename: string,
|
|
174
|
+
/** The cycle's title, or the kind label + slot label. */
|
|
175
|
+
surveyLabel: string,
|
|
176
|
+
/** Human date of the scheduled send, e.g. "Monday 14 September". */
|
|
177
|
+
sendDateLabel: string,
|
|
178
|
+
/** How many people it will go to. */
|
|
179
|
+
recipientCount: number,
|
|
180
|
+
/** 7 or 1. */
|
|
181
|
+
daysUntilSend: number,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* (new) Staff heads-up, sent at T-7 and T-1 before any scheduled send. One template
|
|
186
|
+
* for every kind. The two buttons are "view the cycle" and "turn auto-send off".
|
|
187
|
+
*/
|
|
188
|
+
export const SURVEY_HEADS_UP_COPY = {
|
|
189
|
+
subject: (ctx: SurveyHeadsUpContext) =>
|
|
190
|
+
ctx.daysUntilSend === 1 ?
|
|
191
|
+
`Tomorrow: ${ctx.surveyLabel} sends to ${ctx.recipientCount} ${ctx.recipientCount === 1 ? "person" : "people"}` :
|
|
192
|
+
`In ${ctx.daysUntilSend} days: ${ctx.surveyLabel} is scheduled to send`,
|
|
193
|
+
preheader: (ctx: SurveyHeadsUpContext) => `Nothing to do unless you want to change it — sending ${ctx.sendDateLabel}.`,
|
|
194
|
+
title: () => "A survey is about to go out",
|
|
195
|
+
body: (ctx: SurveyHeadsUpContext) =>
|
|
196
|
+
`${ctx.surveyLabel} is scheduled to send to ${ctx.recipientCount} ${ctx.recipientCount === 1 ? "person" : "people"} on ${ctx.sendDateLabel}.\n\nYou don't need to do anything — it will go out automatically. If the timing or the recipients aren't right, open it below to change them, or turn auto-send off to hold it.`,
|
|
197
|
+
primaryButton: "View the survey",
|
|
198
|
+
secondaryButton: "Turn off auto-send",
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ─── Kind definitions ─────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/** The leaver thread, in send order. */
|
|
204
|
+
export const SURVEY_THREADS: {[thread: string]: SurveyKind[]} = {
|
|
205
|
+
leaver: ["leaverIntentions", "leaverResults", "leaverChristmas"],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const LEAVER_LEGACY_REF = {collectionLink: "leaverGroups", itemIdSource: "threadKey" as SurveyLegacyItemIdSource};
|
|
209
|
+
|
|
210
|
+
export const SURVEY_KINDS: Record<SurveyKind, SurveyKindDef> = {
|
|
211
|
+
skills: {
|
|
212
|
+
kind: "skills",
|
|
213
|
+
label: "Skills survey",
|
|
214
|
+
studentCard: {title: "New Skills Assessment", description: "Click to complete."},
|
|
215
|
+
authMode: "account",
|
|
216
|
+
raters: {students: true, parents: true, employers: true, trustedContact: false},
|
|
217
|
+
parentMode: "rater",
|
|
218
|
+
staffCanCompleteOnBehalf: true,
|
|
219
|
+
responseStore: "skillAssessments",
|
|
220
|
+
messageTypes: {
|
|
221
|
+
invite: "surveySkillsInvite",
|
|
222
|
+
reminder: "surveySkillsReminder",
|
|
223
|
+
parentInvite: "surveySkillsParentInvite",
|
|
224
|
+
parentReminder: "surveySkillsParentReminder",
|
|
225
|
+
},
|
|
226
|
+
legacy: {collectionLink: "users", itemIdSource: "uid", messageTypes: {invite: "skillRequest"}},
|
|
227
|
+
formPath: ({requestId}) => `/institutes/surveys/skills/${requestId}`,
|
|
228
|
+
// Students went in-app; parents and employers used the tokenised public form.
|
|
229
|
+
legacyFormPath: ({rater, oId, assessmentId, externalKey}) => rater === "students" ?
|
|
230
|
+
`/institutes/skills/${assessmentId}` :
|
|
231
|
+
`/skillsRequest/${oId}/${assessmentId}/${externalKey}`,
|
|
232
|
+
copy: {
|
|
233
|
+
invite: {
|
|
234
|
+
// (migrated) subject/title/button are shared across raters; only the
|
|
235
|
+
// preheader and body differ — as in sendSkillsAssessmentRequest.
|
|
236
|
+
student: {
|
|
237
|
+
subject: () => "[Action Required] Complete Skills Assessment",
|
|
238
|
+
preheader: () => "Please complete this self-reflection of your skills development",
|
|
239
|
+
title: () => "Complete Skills Assessment",
|
|
240
|
+
body: () => "Complete this skills questionnaire to assess your transferrable skills and identify areas you could improve. Click the link below to view the questionnaire.",
|
|
241
|
+
button: "Complete Assessment",
|
|
242
|
+
},
|
|
243
|
+
parent: {
|
|
244
|
+
subject: () => "[Action Required] Complete Skills Assessment",
|
|
245
|
+
preheader: (ctx) => `Please complete this skills assessment for ${studentFullName(ctx)}`,
|
|
246
|
+
title: () => "Complete Skills Assessment",
|
|
247
|
+
body: () => "Please support your student's development by completing this short skills assessment that helps us understand how you think your child is progressing. Click the link below to complete the questionnaire.",
|
|
248
|
+
button: "Complete Assessment",
|
|
249
|
+
},
|
|
250
|
+
employer: {
|
|
251
|
+
subject: () => "[Action Required] Complete Skills Assessment",
|
|
252
|
+
preheader: (ctx) => `Please provide feedback for ${studentFullName(ctx)}`,
|
|
253
|
+
title: () => "Complete Skills Assessment",
|
|
254
|
+
body: (ctx) => `Please complete this short questionnaire based on your experience with ${studentFullName(ctx)}. Your feedback will help shape how ${ctx.studentForename} develops their transferrable skills to support their future career. Click the link below to view the skills assessment.`,
|
|
255
|
+
button: "Complete Assessment",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
// Skills has never sent an automatic reminder — see reminderAfterDays.
|
|
259
|
+
reminder: {},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
aspirations: {
|
|
264
|
+
kind: "aspirations",
|
|
265
|
+
label: "Careers aspirations survey",
|
|
266
|
+
studentCard: {title: "Career Aspirations Survey", description: "Your school wants to hear about your career aspirations"},
|
|
267
|
+
authMode: "account",
|
|
268
|
+
raters: {students: true, parents: true, employers: false, trustedContact: false},
|
|
269
|
+
parentMode: "rater",
|
|
270
|
+
staffCanCompleteOnBehalf: true,
|
|
271
|
+
responseStore: "surveyRequests",
|
|
272
|
+
reminderAfterDays: 14,
|
|
273
|
+
defaultCloseAfterDays: 21,
|
|
274
|
+
messageTypes: {
|
|
275
|
+
invite: "surveyAspirationsInvite",
|
|
276
|
+
reminder: "surveyAspirationsReminder",
|
|
277
|
+
parentInvite: "surveyAspirationsParentInvite",
|
|
278
|
+
parentReminder: "surveyAspirationsParentReminder",
|
|
279
|
+
},
|
|
280
|
+
legacy: {
|
|
281
|
+
collectionLink: "aspirationRequests",
|
|
282
|
+
itemIdSource: "requestId",
|
|
283
|
+
messageTypes: {invite: "aspirationInvite", reminder: "aspirationReminder"},
|
|
284
|
+
},
|
|
285
|
+
formPath: ({requestId}) => `/institutes/surveys/aspirations/${requestId}`,
|
|
286
|
+
legacyFormPath: ({requestId}) => `/app/destinations/students/form/${requestId}`,
|
|
287
|
+
copy: {
|
|
288
|
+
invite: {
|
|
289
|
+
student: {
|
|
290
|
+
// (migrated) sendAspirationInviteEmail
|
|
291
|
+
subject: (ctx) => `${ctx.instituteName} — share your career aspirations`,
|
|
292
|
+
preheader: () => "It only takes a few minutes and helps your school plan for your future.",
|
|
293
|
+
title: () => "Tell us about your career aspirations",
|
|
294
|
+
body: (ctx) => `${ctx.instituteName} would like to hear about your career interests and ideas for the future.\n\nClick the button below to log in to CareerThread and complete a short form — it takes about 3 minutes. There are no right or wrong answers.`,
|
|
295
|
+
button: "Share my aspirations",
|
|
296
|
+
},
|
|
297
|
+
parent: GENERIC_PARENT_COPY.invite,
|
|
298
|
+
},
|
|
299
|
+
reminder: {
|
|
300
|
+
student: {
|
|
301
|
+
// (migrated) sendAspirationReminderEmail
|
|
302
|
+
subject: (ctx) => `Reminder — share your career aspirations with ${ctx.instituteName}`,
|
|
303
|
+
preheader: () => "The form closes soon — it only takes a few minutes.",
|
|
304
|
+
title: () => "Just a reminder",
|
|
305
|
+
body: (ctx) => `We noticed you haven't yet completed the careers aspirations form from ${ctx.instituteName}.\n\nLog in to CareerThread using the link below — the form takes about 3 minutes and closes soon.`,
|
|
306
|
+
button: "Complete the form",
|
|
307
|
+
},
|
|
308
|
+
parent: GENERIC_PARENT_COPY.reminder,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
leaverIntentions: {
|
|
314
|
+
kind: "leaverIntentions",
|
|
315
|
+
label: "Leaver transition form",
|
|
316
|
+
studentCard: {title: "Leaver Form", description: "Tell us what you're planning to do next"},
|
|
317
|
+
authMode: "account",
|
|
318
|
+
raters: {students: true, parents: true, employers: false, trustedContact: false},
|
|
319
|
+
parentMode: "rater",
|
|
320
|
+
staffCanCompleteOnBehalf: true,
|
|
321
|
+
thread: "leaver",
|
|
322
|
+
threadOrder: 1,
|
|
323
|
+
responseStore: "surveyRequests",
|
|
324
|
+
reminderAfterDays: 14,
|
|
325
|
+
messageTypes: {
|
|
326
|
+
invite: "surveyLeaverIntentionsInvite",
|
|
327
|
+
reminder: "surveyLeaverIntentionsReminder",
|
|
328
|
+
parentInvite: "surveyLeaverIntentionsParentInvite",
|
|
329
|
+
parentReminder: "surveyLeaverIntentionsParentReminder",
|
|
330
|
+
},
|
|
331
|
+
legacy: {...LEAVER_LEGACY_REF, messageTypes: {invite: "leaverInvite", reminder: "leaverReminder"}},
|
|
332
|
+
formPath: ({requestId}) => `/institutes/surveys/leaverIntentions/${requestId}`,
|
|
333
|
+
legacyFormPath: ({requestId}) => `/institutes/leaver/${requestId}`,
|
|
334
|
+
copy: {
|
|
335
|
+
invite: {
|
|
336
|
+
student: {
|
|
337
|
+
// (migrated) sendLeaverInviteEmail
|
|
338
|
+
subject: (ctx) => `${ctx.instituteName} — tell us your next step`,
|
|
339
|
+
preheader: () => "A few minutes now helps your school report your destination and stay in touch.",
|
|
340
|
+
title: () => "Before you leave — tell us your plans",
|
|
341
|
+
body: (ctx) => `As you get ready to leave ${ctx.instituteName}, please complete a short form telling us what you're planning to do next.\n\nIt takes about 5 minutes and helps your school keep your records up to date. If you'd like to, you can also choose to stay connected as an alumnus.`,
|
|
342
|
+
button: "Complete my leaver form",
|
|
343
|
+
},
|
|
344
|
+
parent: GENERIC_PARENT_COPY.invite,
|
|
345
|
+
},
|
|
346
|
+
reminder: {
|
|
347
|
+
student: {
|
|
348
|
+
// (migrated) sendLeaverReminderEmail
|
|
349
|
+
subject: (ctx) => `Reminder — tell ${ctx.instituteName} your next step`,
|
|
350
|
+
preheader: () => "Complete your leaver form before you lose access to your school account.",
|
|
351
|
+
title: () => "Just a reminder",
|
|
352
|
+
body: (ctx) => `We noticed you haven't yet completed your leaver form from ${ctx.instituteName}.\n\nLog in to CareerThread using the link below — it takes about 5 minutes. Once you leave, your school account is deactivated, so it's worth doing now.`,
|
|
353
|
+
button: "Complete my leaver form",
|
|
354
|
+
},
|
|
355
|
+
parent: GENERIC_PARENT_COPY.reminder,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
leaverResults: {
|
|
361
|
+
kind: "leaverResults",
|
|
362
|
+
label: "Results day check-in",
|
|
363
|
+
studentCard: {title: "Results Day Check-in", description: "Now you have your results, tell us what you're actually doing next"},
|
|
364
|
+
// The school email is deactivated by results day, so these go to the personal
|
|
365
|
+
// address captured on the intentions form and authenticate by token.
|
|
366
|
+
authMode: "magicLink",
|
|
367
|
+
raters: {students: true, parents: true, employers: false, trustedContact: false},
|
|
368
|
+
parentMode: "rater",
|
|
369
|
+
staffCanCompleteOnBehalf: true,
|
|
370
|
+
thread: "leaver",
|
|
371
|
+
threadOrder: 2,
|
|
372
|
+
responseStore: "surveyRequests",
|
|
373
|
+
reminderAfterDays: 7,
|
|
374
|
+
messageTypes: {
|
|
375
|
+
invite: "surveyLeaverResultsInvite",
|
|
376
|
+
reminder: "surveyLeaverResultsReminder",
|
|
377
|
+
parentInvite: "surveyLeaverResultsParentInvite",
|
|
378
|
+
parentReminder: "surveyLeaverResultsParentReminder",
|
|
379
|
+
},
|
|
380
|
+
legacy: {...LEAVER_LEGACY_REF, messageTypes: {invite: "leaverResultsInvite", reminder: "leaverResultsReminder"}},
|
|
381
|
+
formPath: ({requestId, token, role}) =>
|
|
382
|
+
`/survey/leaverResults/${requestId}?token=${token ?? ""}&role=${role ?? "student"}`,
|
|
383
|
+
legacyFormPath: ({requestId, token}) => `/leaverCheckpoint/${requestId}/resultsDay/${token ?? ""}`,
|
|
384
|
+
copy: {
|
|
385
|
+
invite: {
|
|
386
|
+
student: {
|
|
387
|
+
// (migrated) CHECKPOINT_COPY.resultsDay
|
|
388
|
+
subject: (ctx) => `Congratulations from ${ctx.instituteName} — what's next?`,
|
|
389
|
+
preheader: () => "Now you have your results, tell us what you're actually doing. It takes 2 minutes.",
|
|
390
|
+
title: () => "Congratulations on finishing!",
|
|
391
|
+
body: (ctx) => `Now you've got your results, ${ctx.instituteName} would love to know what you're actually doing next.\n\nIt takes about 2 minutes and there's no login needed — just tap the button below. Whatever you've decided, we'd love to hear it.`,
|
|
392
|
+
button: "Tell us what's next",
|
|
393
|
+
},
|
|
394
|
+
parent: GENERIC_PARENT_COPY.invite,
|
|
395
|
+
},
|
|
396
|
+
reminder: {
|
|
397
|
+
student: {
|
|
398
|
+
// (migrated) sendLeaverCheckpointReminderEmail — its subject was built
|
|
399
|
+
// from the invite button, lowercased.
|
|
400
|
+
subject: (ctx) => `Reminder — tell us what's next with ${ctx.instituteName}`,
|
|
401
|
+
preheader: () => "Now you have your results, tell us what you're actually doing. It takes 2 minutes.",
|
|
402
|
+
title: () => "Just a reminder",
|
|
403
|
+
body: (ctx) => `We noticed you haven't yet had a chance to update ${ctx.instituteName} on what you're doing.\n\nIt only takes 2 minutes, and there's no login needed — just tap the button below.`,
|
|
404
|
+
button: "Tell us what's next",
|
|
405
|
+
},
|
|
406
|
+
parent: GENERIC_PARENT_COPY.reminder,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
leaverChristmas: {
|
|
412
|
+
kind: "leaverChristmas",
|
|
413
|
+
label: "Christmas check-in",
|
|
414
|
+
studentCard: {title: "Christmas Check-in", description: "Let your school know how you're getting on since you left"},
|
|
415
|
+
authMode: "magicLink",
|
|
416
|
+
raters: {students: true, parents: true, employers: false, trustedContact: false},
|
|
417
|
+
parentMode: "rater",
|
|
418
|
+
staffCanCompleteOnBehalf: true,
|
|
419
|
+
thread: "leaver",
|
|
420
|
+
threadOrder: 3,
|
|
421
|
+
responseStore: "surveyRequests",
|
|
422
|
+
reminderAfterDays: 7,
|
|
423
|
+
messageTypes: {
|
|
424
|
+
invite: "surveyLeaverChristmasInvite",
|
|
425
|
+
reminder: "surveyLeaverChristmasReminder",
|
|
426
|
+
parentInvite: "surveyLeaverChristmasParentInvite",
|
|
427
|
+
parentReminder: "surveyLeaverChristmasParentReminder",
|
|
428
|
+
},
|
|
429
|
+
legacy: {...LEAVER_LEGACY_REF, messageTypes: {invite: "leaverChristmasInvite", reminder: "leaverChristmasReminder"}},
|
|
430
|
+
formPath: ({requestId, token, role}) =>
|
|
431
|
+
`/survey/leaverChristmas/${requestId}?token=${token ?? ""}&role=${role ?? "student"}`,
|
|
432
|
+
legacyFormPath: ({requestId, token}) => `/leaverCheckpoint/${requestId}/christmas/${token ?? ""}`,
|
|
433
|
+
copy: {
|
|
434
|
+
invite: {
|
|
435
|
+
student: {
|
|
436
|
+
// (migrated) CHECKPOINT_COPY.christmas
|
|
437
|
+
subject: (ctx) => `Quick check-in from ${ctx.instituteName} — how's it going?`,
|
|
438
|
+
preheader: () => "Still 2 minutes — let us know how you're getting on since you left.",
|
|
439
|
+
title: () => "How's it going since you left?",
|
|
440
|
+
body: (ctx) => `It's been a few months since you left ${ctx.instituteName}, so we wanted to check in.\n\nAre you still doing what you planned, or has something changed? Either way it's completely fine — just let us know so we can keep your records up to date. It takes about 2 minutes, no login needed.`,
|
|
441
|
+
button: "Quick check-in",
|
|
442
|
+
},
|
|
443
|
+
parent: GENERIC_PARENT_COPY.invite,
|
|
444
|
+
},
|
|
445
|
+
reminder: {
|
|
446
|
+
student: {
|
|
447
|
+
// (migrated) sendLeaverCheckpointReminderEmail
|
|
448
|
+
subject: (ctx) => `Reminder — quick check-in with ${ctx.instituteName}`,
|
|
449
|
+
preheader: () => "Still 2 minutes — let us know how you're getting on since you left.",
|
|
450
|
+
title: () => "Just a reminder",
|
|
451
|
+
body: (ctx) => `We noticed you haven't yet had a chance to update ${ctx.instituteName} on what you're doing.\n\nIt only takes 2 minutes, and there's no login needed — just tap the button below.`,
|
|
452
|
+
button: "Quick check-in",
|
|
453
|
+
},
|
|
454
|
+
parent: GENERIC_PARENT_COPY.reminder,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
alumniCheckIn: {
|
|
460
|
+
kind: "alumniCheckIn",
|
|
461
|
+
label: "Alumni check-in",
|
|
462
|
+
// No student dashboard card — alumni have no CareerThread account.
|
|
463
|
+
authMode: "magicLink",
|
|
464
|
+
raters: {students: true, parents: false, employers: false, trustedContact: true},
|
|
465
|
+
parentMode: "fallback",
|
|
466
|
+
staffCanCompleteOnBehalf: true,
|
|
467
|
+
responseStore: "surveyRequests",
|
|
468
|
+
// Chased by the emailInteractions reminder chain, which escalates to the
|
|
469
|
+
// trusted contact, rather than by a per-cycle reminder.
|
|
470
|
+
messageTypes: {
|
|
471
|
+
invite: "surveyAlumniCheckInInvite",
|
|
472
|
+
reminder: "surveyAlumniCheckInReminder",
|
|
473
|
+
parentInvite: "surveyAlumniCheckInTrustedContact",
|
|
474
|
+
parentReminder: "surveyAlumniCheckInTrustedContactReminder",
|
|
475
|
+
},
|
|
476
|
+
legacy: {
|
|
477
|
+
collectionLink: "alumni",
|
|
478
|
+
itemIdSource: "alumniId",
|
|
479
|
+
messageTypes: {
|
|
480
|
+
invite: "alumniCheckInInvite",
|
|
481
|
+
trustedContact: "alumniCheckInTrustedContact",
|
|
482
|
+
linkRefresh: "alumniCheckInLinkRefresh",
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
formPath: ({requestId, token, role}) =>
|
|
486
|
+
`/survey/alumniCheckIn/${requestId}?token=${token ?? ""}&role=${role ?? "student"}`,
|
|
487
|
+
legacyFormPath: ({alumniId, token}) => `/alumniCheckIn/${alumniId}/${token ?? ""}`,
|
|
488
|
+
copy: {
|
|
489
|
+
invite: {
|
|
490
|
+
student: {
|
|
491
|
+
// (migrated) sendAlumniCheckInInviteEmail — note the plain hyphens,
|
|
492
|
+
// which this copy uses where the leaver copy uses en dashes.
|
|
493
|
+
subject: (ctx) => `Where are you now? A catch-up from ${ctx.instituteName}`,
|
|
494
|
+
preheader: () => "One click if nothing's changed - two minutes if it has.",
|
|
495
|
+
title: () => "Where has your journey taken you?",
|
|
496
|
+
body: (ctx) => `We love keeping up with where our former students end up. Let us know what you're doing now - it takes about two minutes, and if nothing has changed since we last spoke it's a single click. You can also choose whether to share your journey with our current students, to show them where life after ${ctx.instituteName} can lead.`,
|
|
497
|
+
button: "Update my profile",
|
|
498
|
+
secondaryButton: {title: "I'm in the same place", urlFragment: "#noChange"},
|
|
499
|
+
},
|
|
500
|
+
trustedContact: {
|
|
501
|
+
// (migrated) sendTrustedContactCheckInFallbackEmail. Minimum
|
|
502
|
+
// disclosure: the alumnus's forename only.
|
|
503
|
+
subject: (ctx) => `A quick favour from ${ctx.instituteName}`,
|
|
504
|
+
preheader: (ctx) => `Could you tell us how ${ctx.studentForename} is getting on? It takes a minute.`,
|
|
505
|
+
title: (ctx) => `How is ${ctx.studentForename} getting on?`,
|
|
506
|
+
body: (ctx) => `We're the careers team at ${ctx.instituteName}. We've been trying to reach ${ctx.studentForename} for our annual catch-up but haven't heard back. As their nominated contact, could you let us know what they're doing now? It takes about a minute, and there's no login needed.`,
|
|
507
|
+
button: "Tell us how they're doing",
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
reminder: {},
|
|
511
|
+
extra: {
|
|
512
|
+
// (migrated) sendAlumniCheckInFreshLinkEmail — sent when someone
|
|
513
|
+
// returns to a used or expired link.
|
|
514
|
+
linkRefresh: {
|
|
515
|
+
subject: (ctx) => `Your fresh profile link from ${ctx.instituteName}`,
|
|
516
|
+
preheader: () => "Here's a new secure link to your alumni profile.",
|
|
517
|
+
title: () => "Here's a fresh link",
|
|
518
|
+
body: () => "The link you followed had expired, so we've sent you this new one. Click below to view or update your alumni profile.",
|
|
519
|
+
button: "Open my profile",
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// ─── Lookups ──────────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
export const surveyKindDef = (kind: SurveyKind): SurveyKindDef => SURVEY_KINDS[kind];
|
|
529
|
+
|
|
530
|
+
export const isSurveyKind = (value: unknown): value is SurveyKind =>
|
|
531
|
+
typeof value === "string" && (SURVEY_KINDS_LIST as string[]).includes(value);
|
|
532
|
+
|
|
533
|
+
/** The kinds in a kind's thread, in send order. A kind with no thread is alone. */
|
|
534
|
+
export const surveyThreadKinds = (kind: SurveyKind): SurveyKind[] => {
|
|
535
|
+
const thread = SURVEY_KINDS[kind].thread;
|
|
536
|
+
return thread ? SURVEY_THREADS[thread] : [kind];
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Kinds that come LATER in this kind's thread. Their having been sent is what makes
|
|
541
|
+
* chasing this one pointless — replaces the hand-maintained SUPERSEDES_INTENTIONS /
|
|
542
|
+
* SUPERSEDES_RESULTS constants in notifications/leavers.ts.
|
|
543
|
+
*/
|
|
544
|
+
export const supersedingSurveyKinds = (kind: SurveyKind): SurveyKind[] => {
|
|
545
|
+
const order = SURVEY_KINDS[kind].threadOrder;
|
|
546
|
+
if (!order) return [];
|
|
547
|
+
return surveyThreadKinds(kind)
|
|
548
|
+
.filter((k) => (SURVEY_KINDS[k].threadOrder ?? 0) > order);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Message types to pass as Email.sendEmail's `supersededBy` — if any has already
|
|
553
|
+
* gone to this person for this thread, the email being built is skipped.
|
|
554
|
+
*
|
|
555
|
+
* Only covers thread supersession. Same-kind supersession (a newer cycle to the same
|
|
556
|
+
* person, plan decision D8) can't be expressed this way, because each cycle is its
|
|
557
|
+
* own collection item — the engine stamps `supersededAt` on the older request and
|
|
558
|
+
* skips it instead.
|
|
559
|
+
*/
|
|
560
|
+
export const supersedingMessageTypes = (kind: SurveyKind): string[] =>
|
|
561
|
+
supersedingSurveyKinds(kind).flatMap((k) =>
|
|
562
|
+
[SURVEY_KINDS[k].messageTypes.invite, SURVEY_KINDS[k].messageTypes.reminder]);
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The same list in pre-cutover message types. Interaction docs written before the
|
|
566
|
+
* cutover carry these AND a different collectionLink (see legacy.collectionLink), so
|
|
567
|
+
* the engine needs a second supersession query against them for one release. Both
|
|
568
|
+
* this and the dual-read go in Phase E.
|
|
569
|
+
*/
|
|
570
|
+
export const legacySupersedingMessageTypes = (kind: SurveyKind): string[] =>
|
|
571
|
+
supersedingSurveyKinds(kind).flatMap((k) => {
|
|
572
|
+
const legacy = SURVEY_KINDS[k].legacy.messageTypes;
|
|
573
|
+
return [legacy.invite, legacy.reminder].filter((t): t is string => Boolean(t));
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The copy for one email, falling back to the shared parent template for kinds that
|
|
578
|
+
* don't override it. Returns undefined when a kind deliberately has no such email
|
|
579
|
+
* (skills sends no reminders; alumni check-ins re-send the invite instead).
|
|
580
|
+
*/
|
|
581
|
+
export const surveyEmailCopy = (
|
|
582
|
+
kind: SurveyKind, audience: SurveyAudience, variant: SurveyCopyVariant,
|
|
583
|
+
): SurveyEmailCopy|undefined => {
|
|
584
|
+
const def = SURVEY_KINDS[kind];
|
|
585
|
+
const own = def.copy[variant][audience];
|
|
586
|
+
if (own) return own;
|
|
587
|
+
if (audience === "parent" && def.parentMode === "rater") return GENERIC_PARENT_COPY[variant];
|
|
588
|
+
return undefined;
|
|
589
|
+
};
|