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.
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ /**
3
+ * surveys/schedule.ts — resolving survey schedules to concrete dates.
4
+ *
5
+ * Home of the relative survey-date rules ("first Monday of June") that were
6
+ * previously in constants.ts, plus the slot→date and academic-year helpers that
7
+ * provisionDueSkillsSurveys (skillsAssessments.ts) and destinationsProvisionCron
8
+ * (aspirations.ts) each had their own copy of.
9
+ *
10
+ * A rule is year-agnostic; it resolves into a concrete date for a given academic
11
+ * year (each September) via surveyRuleToMMDD / surveyRuleToDate.
12
+ *
13
+ * ── One academic-year boundary: 1 September ───────────────────────────────────
14
+ * Everything here derives the academic year the same way — currentAcademicStartYear,
15
+ * and academicYearOf which labels it. September to August is one academic year.
16
+ *
17
+ * This is a deliberate correction. Pre-engine code had two boundaries: the year
18
+ * LABEL flipped in August (skillsAcademicYear in skillsAssessments.ts, and the
19
+ * inline `sendMonth >= 8` in createAspirationCycle) while rule resolution flipped
20
+ * in September (destinationsStartYear). So a survey sent in August was labelled with
21
+ * the year ahead of the one it was scheduled from. Two consequences for the rollout:
22
+ * • the migration scripts must RE-DERIVE academicYear from scheduledSendDate with
23
+ * academicYearOf rather than copying the stored label, or August-dated cycles
24
+ * (leaver results-day checkpoints ~29 Aug especially) land in the wrong year;
25
+ * • the two legacy derivations must be replaced by academicYearOf when their
26
+ * callers move onto the engine, so new writes agree with migrated ones.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.isSlotDueOn = exports.resolveSlotsForYear = exports.slotMMDDForYear = exports.mmddToISO = exports.surveyRuleToMMDD = exports.surveyRuleToDate = exports.resolveNthWeekday = exports.normaliseAcademicYear = exports.academicYearSlashLabel = exports.academicYearOf = exports.academicYearLabel = exports.currentAcademicStartYear = exports.academicCalendarYear = exports.toMMDD = void 0;
30
+ const pad2 = (n) => String(n).padStart(2, "0");
31
+ /** "MM-DD" for a date, in local time — the format survey slots are stored in. */
32
+ const toMMDD = (date = new Date()) => `${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
33
+ exports.toMMDD = toMMDD;
34
+ /**
35
+ * Academic years run September→August. Given the year a school year STARTS in
36
+ * (e.g. 2026 for "2026/27"), return the calendar year the given month falls in:
37
+ * September–December sit in the start year; January–August roll into the next.
38
+ */
39
+ const academicCalendarYear = (month, academicStartYear) => month >= 9 ? academicStartYear : academicStartYear + 1;
40
+ exports.academicCalendarYear = academicCalendarYear;
41
+ /**
42
+ * The academic year a date falls within, as the START year. From September onwards
43
+ * we are in a new academic year (getMonth() is 0-indexed, so >= 8 is September).
44
+ * Matches DestinationsSetup.currentStartYear and destinationsStartYear.
45
+ */
46
+ const currentAcademicStartYear = (date = new Date()) => date.getMonth() >= 8 ? date.getFullYear() : date.getFullYear() - 1;
47
+ exports.currentAcademicStartYear = currentAcademicStartYear;
48
+ /** Hyphen academic-year label from a start year, e.g. 2026 → "2026-27". */
49
+ const academicYearLabel = (startYear) => `${startYear}-${String(startYear + 1).slice(-2)}`;
50
+ exports.academicYearLabel = academicYearLabel;
51
+ /**
52
+ * The academic-year LABEL for a date, hyphen form (e.g. "2026-27"). Uses the single
53
+ * 1 September boundary, so it always agrees with currentAcademicStartYear: a date in
54
+ * August belongs to the academic year that is ending, not the one about to start.
55
+ */
56
+ const academicYearOf = (date = new Date()) => (0, exports.academicYearLabel)((0, exports.currentAcademicStartYear)(date));
57
+ exports.academicYearOf = academicYearOf;
58
+ /** Slash academic-year label from a start year, e.g. 2026 → "2026/27". The form
59
+ * legacy leaverGroups and DestinationsConfig.lastProvisionedYear use. */
60
+ const academicYearSlashLabel = (startYear) => `${startYear}/${String(startYear + 1).slice(-2)}`;
61
+ exports.academicYearSlashLabel = academicYearSlashLabel;
62
+ /** Convert either academic-year form to the other's separator, e.g. "2026/27" → "2026-27". */
63
+ const normaliseAcademicYear = (year) => year.replace("/", "-");
64
+ exports.normaliseAcademicYear = normaliseAcademicYear;
65
+ /**
66
+ * Resolve a rule to a concrete Date in a specific CALENDAR year+month, e.g.
67
+ * ("first", Monday, June 2027). "last" walks back from the month's final day.
68
+ */
69
+ const resolveNthWeekday = (calendarYear, month, ordinal, weekday) => {
70
+ if (ordinal === "last") {
71
+ const lastDay = new Date(calendarYear, month, 0).getDate(); // day 0 of next month
72
+ for (let d = lastDay; d >= 1; d--) {
73
+ const date = new Date(calendarYear, month - 1, d);
74
+ if (date.getDay() === weekday)
75
+ return date;
76
+ }
77
+ return new Date(calendarYear, month - 1, lastDay);
78
+ }
79
+ const nth = { first: 1, second: 2, third: 3, fourth: 4 }[ordinal];
80
+ let count = 0;
81
+ const daysInMonth = new Date(calendarYear, month, 0).getDate();
82
+ for (let d = 1; d <= daysInMonth; d++) {
83
+ const date = new Date(calendarYear, month - 1, d);
84
+ if (date.getDay() === weekday && ++count === nth)
85
+ return date;
86
+ }
87
+ // Fewer than `nth` occurrences (e.g. a 5th Monday) — fall back to the last one.
88
+ return (0, exports.resolveNthWeekday)(calendarYear, month, "last", weekday);
89
+ };
90
+ exports.resolveNthWeekday = resolveNthWeekday;
91
+ /** Resolve a rule to the concrete Date for the academic year starting in academicStartYear. */
92
+ const surveyRuleToDate = (rule, academicStartYear) => (0, exports.resolveNthWeekday)((0, exports.academicCalendarYear)(rule.month, academicStartYear), rule.month, rule.ordinal, rule.weekday);
93
+ exports.surveyRuleToDate = surveyRuleToDate;
94
+ /** Resolve a rule to an "MM-DD" string for the academic year starting in academicStartYear. */
95
+ const surveyRuleToMMDD = (rule, academicStartYear) => {
96
+ const d = (0, exports.surveyRuleToDate)(rule, academicStartYear);
97
+ return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
98
+ };
99
+ exports.surveyRuleToMMDD = surveyRuleToMMDD;
100
+ /**
101
+ * "MM-DD" → the ISO datetime it falls on in the given academic year, at 08:00 UTC
102
+ * (when sends fire). Returns "" for a malformed or non-existent date (e.g. "02-30"),
103
+ * which callers treat as "skip this slot". Lifted verbatim from aspirations.ts.
104
+ */
105
+ const mmddToISO = (mmdd, academicStartYear) => {
106
+ if (!/^\d{2}-\d{2}$/.test(mmdd || ""))
107
+ return "";
108
+ const [mm, dd] = mmdd.split("-").map(Number);
109
+ const year = (0, exports.academicCalendarYear)(mm, academicStartYear);
110
+ const d = new Date(Date.UTC(year, mm - 1, dd, 8, 0, 0));
111
+ if (d.getUTCMonth() !== mm - 1 || d.getUTCDate() !== dd)
112
+ return "";
113
+ return d.toISOString();
114
+ };
115
+ exports.mmddToISO = mmddToISO;
116
+ /**
117
+ * The MM-DD a slot fires on in a given academic year. A `rule` is the source of
118
+ * truth and is re-resolved; a slot with no rule keeps its fixed sendMMDD.
119
+ */
120
+ const slotMMDDForYear = (slot, academicStartYear) => slot.rule ? (0, exports.surveyRuleToMMDD)(slot.rule, academicStartYear) : slot.sendMMDD;
121
+ exports.slotMMDDForYear = slotMMDDForYear;
122
+ /**
123
+ * Resolve a year's programme of slots to concrete dates, dropping any whose date
124
+ * doesn't exist. Replaces the per-caller `refreshSlot` in provisionDueSkillsSurveys
125
+ * and the inline slot loop in _provisionDestinationsYear.
126
+ *
127
+ * Slots are returned in send order. Disabled slots are INCLUDED (with enabled:false)
128
+ * so provisioning still creates their cycle with autoSend off — staff can see the
129
+ * survey on the timeline and send it by hand.
130
+ */
131
+ const resolveSlotsForYear = (slots, academicStartYear) => (slots || [])
132
+ .map((slot) => {
133
+ const sendMMDD = (0, exports.slotMMDDForYear)(slot, academicStartYear);
134
+ return { ...slot, sendMMDD, sendISO: (0, exports.mmddToISO)(sendMMDD, academicStartYear), enabled: slot.enabled !== false };
135
+ })
136
+ .filter((slot) => Boolean(slot.sendISO))
137
+ .sort((a, b) => a.sendISO.localeCompare(b.sendISO));
138
+ exports.resolveSlotsForYear = resolveSlotsForYear;
139
+ /**
140
+ * Is this slot's send day the given date? Compares MM-DD, resolving any rule against
141
+ * the academic year `date` sits in, which is how the daily crons decide what to fire.
142
+ *
143
+ * Deliberately ignores `enabled`: a disabled slot is still "due" (its cycle gets
144
+ * provisioned) — whether it SENDS is the cycle's autoSend flag, checked by the cron.
145
+ */
146
+ const isSlotDueOn = (slot, date = new Date()) => (0, exports.slotMMDDForYear)(slot, (0, exports.currentAcademicStartYear)(date)) === (0, exports.toMMDD)(date);
147
+ exports.isSlotDueOn = isSlotDueOn;
148
+ //# sourceMappingURL=schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","sourceRoot":"","sources":["../../src/surveys/schedule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const registry_1 = require("./registry");
4
+ const schedule_1 = require("./schedule");
5
+ const types_1 = require("./types");
6
+ describe("resolveNthWeekday", () => {
7
+ // June 2027: 1st is a Tuesday, so Mondays are 7, 14, 21, 28.
8
+ it("finds the first Monday of a month", () => {
9
+ expect((0, schedule_1.resolveNthWeekday)(2027, 6, "first", 1).getDate()).toEqual(7);
10
+ });
11
+ it("finds the last Monday of a month", () => {
12
+ expect((0, schedule_1.resolveNthWeekday)(2027, 6, "last", 1).getDate()).toEqual(28);
13
+ });
14
+ it("falls back to the last occurrence when there is no nth", () => {
15
+ // Only four Mondays in June 2027 — a "fourth" resolves to the 28th, and so
16
+ // must anything asking for more.
17
+ expect((0, schedule_1.resolveNthWeekday)(2027, 6, "fourth", 1).getDate()).toEqual(28);
18
+ });
19
+ it("handles February in a leap year", () => {
20
+ // Feb 2028 has 29 days; the 29th is a Tuesday, so the last Tuesday is the 29th.
21
+ expect((0, schedule_1.resolveNthWeekday)(2028, 2, "last", 2).getDate()).toEqual(29);
22
+ });
23
+ it("handles February in a non-leap year", () => {
24
+ // Feb 2027 ends on Sunday the 28th.
25
+ expect((0, schedule_1.resolveNthWeekday)(2027, 2, "last", 0).getDate()).toEqual(28);
26
+ });
27
+ });
28
+ describe("academic year boundaries", () => {
29
+ it("puts autumn months in the start year and spring months in the next", () => {
30
+ expect((0, schedule_1.academicCalendarYear)(9, 2026)).toEqual(2026);
31
+ expect((0, schedule_1.academicCalendarYear)(12, 2026)).toEqual(2026);
32
+ expect((0, schedule_1.academicCalendarYear)(1, 2026)).toEqual(2027);
33
+ expect((0, schedule_1.academicCalendarYear)(8, 2026)).toEqual(2027);
34
+ });
35
+ it("rolls the start year over in September", () => {
36
+ expect((0, schedule_1.currentAcademicStartYear)(new Date(2026, 7, 31))).toEqual(2025); // 31 Aug
37
+ expect((0, schedule_1.currentAcademicStartYear)(new Date(2026, 8, 1))).toEqual(2026); // 1 Sep
38
+ });
39
+ it("rolls the year LABEL over on the same 1 September boundary", () => {
40
+ expect((0, schedule_1.academicYearOf)(new Date(2026, 6, 31))).toEqual("2025-26"); // 31 Jul
41
+ expect((0, schedule_1.academicYearOf)(new Date(2026, 7, 1))).toEqual("2025-26"); // 1 Aug
42
+ expect((0, schedule_1.academicYearOf)(new Date(2026, 7, 31))).toEqual("2025-26"); // 31 Aug
43
+ expect((0, schedule_1.academicYearOf)(new Date(2026, 8, 1))).toEqual("2026-27"); // 1 Sep
44
+ expect((0, schedule_1.academicYearOf)(new Date(2026, 11, 31))).toEqual("2026-27"); // 31 Dec
45
+ });
46
+ it("labels every date with the year its start year labels", () => {
47
+ // The single boundary means these two can never disagree — the property the
48
+ // pre-engine August/September split broke for August sends.
49
+ [new Date(2026, 7, 29), new Date(2026, 8, 1), new Date(2027, 0, 15), new Date(2027, 5, 7)]
50
+ .forEach((date) => expect((0, schedule_1.academicYearOf)(date)).toEqual((0, schedule_1.academicYearLabel)((0, schedule_1.currentAcademicStartYear)(date))));
51
+ });
52
+ it("formats both label styles", () => {
53
+ expect((0, schedule_1.academicYearLabel)(2026)).toEqual("2026-27");
54
+ expect((0, schedule_1.academicYearSlashLabel)(2026)).toEqual("2026/27");
55
+ expect((0, schedule_1.normaliseAcademicYear)("2026/27")).toEqual("2026-27");
56
+ expect((0, schedule_1.normaliseAcademicYear)("2026-27")).toEqual("2026-27");
57
+ });
58
+ });
59
+ describe("surveyRuleToMMDD", () => {
60
+ it("resolves a rule into the right calendar year for the academic year", () => {
61
+ // "first Monday of June" for 2026/27 lands in June 2027.
62
+ expect((0, schedule_1.surveyRuleToDate)({ ordinal: "first", weekday: 1, month: 6 }, 2026).getFullYear()).toEqual(2027);
63
+ expect((0, schedule_1.surveyRuleToMMDD)({ ordinal: "first", weekday: 1, month: 6 }, 2026)).toEqual("06-07");
64
+ });
65
+ it("keeps an autumn rule inside the start year", () => {
66
+ // "first Monday of September" for 2026/27 is 7 September 2026.
67
+ expect((0, schedule_1.surveyRuleToDate)({ ordinal: "first", weekday: 1, month: 9 }, 2026).getFullYear()).toEqual(2026);
68
+ expect((0, schedule_1.surveyRuleToMMDD)({ ordinal: "first", weekday: 1, month: 9 }, 2026)).toEqual("09-07");
69
+ });
70
+ it("zero-pads single-digit months and days", () => {
71
+ expect((0, schedule_1.surveyRuleToMMDD)({ ordinal: "first", weekday: 5, month: 1 }, 2026)).toEqual("01-01"); // 1 Jan 2027 is a Friday
72
+ });
73
+ });
74
+ describe("mmddToISO", () => {
75
+ it("sends at 08:00 UTC on the day", () => {
76
+ expect((0, schedule_1.mmddToISO)("09-15", 2026)).toEqual("2026-09-15T08:00:00.000Z");
77
+ });
78
+ it("crosses the year boundary for spring dates", () => {
79
+ expect((0, schedule_1.mmddToISO)("06-15", 2026)).toEqual("2027-06-15T08:00:00.000Z");
80
+ });
81
+ it("returns empty for dates that don't exist", () => {
82
+ expect((0, schedule_1.mmddToISO)("02-30", 2026)).toEqual("");
83
+ expect((0, schedule_1.mmddToISO)("13-01", 2026)).toEqual("");
84
+ expect((0, schedule_1.mmddToISO)("6-15", 2026)).toEqual("");
85
+ expect((0, schedule_1.mmddToISO)("", 2026)).toEqual("");
86
+ });
87
+ it("accepts 29 February only in a leap year", () => {
88
+ expect((0, schedule_1.mmddToISO)("02-29", 2027)).toEqual("2028-02-29T08:00:00.000Z");
89
+ expect((0, schedule_1.mmddToISO)("02-29", 2026)).toEqual("");
90
+ });
91
+ });
92
+ describe("resolveSlotsForYear", () => {
93
+ const rule = { rule: { ordinal: "first", weekday: 1, month: 6 }, sendMMDD: "06-01", label: "Summer" };
94
+ const legacy = { sendMMDD: "10-01", label: "Autumn" };
95
+ it("re-resolves a rule and leaves a legacy sendMMDD-only slot alone", () => {
96
+ const [autumn, summer] = (0, schedule_1.resolveSlotsForYear)([rule, legacy], 2026);
97
+ expect(autumn.sendMMDD).toEqual("10-01");
98
+ expect(summer.sendMMDD).toEqual("06-07"); // rule wins over the stale 06-01
99
+ });
100
+ it("returns slots in send order across the academic year", () => {
101
+ expect((0, schedule_1.resolveSlotsForYear)([rule, legacy], 2026).map((s) => s.label)).toEqual(["Autumn", "Summer"]);
102
+ });
103
+ it("includes disabled slots, flagged", () => {
104
+ const slots = (0, schedule_1.resolveSlotsForYear)([{ sendMMDD: "10-01", enabled: false }, { sendMMDD: "11-01" }], 2026);
105
+ expect(slots.map((s) => s.enabled)).toEqual([false, true]);
106
+ });
107
+ it("drops slots whose date doesn't exist", () => {
108
+ expect((0, schedule_1.resolveSlotsForYear)([{ sendMMDD: "02-30" }, { sendMMDD: "11-01" }], 2026)).toHaveLength(1);
109
+ });
110
+ it("handles no slots", () => {
111
+ expect((0, schedule_1.resolveSlotsForYear)(undefined, 2026)).toEqual([]);
112
+ });
113
+ });
114
+ describe("isSlotDueOn", () => {
115
+ it("is due on the rule's resolved date, not the stale stored one", () => {
116
+ const slot = { rule: { ordinal: "first", weekday: 1, month: 6 }, sendMMDD: "06-01" };
117
+ expect((0, schedule_1.isSlotDueOn)(slot, new Date(2027, 5, 7))).toEqual(true); // 7 Jun 2027
118
+ expect((0, schedule_1.isSlotDueOn)(slot, new Date(2027, 5, 1))).toEqual(false);
119
+ });
120
+ it("is due on a legacy slot's fixed date", () => {
121
+ expect((0, schedule_1.isSlotDueOn)({ sendMMDD: "10-01" }, new Date(2026, 9, 1))).toEqual(true);
122
+ });
123
+ it("ignores `enabled` — a disabled slot is still due, and simply doesn't auto-send", () => {
124
+ expect((0, schedule_1.isSlotDueOn)({ sendMMDD: "10-01", enabled: false }, new Date(2026, 9, 1))).toEqual(true);
125
+ });
126
+ it("agrees with toMMDD", () => {
127
+ const date = new Date(2026, 8, 3);
128
+ expect((0, schedule_1.isSlotDueOn)({ sendMMDD: (0, schedule_1.toMMDD)(date) }, date)).toEqual(true);
129
+ });
130
+ });
131
+ describe("survey registry", () => {
132
+ it("describes every kind", () => {
133
+ expect(Object.keys(registry_1.SURVEY_KINDS).sort()).toEqual([...types_1.SURVEY_KINDS_LIST].sort());
134
+ });
135
+ it.each(types_1.SURVEY_KINDS_LIST)("%s is fully described", (kind) => {
136
+ const def = registry_1.SURVEY_KINDS[kind];
137
+ expect(def.kind).toEqual(kind);
138
+ expect(def.label).toBeTruthy();
139
+ expect(["account", "magicLink"]).toContain(def.authMode);
140
+ expect(["rater", "fallback", "none"]).toContain(def.parentMode);
141
+ expect(def.raters.students || def.raters.parents || def.raters.employers || def.raters.trustedContact).toEqual(true);
142
+ expect(def.legacy.collectionLink).toBeTruthy();
143
+ expect(def.formPath({ kind, requestId: "abc" })).toContain("abc");
144
+ });
145
+ it.each(types_1.SURVEY_KINDS_LIST)("%s has invite copy for every rater it asks", (kind) => {
146
+ const def = registry_1.SURVEY_KINDS[kind];
147
+ const audiences = [
148
+ def.raters.students ? "student" : undefined,
149
+ def.raters.parents ? "parent" : undefined,
150
+ def.raters.employers ? "employer" : undefined,
151
+ def.raters.trustedContact ? "trustedContact" : undefined,
152
+ ].filter(Boolean);
153
+ const ctx = { instituteName: "Test School", forename: "Sam", studentForename: "Alex", studentSurname: "Doe", surveyLabel: def.label };
154
+ audiences.forEach((audience) => {
155
+ const copy = (0, registry_1.surveyEmailCopy)(kind, audience, "invite");
156
+ expect(copy).toBeDefined();
157
+ expect(copy?.subject(ctx)).toBeTruthy();
158
+ expect(copy?.body(ctx)).toBeTruthy();
159
+ expect(copy?.button).toBeTruthy();
160
+ });
161
+ });
162
+ it("has unique message types across kinds", () => {
163
+ const all = types_1.SURVEY_KINDS_LIST.flatMap((k) => Object.values(registry_1.SURVEY_KINDS[k].messageTypes));
164
+ expect(new Set(all).size).toEqual(all.length);
165
+ });
166
+ it("orders the leaver thread and supersedes forwards only", () => {
167
+ expect(registry_1.SURVEY_THREADS.leaver).toEqual(["leaverIntentions", "leaverResults", "leaverChristmas"]);
168
+ expect((0, registry_1.supersedingSurveyKinds)("leaverIntentions")).toEqual(["leaverResults", "leaverChristmas"]);
169
+ expect((0, registry_1.supersedingSurveyKinds)("leaverResults")).toEqual(["leaverChristmas"]);
170
+ expect((0, registry_1.supersedingSurveyKinds)("leaverChristmas")).toEqual([]);
171
+ });
172
+ it("derives the same supersession lists the hand-written constants held", () => {
173
+ // notifications/leavers.ts SUPERSEDES_INTENTIONS / SUPERSEDES_RESULTS.
174
+ expect((0, registry_1.legacySupersedingMessageTypes)("leaverIntentions")).toEqual([
175
+ "leaverResultsInvite", "leaverResultsReminder", "leaverChristmasInvite", "leaverChristmasReminder",
176
+ ]);
177
+ expect((0, registry_1.legacySupersedingMessageTypes)("leaverResults")).toEqual([
178
+ "leaverChristmasInvite", "leaverChristmasReminder",
179
+ ]);
180
+ expect((0, registry_1.legacySupersedingMessageTypes)("leaverChristmas")).toEqual([]);
181
+ });
182
+ it("supersedes nothing for kinds outside a thread", () => {
183
+ expect((0, registry_1.supersedingMessageTypes)("aspirations")).toEqual([]);
184
+ expect((0, registry_1.supersedingMessageTypes)("skills")).toEqual([]);
185
+ expect((0, registry_1.supersedingMessageTypes)("alumniCheckIn")).toEqual([]);
186
+ });
187
+ it("gives thread kinds new-format supersession lists too", () => {
188
+ expect((0, registry_1.supersedingMessageTypes)("leaverResults")).toEqual([
189
+ "surveyLeaverChristmasInvite", "surveyLeaverChristmasReminder",
190
+ ]);
191
+ });
192
+ it("keeps the migrated wording of the emails it replaces", () => {
193
+ const ctx = { instituteName: "Test School", forename: "Sam" };
194
+ expect((0, registry_1.surveyEmailCopy)("aspirations", "student", "invite")?.subject(ctx))
195
+ .toEqual("Test School — share your career aspirations");
196
+ expect((0, registry_1.surveyEmailCopy)("leaverIntentions", "student", "invite")?.subject(ctx))
197
+ .toEqual("Test School — tell us your next step");
198
+ expect((0, registry_1.surveyEmailCopy)("leaverResults", "student", "invite")?.title(ctx))
199
+ .toEqual("Congratulations on finishing!");
200
+ expect((0, registry_1.surveyEmailCopy)("leaverChristmas", "student", "invite")?.button).toEqual("Quick check-in");
201
+ expect((0, registry_1.surveyEmailCopy)("alumniCheckIn", "student", "invite")?.secondaryButton?.title)
202
+ .toEqual("I'm in the same place");
203
+ expect((0, registry_1.surveyEmailCopy)("skills", "student", "invite")?.subject(ctx))
204
+ .toEqual("[Action Required] Complete Skills Assessment");
205
+ });
206
+ it("has no reminder for the kinds that never sent one", () => {
207
+ expect((0, registry_1.surveyEmailCopy)("skills", "student", "reminder")).toBeUndefined();
208
+ expect((0, registry_1.surveyEmailCopy)("alumniCheckIn", "student", "reminder")).toBeUndefined();
209
+ expect(registry_1.SURVEY_KINDS.skills.reminderAfterDays).toBeUndefined();
210
+ expect(registry_1.SURVEY_KINDS.alumniCheckIn.reminderAfterDays).toBeUndefined();
211
+ });
212
+ it("keeps today's reminder cadences", () => {
213
+ expect(registry_1.SURVEY_KINDS.aspirations.reminderAfterDays).toEqual(14);
214
+ expect(registry_1.SURVEY_KINDS.leaverIntentions.reminderAfterDays).toEqual(14);
215
+ expect(registry_1.SURVEY_KINDS.leaverResults.reminderAfterDays).toEqual(7);
216
+ expect(registry_1.SURVEY_KINDS.leaverChristmas.reminderAfterDays).toEqual(7);
217
+ });
218
+ it("falls back to the shared parent template only for parent raters", () => {
219
+ expect((0, registry_1.surveyEmailCopy)("aspirations", "parent", "invite")?.button).toEqual("Complete the form");
220
+ // Skills overrides it with its own pre-existing wording.
221
+ expect((0, registry_1.surveyEmailCopy)("skills", "parent", "invite")?.button).toEqual("Complete Assessment");
222
+ // Alumni check-ins contact the trusted contact as a fallback, never as a rater.
223
+ expect((0, registry_1.surveyEmailCopy)("alumniCheckIn", "parent", "invite")).toBeUndefined();
224
+ });
225
+ it("builds the legacy URLs live emails already point at", () => {
226
+ expect(registry_1.SURVEY_KINDS.aspirations.legacyFormPath?.({ kind: "aspirations", requestId: "r1" }))
227
+ .toEqual("/app/destinations/students/form/r1");
228
+ expect(registry_1.SURVEY_KINDS.leaverIntentions.legacyFormPath?.({ kind: "leaverIntentions", requestId: "r1" }))
229
+ .toEqual("/institutes/leaver/r1");
230
+ expect(registry_1.SURVEY_KINDS.leaverResults.legacyFormPath?.({ kind: "leaverResults", requestId: "r1", token: "t1" }))
231
+ .toEqual("/leaverCheckpoint/r1/resultsDay/t1");
232
+ expect(registry_1.SURVEY_KINDS.alumniCheckIn.legacyFormPath?.({ kind: "alumniCheckIn", requestId: "r1", alumniId: "a1", token: "t1" }))
233
+ .toEqual("/alumniCheckIn/a1/t1");
234
+ expect(registry_1.SURVEY_KINDS.skills.legacyFormPath?.({ kind: "skills", requestId: "r1", rater: "students", assessmentId: "s1" }))
235
+ .toEqual("/institutes/skills/s1");
236
+ expect(registry_1.SURVEY_KINDS.skills.legacyFormPath?.({ kind: "skills", requestId: "r1", rater: "parents", oId: "o1", assessmentId: "s1", externalKey: "k1" }))
237
+ .toEqual("/skillsRequest/o1/s1/k1");
238
+ });
239
+ });
240
+ //# sourceMappingURL=schedule.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.test.js","sourceRoot":"","sources":["../../src/surveys/schedule.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,204 @@
1
+ /**
2
+ * surveys/types.ts — the unified survey engine's data model.
3
+ *
4
+ * One shape for every student/alumni survey CareerThread sends: skills surveys,
5
+ * careers aspiration surveys (Form A), leaver transition forms + their post-leaving
6
+ * checkpoints (Form B), and annual alumni check-ins. Each is a `SurveyKind`; the
7
+ * behavioural differences between them live in the registry (./registry), not in
8
+ * the data model.
9
+ *
10
+ * Firestore:
11
+ * surveyCycles/{cycleId} — one scheduled SEND to one cohort
12
+ * surveyRequests/{requestId} — one PERSON inside one cycle
13
+ *
14
+ * These supersede aspirationCycles/aspirationRequests, leaverGroups/leaverRequests,
15
+ * skillsAssessmentRequests, and the per-Alumni check-in fields. Migration mapping is
16
+ * documented per field below and in SURVEY_UNIFICATION_PLAN.md §3.2.
17
+ */
18
+ import type { SurveyDateRule } from "../typeDefinitions";
19
+ /**
20
+ * The six survey kinds the engine serves. The three leaver kinds are separate
21
+ * kinds that share a `threadKey`, so a leaver's journey is three cycles/requests
22
+ * rather than one request doc with inline checkpoint state.
23
+ */
24
+ export type SurveyKind = "skills" | "aspirations" | "leaverIntentions" | "leaverResults" | "leaverChristmas" | "alumniCheckIn";
25
+ /** Every SurveyKind as a runtime array — for iteration, validation and registry tests. */
26
+ export declare const SURVEY_KINDS_LIST: SurveyKind[];
27
+ /** Who can answer a survey. Mirrors SkillRater plus the alumni trusted-contact fallback. */
28
+ export type SurveyRaterType = "students" | "parents" | "employers" | "trustedContact";
29
+ /** Which rater tracks a cycle asks for. Replaces SkillAssessmentRequest's
30
+ * requestStudents/requestParents/requestEmployers trio and carries a slot's
31
+ * `parents` toggle onto the cycle it provisions. */
32
+ export type SurveyRaters = {
33
+ students?: boolean;
34
+ parents?: boolean;
35
+ employers?: boolean;
36
+ };
37
+ /**
38
+ * One scheduled survey in an annual programme — the unified replacement for
39
+ * SkillsSurveySlot and AspirationSurveySlot (both are now aliases of this).
40
+ *
41
+ * `rule` (when present) is the source of truth: a relative rule like "first Monday
42
+ * of June", resolved to a concrete date for each academic year at provision time.
43
+ * `sendMMDD` ("MM-DD") is the materialised date for the current academic year, and
44
+ * the sole value on legacy slots created before rules existed; provisioning keeps it
45
+ * in step with `rule`.
46
+ */
47
+ export type SurveySlot = {
48
+ sendMMDD: string;
49
+ rule?: SurveyDateRule;
50
+ label?: string;
51
+ /** Ask the student themselves. Absent = true (legacy aspiration slots had no flags). */
52
+ students?: boolean;
53
+ /** Ask the parent as well. Available on every kind whose registry entry allows a parent rater. */
54
+ parents?: boolean;
55
+ /** The per-survey auto-send switch. Absent = enabled; false means the cron
56
+ * provisions/keeps the cycle but never sends it without staff action. */
57
+ enabled?: boolean;
58
+ };
59
+ export type SurveyCycleStatus = "draft" | "scheduled" | "sending" | "active" | "closed";
60
+ export type SurveyRequestStatus = "pending" | "sent" | "completed" | "bounced";
61
+ /** Who submitted a response, as passed to submitSurveyResponse. */
62
+ export type SurveySubmittedBy = "student" | "parent" | "employer" | "staff" | "trustedContact";
63
+ /**
64
+ * One send to one cohort — AspirationCycle generalised across kinds.
65
+ *
66
+ * Legacy status mapping: LeaverGroup.stage "created"/"review" → "draft",
67
+ * "scheduled" → "scheduled", "sent" → "active", "archived" → "closed";
68
+ * SkillAssessmentRequest.status "draft"/"scheduled" map directly and "sent" → "active".
69
+ *
70
+ * Stored in collection: surveyCycles
71
+ */
72
+ export type SurveyCycle = {
73
+ oId: string;
74
+ kind: SurveyKind;
75
+ /** e.g. "2026-27". Always the hyphen form here — see academicYearOf in ./schedule.
76
+ * Legacy leaverGroups used the slash form ("2026/27") and are converted on migration. */
77
+ academicYear: string;
78
+ /** The single year group this cycle targets. Absent for alumni check-in cycles,
79
+ * which are institute-wide. Replaces AspirationCycle.yearGroupsIncluded. */
80
+ yearGroup?: number;
81
+ /** SkillsMapping.id ("all" for the All students group) — skills cycles only. */
82
+ groupId?: string;
83
+ /** Chains the cycles of one journey: the three leaver kinds for a cohort share
84
+ * one threadKey (the pre-migration leaverGroups id). Also the supersession key. */
85
+ threadKey?: string;
86
+ status: SurveyCycleStatus;
87
+ /** Staff-facing name. From SkillAssessmentRequest.title / LeaverGroup.title;
88
+ * derived from the kind label + slot label for cycles that had none. */
89
+ title?: string;
90
+ description?: string;
91
+ /** ISO datetime — when invites fire. */
92
+ scheduledSendDate: string;
93
+ /** ISO datetime — form links expire. Defaults to scheduledSendDate + closeAfterDays. */
94
+ closeDate?: string;
95
+ /** Default 21. */
96
+ closeAfterDays?: number;
97
+ /** False = the cron never sends this cycle; staff must press send. Set from the
98
+ * provisioning slot's `enabled` flag and toggled by setSurveyAutoSend. */
99
+ autoSend: boolean;
100
+ /** Which rater tracks this cycle asks for. Absent = students only. */
101
+ raters?: SurveyRaters;
102
+ /** ISO — staff heads-up guards, so each fires at most once per cycle. */
103
+ headsUp7SentAt?: string;
104
+ headsUp1SentAt?: string;
105
+ /** ISO — when invites actually fired. */
106
+ sentAt?: string;
107
+ closedAt?: string;
108
+ /** ISO — day-N reminder fire time. */
109
+ reminderSentAt?: string;
110
+ /** Count at send time. */
111
+ totalInvited: number;
112
+ /** Incremented on each completion. */
113
+ totalResponded: number;
114
+ /** Staff uid, or "system" for cron-provisioned cycles. */
115
+ createdBy: string;
116
+ createdAt: string;
117
+ /** Optional custom email template; falls back to the registry copy. */
118
+ emailTemplateId?: string;
119
+ /** JSON-serialised DataViewerSelectionBackend — the staff-picked recipients, or
120
+ * the rule to re-resolve them at send. Was `studentSelection` on AspirationCycle
121
+ * and `students` on SkillAssessmentRequest / LeaverGroup. */
122
+ studentSelection?: string;
123
+ /** True on auto-provisioned cycles — requests are fanned out from studentSelection
124
+ * when emails fire, not at creation, so the cohort tracks the live roster. */
125
+ resolveRecipientsAtSend?: boolean;
126
+ /** Created by surveyCron from institute config rather than by staff. */
127
+ autoProvisioned?: boolean;
128
+ /** Pre-aggregated response data for the dashboard: AspirationAggregates,
129
+ * LeaverAggregates or a kind-specific shape. Typed per kind by the engine's
130
+ * handler map — the aggregate shapes themselves are unchanged. */
131
+ aggregates?: unknown;
132
+ /** Kind-specific cycle settings that don't generalise, e.g. leaverIntentions'
133
+ * allStudentsLeave or a skills cycle's group name. Read only by that kind's handler. */
134
+ kindConfig?: {
135
+ [key: string]: unknown;
136
+ };
137
+ };
138
+ /**
139
+ * One person inside one cycle — AspirationRequest generalised, with the leaver
140
+ * magic-link fields and a parent track folded in.
141
+ *
142
+ * `status`/`completed` are the STUDENT (or, for alumniCheckIn, the alumnus's) track.
143
+ * `completed` stays a denormalised boolean so the compound queries and Algolia
144
+ * facets the staff pages rely on keep working unchanged.
145
+ *
146
+ * Skills employer raters are NOT mirrored here: per plan decision D4 they remain
147
+ * tracked solely as skillAssessments docs, which stay the skills response store.
148
+ *
149
+ * Stored in collection: surveyRequests
150
+ */
151
+ export type SurveyRequest = {
152
+ oId: string;
153
+ cycleId: string;
154
+ kind: SurveyKind;
155
+ academicYear: string;
156
+ /** Student uid — matched against context.auth.uid on account-auth kinds.
157
+ * Absent on alumniCheckIn requests for alumni with no linked account. */
158
+ uid?: string;
159
+ /** alumni/{alumniId} — set on alumniCheckIn requests, and on leaver requests
160
+ * once the leaver opted in and an Alumni record was created. */
161
+ alumniId?: string;
162
+ /** Copied from the cycle so a request can be resolved without reading it. */
163
+ threadKey?: string;
164
+ /** MIS school ID. */
165
+ schoolId?: string;
166
+ forename: string;
167
+ /** Optional — legacy requests predate this field. */
168
+ surname?: string;
169
+ email: string;
170
+ /** Post-leaving contact address; where invites go once the school email dies. */
171
+ personalEmail?: string;
172
+ yearGroup?: number;
173
+ status: SurveyRequestStatus;
174
+ completed: boolean;
175
+ /** ISO — when the invite was emailed. */
176
+ sentAt?: string;
177
+ completedAt?: string;
178
+ /** ISO — staff manual resend guard (1 resend/hour). */
179
+ lastResentAt?: string;
180
+ /** Magic-link auth for kinds whose registry authMode is "magicLink".
181
+ * Alumni linkCode/linkCodeExpiry migrate onto these fields. */
182
+ token?: string;
183
+ tokenExpiry?: string;
184
+ /** The kind's response payload: AspirationEntry, LeaverTransitionData,
185
+ * LeaverCheckpointResponse, CheckInEntry, or a skills score map. Left unknown
186
+ * here so core stays free of per-kind unions; narrowed by the kind's handler. */
187
+ response?: unknown;
188
+ /** True when a staff member submitted on the person's behalf. */
189
+ completedByStaff?: boolean;
190
+ completedByStaffId?: string;
191
+ /** Set when a newer survey to this person made chasing this one pointless
192
+ * (plan decision D8). Reminders stop; the staff timeline renders it "skipped". */
193
+ supersededAt?: string;
194
+ supersededByCycleId?: string;
195
+ parentEmail?: string;
196
+ parentStatus?: SurveyRequestStatus;
197
+ parentCompleted?: boolean;
198
+ parentSentAt?: string;
199
+ parentCompletedAt?: string;
200
+ parentResponse?: unknown;
201
+ /** Magic-link token for the parent's copy of the form — parents never have accounts. */
202
+ parentToken?: string;
203
+ parentTokenExpiry?: string;
204
+ };