placementt-core 1.400.1237 → 1.400.1239
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 +4 -1
- package/lib/constants.js +256 -3
- package/lib/constants.js.map +1 -1
- package/lib/onboardingPathways.d.ts +43 -20
- package/lib/onboardingPathways.js +57 -44
- package/lib/onboardingPathways.js.map +1 -1
- package/lib/onboardingPathways.test.js +18 -7
- package/lib/onboardingPathways.test.js.map +1 -1
- package/lib/renewals.d.ts +21 -6
- package/lib/renewals.js +42 -9
- package/lib/renewals.js.map +1 -1
- package/lib/renewals.test.js +17 -0
- package/lib/renewals.test.js.map +1 -1
- package/lib/typeDefinitions.d.ts +8 -70
- package/lib/typeDefinitions.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +256 -2
- package/src/onboardingPathways.test.ts +21 -6
- package/src/onboardingPathways.ts +60 -51
- package/src/renewals.test.ts +21 -1
- package/src/renewals.ts +45 -11
- package/src/typeDefinitions.ts +10 -70
|
@@ -2,7 +2,7 @@ import {InstituteData} from "./typeDefinitions";
|
|
|
2
2
|
import {ResolvedProducts} from "./productResolver";
|
|
3
3
|
import {SurveyGroupScope} from "./surveys/types";
|
|
4
4
|
import {activityLoggingState} from "./activityLogging";
|
|
5
|
-
import {
|
|
5
|
+
import {FEATURE_MARKETING_BY_KEY, FeatureMarketingCounts, FeatureMarketingKey, askForEngagementPlan, featurePlanStatus, isSelfServeSignUp} from "./featureMarketing";
|
|
6
6
|
import {hasWondeConnection} from "./studentData";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -46,19 +46,13 @@ export const ONBOARDING_PATHWAY_KEYS: OnboardingPathwayKey[] = [
|
|
|
46
46
|
* were onboarded by hand and get the plan board on their homepage instead. */
|
|
47
47
|
export const JOIN_WINDOW_DAYS = 60;
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
* Template ids from here on are labels for emails written in the backend
|
|
58
|
-
* (awsSES/templates/engagement.ts), in the 9000s to stay clear of Brevo's. */
|
|
59
|
-
export const ONBOARDING_REMINDER_TEMPLATE_IDS: {second: number, third: number} = {
|
|
60
|
-
second: 9090,
|
|
61
|
-
third: 9091,
|
|
49
|
+
/** The shared reminders in a step email's weekly reminder chain
|
|
50
|
+
* (sendEmailReminders): the first and third, with the step's name and link
|
|
51
|
+
* filled in. The second resends the step's own email. A step on the plan
|
|
52
|
+
* board gets the version with a "pick a different term" link. */
|
|
53
|
+
export const ONBOARDING_REMINDER_TEMPLATES = {
|
|
54
|
+
first: {plain: "engagementReminderDay7", plan: "engagementReminderDay7Plan"},
|
|
55
|
+
third: {plain: "engagementReminderDay21", plan: "engagementReminderDay21Plan"},
|
|
62
56
|
};
|
|
63
57
|
|
|
64
58
|
/** Everything marketing reads, plus the counts that only onboarding asks for. */
|
|
@@ -102,9 +96,10 @@ export type OnboardingStep = {
|
|
|
102
96
|
/** Built for the current academic year. Absent on a once-only step. Only
|
|
103
97
|
* consulted once `done` is true, so it need not repeat that check. */
|
|
104
98
|
thisYear?: (ctx: OnboardingContext) => boolean,
|
|
105
|
-
/**
|
|
106
|
-
* with `thisYear`.
|
|
107
|
-
|
|
99
|
+
/** The admin email template (constants.ts emailTemplates) for each email
|
|
100
|
+
* variant. `resend` is only read on steps with `thisYear`. Absent means no
|
|
101
|
+
* email has been written for the step yet. */
|
|
102
|
+
emailTemplates?: {setup: string, resend?: string},
|
|
108
103
|
/** The feature on the school's plan board this step sets up, when it is
|
|
109
104
|
* not the pathway's. Nothing is sent about it before its planned term. */
|
|
110
105
|
feature?: FeatureMarketingKey,
|
|
@@ -159,7 +154,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
159
154
|
label: "Pick Your Features",
|
|
160
155
|
description: "Choose what you'll use CareerThread for, and we'll set your account up around it.",
|
|
161
156
|
path: "/home",
|
|
162
|
-
|
|
157
|
+
emailTemplates: {setup: "engagementPickFeatures"},
|
|
163
158
|
needs: ["students"],
|
|
164
159
|
optional: true,
|
|
165
160
|
// Accounts from before the picker that already have students are past this.
|
|
@@ -170,7 +165,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
170
165
|
label: "Plan Your Year",
|
|
171
166
|
description: "Drag each feature into the term you want to set it up in. We won't email about anything before then.",
|
|
172
167
|
path: "/organisation/overview",
|
|
173
|
-
|
|
168
|
+
emailTemplates: {setup: "engagementPlanYear"},
|
|
174
169
|
needs: [],
|
|
175
170
|
optional: true,
|
|
176
171
|
// Asked once; a plan saved in any year counts.
|
|
@@ -182,7 +177,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
182
177
|
label: "Invite Your Staff",
|
|
183
178
|
description: "Give the rest of the careers team their own logins so the work is shared.",
|
|
184
179
|
path: "/organisation/staff",
|
|
185
|
-
|
|
180
|
+
emailTemplates: {setup: "engagementInviteStaff"},
|
|
186
181
|
needs: ["staff"],
|
|
187
182
|
emails: 1,
|
|
188
183
|
optional: true,
|
|
@@ -200,7 +195,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
200
195
|
label: "Choose How to Add Your Students",
|
|
201
196
|
description: "Connect through Wonde, send us a file from your MIS, or upload a spreadsheet. Pick one and we'll send what you need.",
|
|
202
197
|
path: "/students",
|
|
203
|
-
|
|
198
|
+
emailTemplates: {setup: "engagementStudentRoute"},
|
|
204
199
|
needs: ["students"],
|
|
205
200
|
// A trust and a hub hold no students of their own; their
|
|
206
201
|
// first step is linking the schools that do.
|
|
@@ -212,7 +207,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
212
207
|
label: "Approve the Wonde Connection",
|
|
213
208
|
description: "Approve CareerThread in your Wonde portal and your students sync automatically.",
|
|
214
209
|
path: "/students",
|
|
215
|
-
|
|
210
|
+
emailTemplates: {setup: "engagementWonde"},
|
|
216
211
|
needs: ["students"],
|
|
217
212
|
applies: studentRouteIs("wonde"),
|
|
218
213
|
done: (ctx) => hasStudents(ctx) || hasWondeConnection(ctx.institute),
|
|
@@ -222,7 +217,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
222
217
|
label: "Set Up Your MIS File Transfer",
|
|
223
218
|
description: "Schedule an export from your MIS to us and your students stay up to date.",
|
|
224
219
|
path: "/students",
|
|
225
|
-
|
|
220
|
+
emailTemplates: {setup: "engagementMis"},
|
|
226
221
|
needs: ["students"],
|
|
227
222
|
applies: studentRouteIs("mis"),
|
|
228
223
|
done: hasStudents,
|
|
@@ -232,7 +227,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
232
227
|
label: "Add Your Students",
|
|
233
228
|
description: "Upload a spreadsheet of your students and every student gets an account.",
|
|
234
229
|
path: "/students",
|
|
235
|
-
|
|
230
|
+
emailTemplates: {setup: "engagementUploadStudents"},
|
|
236
231
|
needs: ["students"],
|
|
237
232
|
applies: studentRouteIs("csv"),
|
|
238
233
|
done: hasStudents,
|
|
@@ -249,7 +244,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
249
244
|
label: "Link Your Schools",
|
|
250
245
|
description: "Invite trust schools to CareerThread, to contribute to their careers provision.",
|
|
251
246
|
path: "/schools",
|
|
252
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
253
247
|
needs: ["schools"],
|
|
254
248
|
done: ({counts}) => (counts.schools || 0) > 0,
|
|
255
249
|
},
|
|
@@ -265,7 +259,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
265
259
|
label: "Add Your Schools",
|
|
266
260
|
description: "Invite the schools in your area and they appear on your hub.",
|
|
267
261
|
path: "/schools",
|
|
268
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
269
262
|
needs: ["schools"],
|
|
270
263
|
done: ({counts}) => (counts.schools || 0) > 0,
|
|
271
264
|
},
|
|
@@ -274,7 +267,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
274
267
|
label: "Add Your Employers",
|
|
275
268
|
description: "Import the employers you already work with.",
|
|
276
269
|
path: "/network",
|
|
277
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
278
270
|
needs: ["employers"],
|
|
279
271
|
done: ({counts}) => (counts.employers || 0) > 0,
|
|
280
272
|
},
|
|
@@ -283,7 +275,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
283
275
|
label: "Enable Your Public Events Page",
|
|
284
276
|
description: "Let employers find and sign up to your events themselves.",
|
|
285
277
|
path: "/network/overview#employerSettings",
|
|
286
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
287
278
|
needs: [],
|
|
288
279
|
done: ({institute}) => Boolean(institute.publicEventPage),
|
|
289
280
|
},
|
|
@@ -292,7 +283,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
292
283
|
label: "Send Your First Email Campaign",
|
|
293
284
|
description: "Reach every employer on the hub in one go.",
|
|
294
285
|
path: "/network/emailCampaigns",
|
|
295
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
296
286
|
needs: ["emailCampaigns"],
|
|
297
287
|
done: ({counts}) => (counts.emailCampaigns || 0) > 0,
|
|
298
288
|
},
|
|
@@ -301,7 +291,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
301
291
|
label: "Create Your First Event",
|
|
302
292
|
description: "Invite schools and employers and track who turns up.",
|
|
303
293
|
path: "/events",
|
|
304
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
305
294
|
needs: ["events"],
|
|
306
295
|
done: marketingAdopted("events"),
|
|
307
296
|
},
|
|
@@ -320,7 +309,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
320
309
|
label: "Request Your First Event",
|
|
321
310
|
description: "Pick the kind of employers you want and we handle the invites.",
|
|
322
311
|
path: "/events",
|
|
323
|
-
|
|
312
|
+
emailTemplates: {setup: "engagementHubFirstEvent"},
|
|
324
313
|
needs: ["events"],
|
|
325
314
|
done: ({counts}) => (counts.events || 0) > 0,
|
|
326
315
|
},
|
|
@@ -337,7 +326,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
337
326
|
label: "Set Up a Work Experience Group",
|
|
338
327
|
description: "Create a recurring group and we'll make the cohort every year.",
|
|
339
328
|
path: "/cohorts",
|
|
340
|
-
|
|
329
|
+
emailTemplates: {setup: "engagementWexGroups", resend: "engagementWexCohort"},
|
|
341
330
|
needs: ["cohorts", "cohortsThisYear"],
|
|
342
331
|
done: marketingAdopted("wexCohort"),
|
|
343
332
|
thisYear: ({counts}) => (counts.cohortsThisYear || 0) > 0,
|
|
@@ -347,7 +336,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
347
336
|
label: "Build a Sign-Off Workflow",
|
|
348
337
|
description: "Decide who signs off each placement, in what order, and which forms they see.",
|
|
349
338
|
path: "/cohorts",
|
|
350
|
-
|
|
339
|
+
emailTemplates: {setup: "engagementWexWorkflow"},
|
|
351
340
|
needs: [],
|
|
352
341
|
done: ({institute}) =>
|
|
353
342
|
Object.values(institute.cohortConfig || {}).some((g) => Boolean(g.workflowId)),
|
|
@@ -357,7 +346,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
357
346
|
label: "Activate Your Students",
|
|
358
347
|
description: "Send them their logins and they can start adding placements.",
|
|
359
348
|
path: "/students",
|
|
360
|
-
|
|
349
|
+
emailTemplates: {setup: "engagementWexActivate"},
|
|
361
350
|
needs: ["activatedStudents"],
|
|
362
351
|
done: ({counts}) => (counts.activatedStudents || 0) > 0,
|
|
363
352
|
},
|
|
@@ -366,7 +355,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
366
355
|
label: "Add Your First Placements",
|
|
367
356
|
description: "Students or staff add placements and the sign-off workflow takes it from there.",
|
|
368
357
|
path: "/cohorts",
|
|
369
|
-
|
|
358
|
+
emailTemplates: {setup: "engagementWexPlacements"},
|
|
370
359
|
needs: ["placements"],
|
|
371
360
|
done: ({counts}) => (counts.placements || 0) > 0,
|
|
372
361
|
},
|
|
@@ -375,7 +364,7 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
375
364
|
label: "Set Up Placement Feedback",
|
|
376
365
|
description: "Ask students and employers how it went, automatically, when each placement ends.",
|
|
377
366
|
path: "/cohorts",
|
|
378
|
-
|
|
367
|
+
emailTemplates: {setup: "engagementWexFeedback"},
|
|
379
368
|
needs: [],
|
|
380
369
|
done: ({institute}) =>
|
|
381
370
|
Object.values(institute.cohortConfig || {}).some((g) => Boolean(g.feedback)),
|
|
@@ -418,7 +407,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
418
407
|
label: "Import Your Employers",
|
|
419
408
|
description: "Bring in the employers you already work with, so your first event has people to invite.",
|
|
420
409
|
path: "/network",
|
|
421
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
422
410
|
needs: ["employers"],
|
|
423
411
|
done: ({counts}) => (counts.employers || 0) > 0,
|
|
424
412
|
},
|
|
@@ -427,7 +415,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
427
415
|
label: "Open Your Employer Page",
|
|
428
416
|
description: "Let employers sign up to your network and see your events themselves.",
|
|
429
417
|
path: "/network/overview#employerSettings",
|
|
430
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
431
418
|
needs: [],
|
|
432
419
|
optional: true,
|
|
433
420
|
done: ({institute}) => Boolean(institute.publicEventPage),
|
|
@@ -437,7 +424,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
437
424
|
label: "Create Your First Event",
|
|
438
425
|
description: "Invite students, staff and employers and we'll track invites and attendance.",
|
|
439
426
|
path: "/events",
|
|
440
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
441
427
|
needs: ["events"],
|
|
442
428
|
done: marketingAdopted("events"),
|
|
443
429
|
},
|
|
@@ -446,7 +432,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
446
432
|
label: "Invite Employers to an Event",
|
|
447
433
|
description: "Pick them from your network by sector and we chase the replies.",
|
|
448
434
|
path: "/events",
|
|
449
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
450
435
|
needs: ["eventAttendees"],
|
|
451
436
|
done: ({counts}) => (counts.eventAttendees || 0) > 0,
|
|
452
437
|
},
|
|
@@ -471,7 +456,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
471
456
|
label: "Set Up Career Aspiration Surveys",
|
|
472
457
|
description: "What students want to do next, asked every year and compared with where they go.",
|
|
473
458
|
path: "/destinations",
|
|
474
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET, resend: BREVO_TEMPLATE_UNSET},
|
|
475
459
|
needs: ["surveyScopesThisYear"],
|
|
476
460
|
done: marketingAdopted("aspirations"),
|
|
477
461
|
thisYear: hasScope("aspirations"),
|
|
@@ -482,7 +466,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
482
466
|
label: "Set Up Leaver Tracking",
|
|
483
467
|
description: "Intentions in spring, destinations in autumn, for every year that leaves.",
|
|
484
468
|
path: "/destinations/leavers",
|
|
485
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET, resend: BREVO_TEMPLATE_UNSET},
|
|
486
469
|
needs: ["surveyScopesThisYear"],
|
|
487
470
|
done: marketingAdopted("leavers"),
|
|
488
471
|
thisYear: hasScope("leavers"),
|
|
@@ -493,7 +476,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
493
476
|
label: "Add Your Alumni",
|
|
494
477
|
description: "Import the leavers you're in touch with, or let this year's leavers opt in.",
|
|
495
478
|
path: "/network/alumni",
|
|
496
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
497
479
|
needs: ["alumni"],
|
|
498
480
|
applies: ({products}) => products.alumni,
|
|
499
481
|
done: marketingAdopted("alumni"),
|
|
@@ -511,7 +493,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
511
493
|
label: "Set Up Skills Tracking",
|
|
512
494
|
description: "Choose the skills you assess and when, and the surveys go out on schedule.",
|
|
513
495
|
path: "/development/assessments",
|
|
514
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET, resend: BREVO_TEMPLATE_UNSET},
|
|
515
496
|
needs: ["surveyScopesThisYear"],
|
|
516
497
|
done: marketingAdopted("skills"),
|
|
517
498
|
thisYear: hasScope("skills"),
|
|
@@ -522,7 +503,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
522
503
|
label: "Set Up 1:1 Careers Guidance",
|
|
523
504
|
description: "Schedule guidance meetings per year group and record them for Gatsby.",
|
|
524
505
|
path: "/development/guidance",
|
|
525
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET, resend: BREVO_TEMPLATE_UNSET},
|
|
526
506
|
needs: ["surveyScopesThisYear"],
|
|
527
507
|
done: marketingAdopted("guidance"),
|
|
528
508
|
thisYear: hasScope("guidance"),
|
|
@@ -540,7 +520,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
540
520
|
label: "Launch It to a Year Group",
|
|
541
521
|
description: "Nothing is shown to a student until you say which years have been told.",
|
|
542
522
|
path: "/development/activities",
|
|
543
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET, resend: BREVO_TEMPLATE_UNSET},
|
|
544
523
|
needs: [],
|
|
545
524
|
// A launch is made for one academic year unless the school said
|
|
546
525
|
// it rolls over, so last year's is set up but not this year's. Setup
|
|
@@ -561,7 +540,6 @@ export const ONBOARDING_PATHWAYS: OnboardingPathway[] = [
|
|
|
561
540
|
label: "Invite Your Parents",
|
|
562
541
|
description: "Write the invitation, upload the list, and the database builds from the replies.",
|
|
563
542
|
path: "/network/parents",
|
|
564
|
-
templateIds: {setup: BREVO_TEMPLATE_UNSET},
|
|
565
543
|
needs: ["parents"],
|
|
566
544
|
done: marketingAdopted("parents"),
|
|
567
545
|
},
|
|
@@ -600,9 +578,8 @@ export type OnboardingStepProgress = {
|
|
|
600
578
|
done: boolean,
|
|
601
579
|
/** Which email this step would get, were it the one sent. */
|
|
602
580
|
variant: OnboardingEmailVariant,
|
|
603
|
-
/** The
|
|
604
|
-
|
|
605
|
-
templateId: number,
|
|
581
|
+
/** The admin email template for that variant, when one has been written. */
|
|
582
|
+
emailTemplate?: string,
|
|
606
583
|
/** The plan board feature it sets up: its own, or its pathway's. */
|
|
607
584
|
feature?: FeatureMarketingKey,
|
|
608
585
|
/** How many emails it gets before it counts as passed or stuck. */
|
|
@@ -650,7 +627,7 @@ export const evaluateOnboarding = (ctx: OnboardingContext): OnboardingPathwayPro
|
|
|
650
627
|
state,
|
|
651
628
|
done: state === "done",
|
|
652
629
|
variant,
|
|
653
|
-
|
|
630
|
+
emailTemplate: s.emailTemplates?.[variant],
|
|
654
631
|
feature: s.feature ?? p.feature,
|
|
655
632
|
emails: s.emails ?? 3,
|
|
656
633
|
optional: Boolean(s.optional),
|
|
@@ -729,3 +706,35 @@ export const planFeatureOf = (kind: "marketing"|"onboarding"|"renewal", key: str
|
|
|
729
706
|
const pathway = ONBOARDING_PATHWAYS.find((p) => p.steps.some((s) => s.key === key));
|
|
730
707
|
return pathway?.steps.find((s) => s.key === key)?.feature ?? pathway?.feature;
|
|
731
708
|
};
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Whether one step is done, judged on its own: an email sent by hand may be
|
|
712
|
+
* about a step that doesn't apply to the school (the Wonde email to a school
|
|
713
|
+
* on the spreadsheet route), so `applies` is not consulted. A step with
|
|
714
|
+
* `thisYear` is done once it is built for this year, and an optional step
|
|
715
|
+
* once it has been passed.
|
|
716
|
+
*
|
|
717
|
+
* @param {OnboardingContext} ctx the institute, its products and counts.
|
|
718
|
+
* @param {string} stepKey the step.
|
|
719
|
+
* @return {boolean|undefined} whether it is done, or undefined for an unknown step.
|
|
720
|
+
*/
|
|
721
|
+
export const onboardingStepDone = (ctx: OnboardingContext, stepKey: string): boolean|undefined => {
|
|
722
|
+
const step = ONBOARDING_PATHWAYS.flatMap((p) => p.steps).find((s) => s.key === stepKey);
|
|
723
|
+
if (!step) return undefined;
|
|
724
|
+
if (step.optional && ctx.institute.engagement?.onboarding?.passed?.[step.key]) return true;
|
|
725
|
+
return step.done(ctx) && (!step.thisYear || step.thisYear(ctx));
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* The step an admin email template is the email for.
|
|
730
|
+
*
|
|
731
|
+
* @param {string} templateKey the email template.
|
|
732
|
+
* @return {object|undefined} the step and its plan board feature.
|
|
733
|
+
*/
|
|
734
|
+
export const onboardingStepOfTemplate = (templateKey: string): {step: OnboardingStep, feature?: FeatureMarketingKey}|undefined => {
|
|
735
|
+
for (const pathway of ONBOARDING_PATHWAYS) {
|
|
736
|
+
const step = pathway.steps.find((s) => s.emailTemplates?.setup === templateKey || s.emailTemplates?.resend === templateKey);
|
|
737
|
+
if (step) return {step, feature: step.feature ?? pathway.feature};
|
|
738
|
+
}
|
|
739
|
+
return undefined;
|
|
740
|
+
};
|
package/src/renewals.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ jest.mock("./firebase/firebaseQuery", () => ({__esModule: true, default: class {
|
|
|
3
3
|
|
|
4
4
|
import {InstituteData} from "./typeDefinitions";
|
|
5
5
|
import {resolveProducts} from "./productResolver";
|
|
6
|
-
import {RenewalStep, renewalSituation} from "./renewals";
|
|
6
|
+
import {RenewalStep, renewalSituation, renewalTaskSchedule} from "./renewals";
|
|
7
7
|
|
|
8
8
|
const school = (renewalDate?: string, overrides: Partial<InstituteData> = {}): InstituteData => ({
|
|
9
9
|
name: "Calder Park Academy",
|
|
@@ -61,3 +61,23 @@ describe("renewals", () => {
|
|
|
61
61
|
expect(at(thanked, "2026-11-10").due).toBeUndefined();
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
describe("renewalTaskSchedule", () => {
|
|
66
|
+
it("dates each renewal task from the renewal date", () => {
|
|
67
|
+
expect(renewalTaskSchedule("2027-10-01", "2026-11-03")).toEqual([
|
|
68
|
+
{step: "headsUp", due: "2027-04-20"},
|
|
69
|
+
{step: "yearInReview", due: "2027-07-01"},
|
|
70
|
+
{step: "sixtyDay", due: "2027-08-02"},
|
|
71
|
+
{step: "thirtyDay", due: "2027-09-01"},
|
|
72
|
+
{step: "lapsed", due: "2027-10-02"},
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("keeps only the step a late date is in, of the ones already gone", () => {
|
|
77
|
+
expect(renewalTaskSchedule("2026-12-15", "2026-11-03").map((t) => t.step)).toEqual(["sixtyDay", "thirtyDay", "lapsed"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("leaves out a heads-up or year in review more than a year before the 60-day email", () => {
|
|
81
|
+
expect(renewalTaskSchedule("2027-06-01", "2026-11-03").map((t) => t.step)).toEqual(["yearInReview", "sixtyDay", "thirtyDay", "lapsed"]);
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/renewals.ts
CHANGED
|
@@ -8,9 +8,8 @@ import {londonYmd} from "./outreachRules";
|
|
|
8
8
|
* Renewal is commercial and per customer, so it is kept apart from
|
|
9
9
|
* re-onboarding, which is academic and per feature (the September plan board
|
|
10
10
|
* and the "new year" steps). Tom renews every customer himself: each email
|
|
11
|
-
* here is a
|
|
12
|
-
*
|
|
13
|
-
* task for him rather than an email.
|
|
11
|
+
* here is a task for him, made when the renewal date is set
|
|
12
|
+
* (renewalTaskSchedule), that he sends from the task with the email in hand.
|
|
14
13
|
*
|
|
15
14
|
* The year runs from `cs.renewalDate`:
|
|
16
15
|
* - headsUp: late April to May, before budgets for next year are set. Next
|
|
@@ -44,14 +43,13 @@ export const RENEWAL_STEP_LABELS: Record<RenewalStep, string> = {
|
|
|
44
43
|
/** Steps that copy in the finance contact as well as the school's contacts. */
|
|
45
44
|
export const RENEWAL_FINANCE_STEPS: RenewalStep[] = ["headsUp", "sixtyDay", "thirtyDay"];
|
|
46
45
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
thanks: 9105,
|
|
46
|
+
/** The admin email template (constants.ts emailTemplates) for each step. */
|
|
47
|
+
export const RENEWAL_EMAIL_TEMPLATES: Record<RenewalStep, string> = {
|
|
48
|
+
headsUp: "engagementRenewalPrice",
|
|
49
|
+
yearInReview: "engagementRenewalReview",
|
|
50
|
+
sixtyDay: "engagementRenewalDue",
|
|
51
|
+
thirtyDay: "engagementRenewalInvoice",
|
|
52
|
+
thanks: "engagementRenewalThanks",
|
|
55
53
|
};
|
|
56
54
|
|
|
57
55
|
export type RenewalSituation = {
|
|
@@ -136,3 +134,39 @@ export const renewalSituation = ({institute, products, now}: {institute: Institu
|
|
|
136
134
|
}
|
|
137
135
|
return {...base, lapsed: false, stage: "early"};
|
|
138
136
|
};
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
const addDays = (ymd: string, days: number): string => new Date(ymdToMs(ymd) + days * DAY_MS).toISOString().slice(0, 10);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The renewal tasks for one renewal date, each with the day it is due: the
|
|
143
|
+
* same timings as renewalSituation, worked out once when the date is set
|
|
144
|
+
* rather than checked daily. The heads-up is the last 20 April and the year
|
|
145
|
+
* in review the last 1 July that fall within the year before the 60-day
|
|
146
|
+
* email. A date set late skips the steps already gone, but keeps the most
|
|
147
|
+
* recent one, since that's the step the customer is in. The thanks is not
|
|
148
|
+
* here: it is due when the date moves on.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} renewalDate YYYY-MM-DD.
|
|
151
|
+
* @param {string} today YYYY-MM-DD, London.
|
|
152
|
+
* @return {object[]} each step and its due date, plus "lapsed" the day after renewal.
|
|
153
|
+
*/
|
|
154
|
+
export const renewalTaskSchedule = (renewalDate: string, today: string): {step: Exclude<RenewalStep, "thanks">|"lapsed", due: string}[] => {
|
|
155
|
+
const sixty = addDays(renewalDate, -60);
|
|
156
|
+
const yearBefore = addDays(renewalDate, -365);
|
|
157
|
+
const lastBefore = (md: string) => {
|
|
158
|
+
const year = Number(sixty.slice(0, 4));
|
|
159
|
+
const candidate = `${year}-${md}` < sixty ? `${year}-${md}` : `${year - 1}-${md}`;
|
|
160
|
+
return candidate >= yearBefore ? candidate : undefined;
|
|
161
|
+
};
|
|
162
|
+
const all = ([
|
|
163
|
+
{step: "headsUp", due: lastBefore("04-20")},
|
|
164
|
+
{step: "yearInReview", due: lastBefore("07-01")},
|
|
165
|
+
{step: "sixtyDay", due: sixty},
|
|
166
|
+
{step: "thirtyDay", due: addDays(renewalDate, -30)},
|
|
167
|
+
] as {step: Exclude<RenewalStep, "thanks">, due?: string}[])
|
|
168
|
+
.filter((t): t is {step: Exclude<RenewalStep, "thanks">, due: string} => Boolean(t.due))
|
|
169
|
+
.sort((a, b) => a.due.localeCompare(b.due));
|
|
170
|
+
const gone = all.filter((t) => t.due < today);
|
|
171
|
+
return [...all.filter((t) => t.due >= today || t === gone[gone.length - 1]), {step: "lapsed", due: addDays(renewalDate, 1)}];
|
|
172
|
+
};
|
package/src/typeDefinitions.ts
CHANGED
|
@@ -2012,7 +2012,7 @@ export type EmailTemplateConfig = {
|
|
|
2012
2012
|
button?: {text: string, link: string},
|
|
2013
2013
|
secondaryButton?: {text: string, link: string},
|
|
2014
2014
|
optionalButtons?: {text: string, link: string}[],
|
|
2015
|
-
type: "workflow"|"feedback"|"event"|"campaign"|"database"|"onboarding"|"skillsInvite"|"aspirationInvite"|"leaverInvite"|"leaverResultsInvite"|"leaverChristmasInvite",
|
|
2015
|
+
type: "workflow"|"feedback"|"event"|"campaign"|"database"|"onboarding"|"skillsInvite"|"aspirationInvite"|"leaverInvite"|"leaverResultsInvite"|"leaverChristmasInvite"|"engagement",
|
|
2016
2016
|
product: EmailTemplateProduct,
|
|
2017
2017
|
sections?: EmailSectionNames[],
|
|
2018
2018
|
group?: string, // Templates sharing a group are listed together, e.g. the three feedback requests.
|
|
@@ -2027,7 +2027,8 @@ export type EmailTemplateConfig = {
|
|
|
2027
2027
|
// Which product a resource (email template, guidance doc, default form) belongs to, so customers only see resources for products they have.
|
|
2028
2028
|
// Any ProductKey from the product resolver, "employerEvents" for the events container itself, or "hub" for careers hub only.
|
|
2029
2029
|
export type ProductTag = ProductKey|"employerEvents"|"hub";
|
|
2030
|
-
|
|
2030
|
+
// "admin": CareerThread's own emails to customers (onboarding, renewals), only selectable by us.
|
|
2031
|
+
export type EmailTemplateProduct = ProductTag|"admin";
|
|
2031
2032
|
|
|
2032
2033
|
export type EmailCampaign = {
|
|
2033
2034
|
id: string,
|
|
@@ -3812,6 +3813,7 @@ export type YearScoreData = {
|
|
|
3812
3813
|
|
|
3813
3814
|
type MessageTypes =
|
|
3814
3815
|
"hubOrTrustInviteSchool" |
|
|
3816
|
+
"engagementEmail" | // an admin email template sent to a school (featureEngagement.ts)
|
|
3815
3817
|
"sendPlacementActionRequired" |
|
|
3816
3818
|
"sendPlacementAdditionalInformationRequired" |
|
|
3817
3819
|
"sendFeedbackReminder" |
|
|
@@ -3862,7 +3864,8 @@ export type SurveyMessageType =
|
|
|
3862
3864
|
export type ReminderFallbackType =
|
|
3863
3865
|
"checkInTrustedContact" | // legacy name for the below; live interaction docs still carry it
|
|
3864
3866
|
"surveyParentFallback" | // chase the parent/trusted contact when the subject ignores their whole chain
|
|
3865
|
-
"alumniCheckInFallback"
|
|
3867
|
+
"alumniCheckInFallback" | // the same, for the annual alumni check-in, which is not a survey
|
|
3868
|
+
"engagementStuck"; // an onboarding email's reminders ran out with the step still not done
|
|
3866
3869
|
|
|
3867
3870
|
export type EmailInteraction = {
|
|
3868
3871
|
messageId: string,
|
|
@@ -4637,20 +4640,11 @@ export type CustomerSuccessData = {
|
|
|
4637
4640
|
};
|
|
4638
4641
|
|
|
4639
4642
|
// ─── Engagement emails ───────────────────────────────────────────────────────
|
|
4640
|
-
// Onboarding
|
|
4641
|
-
//
|
|
4643
|
+
// Onboarding and renewal emails, sent by an admin (onboardingPathways.ts and
|
|
4644
|
+
// renewals.ts say which; the emails are admin email templates).
|
|
4642
4645
|
|
|
4643
4646
|
export type InstituteEngagement = {
|
|
4644
|
-
/** Last engagement email of any kind, for the one-per-week cap. */
|
|
4645
|
-
lastEmailAt?: string,
|
|
4646
4647
|
onboarding?: {
|
|
4647
|
-
/** The step last nudged, so the same step is not sent twice in a row
|
|
4648
|
-
* without a change (the cron caps repeats of one step). */
|
|
4649
|
-
lastStepKey?: string,
|
|
4650
|
-
lastVariant?: "setup"|"resend",
|
|
4651
|
-
lastSentAt?: string,
|
|
4652
|
-
/** How many times in a row the same step has been sent. */
|
|
4653
|
-
repeats?: number,
|
|
4654
4648
|
/** Optional steps whose emails ran out unanswered, keyed by step, with
|
|
4655
4649
|
* the date. They count as done so the next step can start. */
|
|
4656
4650
|
passed?: {[stepKey: string]: string},
|
|
@@ -4709,22 +4703,6 @@ export type FeatureCampaign = {
|
|
|
4709
4703
|
endedAt?: string,
|
|
4710
4704
|
};
|
|
4711
4705
|
|
|
4712
|
-
/** `engagementEmails/{autoId}`: one flat log document per send, for the
|
|
4713
|
-
* admin view. */
|
|
4714
|
-
export type EngagementEmailLog = {
|
|
4715
|
-
oId: string,
|
|
4716
|
-
kind: "marketing"|"onboarding"|"renewal",
|
|
4717
|
-
key: string,
|
|
4718
|
-
variant?: "setup"|"resend",
|
|
4719
|
-
touch?: number,
|
|
4720
|
-
templateId: number,
|
|
4721
|
-
recipients: string[],
|
|
4722
|
-
sentAt: string,
|
|
4723
|
-
campaignId?: string,
|
|
4724
|
-
/** The approved draft it was sent from. */
|
|
4725
|
-
draftId?: string,
|
|
4726
|
-
};
|
|
4727
|
-
|
|
4728
4706
|
export type StudentRoute = {
|
|
4729
4707
|
route: "wonde"|"mis"|"csv",
|
|
4730
4708
|
chosenAt: string,
|
|
@@ -4749,46 +4727,6 @@ export type EngagementPlan = {
|
|
|
4749
4727
|
savedBy: string,
|
|
4750
4728
|
};
|
|
4751
4729
|
|
|
4752
|
-
/** Where an engagement email is in its approval: written as a draft, approved
|
|
4753
|
-
* by an admin for the next school morning, then sent; or skipped by an admin,
|
|
4754
|
-
* expired unapproved, or dropped because the school did the step first. */
|
|
4755
|
-
export type EngagementDraftStatus = "draft"|"approved"|"sent"|"skipped"|"expired"|"dropped";
|
|
4756
|
-
|
|
4757
|
-
/** `engagementDrafts/{autoId}`: one engagement email to one institute. The
|
|
4758
|
-
* daily check writes every email as one of these and nothing sends until an
|
|
4759
|
-
* admin approves it. */
|
|
4760
|
-
export type EngagementDraft = {
|
|
4761
|
-
oId: string,
|
|
4762
|
-
instituteName: string,
|
|
4763
|
-
kind: "marketing"|"onboarding"|"renewal",
|
|
4764
|
-
key: string,
|
|
4765
|
-
variant?: "setup"|"resend",
|
|
4766
|
-
touch?: number,
|
|
4767
|
-
templateId: number,
|
|
4768
|
-
label: string,
|
|
4769
|
-
description: string,
|
|
4770
|
-
path: string,
|
|
4771
|
-
campaignId?: string,
|
|
4772
|
-
/** Onboarding: `engagement.onboarding.repeats` once this is sent. */
|
|
4773
|
-
repeats?: number,
|
|
4774
|
-
recipients: {email: string, forename: string, surname: string}[],
|
|
4775
|
-
/** Drafts with the same template and step, approved together. */
|
|
4776
|
-
batchKey: string,
|
|
4777
|
-
status: EngagementDraftStatus,
|
|
4778
|
-
createdAt: string,
|
|
4779
|
-
approvedAt?: string,
|
|
4780
|
-
approvedBy?: string,
|
|
4781
|
-
/** 7:30am London on the school day after approval. */
|
|
4782
|
-
sendAt?: string,
|
|
4783
|
-
sentAt?: string,
|
|
4784
|
-
sentTo?: string[],
|
|
4785
|
-
/** When it was skipped, expired or dropped, and why. */
|
|
4786
|
-
closedAt?: string,
|
|
4787
|
-
closedReason?: string,
|
|
4788
|
-
/** An admin changed the description or recipients. */
|
|
4789
|
-
edited?: boolean,
|
|
4790
|
-
};
|
|
4791
|
-
|
|
4792
4730
|
export type CustomerTask = {
|
|
4793
4731
|
id: string;
|
|
4794
4732
|
customerId: string;
|
|
@@ -4806,6 +4744,8 @@ export type CustomerTask = {
|
|
|
4806
4744
|
completedBy?: string;
|
|
4807
4745
|
triggerType: "manual" | "renewalLogged" | "timeBased" | "metricThreshold" | "calendar";
|
|
4808
4746
|
triggerKey?: string;
|
|
4747
|
+
/** The admin email template (constants.ts emailTemplates) this task sends, from the task. */
|
|
4748
|
+
emailTemplate?: string;
|
|
4809
4749
|
commId?: string;
|
|
4810
4750
|
commType?: "users";
|
|
4811
4751
|
metadata?: {
|