placementt-core 1.400.1111 → 1.400.1113

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.
@@ -19,7 +19,8 @@ describe("callable registry", () => {
19
19
  });
20
20
  it("names every callable as `<module>-<export>`", () => {
21
21
  for (const name of names) {
22
- expect(name).toMatch(/^[a-zA-Z]+-[a-zA-Z]+$/);
22
+ // Letters first; digits allowed after, for modules like `e2e`.
23
+ expect(name).toMatch(/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z][a-zA-Z0-9]*$/);
23
24
  }
24
25
  });
25
26
  it("gives every callable a request and response zod schema", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../../../src/callables/__tests__/registry.test.ts"],"names":[],"mappings":";;AAAA,6BAAsB;AACtB,0CAAyD;AAEzD;;;;;GAKG;AACH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,uBAAY,CAAC,CAAC;IAExC,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACpD,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,yBAAc,CAAC;aAC9C,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAChE,wEAAwE;QACxE,sEAAsE;QACtE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACnD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC;QAClD,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAC9D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,uBAAY,CAAC,IAAiC,CAAC,CAAC;YACjE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,OAAC,CAAC,OAAO,CAAC,CAAC;YACnD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,cAAc,CAAC,OAAC,CAAC,OAAO,CAAC,CAAC;QACxD,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../../../src/callables/__tests__/registry.test.ts"],"names":[],"mappings":";;AAAA,6BAAsB;AACtB,0CAAyD;AAEzD;;;;;GAKG;AACH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,uBAAY,CAAC,CAAC;IAExC,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACpD,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,yBAAc,CAAC;aAC9C,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAChE,wEAAwE;QACxE,sEAAsE;QACtE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACnD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,+DAA+D;YAC/D,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,6CAA6C,CAAC,CAAC;QACxE,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAC9D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,uBAAY,CAAC,IAAiC,CAAC,CAAC;YACjE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,OAAC,CAAC,OAAO,CAAC,CAAC;YACnD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,cAAc,CAAC,OAAC,CAAC,OAAO,CAAC,CAAC;QACxD,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Contracts for the end-to-end and UX-audit harness
4
+ * (`Placementt-Backend/functions/src/e2e.ts`).
5
+ *
6
+ * `redeemSignInLink` is public: the token is the credential. Links are only
7
+ * ever minted by the harness (service account, straight into Firestore) and
8
+ * only ever sign in a reserved `e2e.*@careerthread.co.uk` user, so the UX audit
9
+ * can walk a registered school without anyone typing a password.
10
+ */
11
+ export declare const e2eCallables: {
12
+ "e2e-redeemSignInLink": {
13
+ request: z.ZodObject<{
14
+ /** The raw token from `/e2eSignIn/:token`. Stored only as a SHA-256 hash. */
15
+ token: z.ZodString;
16
+ }, "strip", z.ZodTypeAny, {
17
+ token: string;
18
+ }, {
19
+ token: string;
20
+ }>;
21
+ response: z.ZodObject<{
22
+ code: z.ZodEnum<["ok", "notFound", "expired"]>;
23
+ /** Firebase custom token for the linked e2e user. Only set when code is "ok". */
24
+ authToken: z.ZodOptional<z.ZodString>;
25
+ }, "strip", z.ZodTypeAny, {
26
+ code: "expired" | "ok" | "notFound";
27
+ authToken?: string | undefined;
28
+ }, {
29
+ code: "expired" | "ok" | "notFound";
30
+ authToken?: string | undefined;
31
+ }>;
32
+ };
33
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.e2eCallables = void 0;
4
+ const zod_1 = require("zod");
5
+ /**
6
+ * Contracts for the end-to-end and UX-audit harness
7
+ * (`Placementt-Backend/functions/src/e2e.ts`).
8
+ *
9
+ * `redeemSignInLink` is public: the token is the credential. Links are only
10
+ * ever minted by the harness (service account, straight into Firestore) and
11
+ * only ever sign in a reserved `e2e.*@careerthread.co.uk` user, so the UX audit
12
+ * can walk a registered school without anyone typing a password.
13
+ */
14
+ exports.e2eCallables = {
15
+ "e2e-redeemSignInLink": {
16
+ request: zod_1.z.object({
17
+ /** The raw token from `/e2eSignIn/:token`. Stored only as a SHA-256 hash. */
18
+ token: zod_1.z.string().min(32).max(128).regex(/^[A-Za-z0-9_-]+$/),
19
+ }),
20
+ response: zod_1.z.object({
21
+ code: zod_1.z.enum(["ok", "notFound", "expired"]),
22
+ /** Firebase custom token for the linked e2e user. Only set when code is "ok". */
23
+ authToken: zod_1.z.string().optional(),
24
+ }),
25
+ },
26
+ };
27
+ //# sourceMappingURL=e2e.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e.js","sourceRoot":"","sources":["../../src/callables/e2e.ts"],"names":[],"mappings":";;;AAAA,6BAAsB;AAGtB;;;;;;;;GAQG;AACU,QAAA,YAAY,GAAG;IACxB,sBAAsB,EAAE;QACpB,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,6EAA6E;YAC7E,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC;SAC/D,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;YAC3C,iFAAiF;YACjF,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACnC,CAAC;KACL;CACoB,CAAC"}
@@ -9,3 +9,5 @@ export * from "./he";
9
9
  export * from "./studentLaunches";
10
10
  export * from "./billing";
11
11
  export * from "./hubImpactReport";
12
+ export * from "./e2e";
13
+ export * from "./wonde";
@@ -25,4 +25,6 @@ __exportStar(require("./he"), exports);
25
25
  __exportStar(require("./studentLaunches"), exports);
26
26
  __exportStar(require("./billing"), exports);
27
27
  __exportStar(require("./hubImpactReport"), exports);
28
+ __exportStar(require("./e2e"), exports);
29
+ __exportStar(require("./wonde"), exports);
28
30
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/callables/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAwB;AACxB,6CAA2B;AAC3B,uDAAqC;AACrC,8CAA4B;AAC5B,2CAAyB;AACzB,6CAA2B;AAC3B,oDAAkC;AAClC,uCAAqB;AACrB,oDAAkC;AAClC,4CAA0B;AAC1B,oDAAkC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/callables/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAwB;AACxB,6CAA2B;AAC3B,uDAAqC;AACrC,8CAA4B;AAC5B,2CAAyB;AACzB,6CAA2B;AAC3B,oDAAkC;AAClC,uCAAqB;AACrB,oDAAkC;AAClC,4CAA0B;AAC1B,oDAAkC;AAClC,wCAAsB;AACtB,0CAAwB"}
@@ -11,6 +11,47 @@ import { z } from "zod";
11
11
  * equals the sum of the group key counts.
12
12
  */
13
13
  export declare const allCallables: {
14
+ "wonde-requestConnection": {
15
+ request: z.ZodObject<{
16
+ oId: z.ZodString;
17
+ itManagerName: z.ZodString;
18
+ itManagerEmail: z.ZodString;
19
+ }, "strip", z.ZodTypeAny, {
20
+ oId: string;
21
+ itManagerName: string;
22
+ itManagerEmail: string;
23
+ }, {
24
+ oId: string;
25
+ itManagerName: string;
26
+ itManagerEmail: string;
27
+ }>;
28
+ response: z.ZodObject<{
29
+ taskId: z.ZodString;
30
+ }, "strip", z.ZodTypeAny, {
31
+ taskId: string;
32
+ }, {
33
+ taskId: string;
34
+ }>;
35
+ };
36
+ "e2e-redeemSignInLink": {
37
+ request: z.ZodObject<{
38
+ token: z.ZodString;
39
+ }, "strip", z.ZodTypeAny, {
40
+ token: string;
41
+ }, {
42
+ token: string;
43
+ }>;
44
+ response: z.ZodObject<{
45
+ code: z.ZodEnum<["ok", "notFound", "expired"]>;
46
+ authToken: z.ZodOptional<z.ZodString>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ code: "expired" | "ok" | "notFound";
49
+ authToken?: string | undefined;
50
+ }, {
51
+ code: "expired" | "ok" | "notFound";
52
+ authToken?: string | undefined;
53
+ }>;
54
+ };
14
55
  "hubImpactReport-createHubImpactReport": {
15
56
  request: z.ZodObject<{
16
57
  oId: z.ZodOptional<z.ZodString>;
@@ -2515,4 +2556,49 @@ export declare const callableGroups: {
2515
2556
  response: z.ZodString;
2516
2557
  };
2517
2558
  };
2559
+ e2eCallables: {
2560
+ "e2e-redeemSignInLink": {
2561
+ request: z.ZodObject<{
2562
+ token: z.ZodString;
2563
+ }, "strip", z.ZodTypeAny, {
2564
+ token: string;
2565
+ }, {
2566
+ token: string;
2567
+ }>;
2568
+ response: z.ZodObject<{
2569
+ code: z.ZodEnum<["ok", "notFound", "expired"]>;
2570
+ authToken: z.ZodOptional<z.ZodString>;
2571
+ }, "strip", z.ZodTypeAny, {
2572
+ code: "expired" | "ok" | "notFound";
2573
+ authToken?: string | undefined;
2574
+ }, {
2575
+ code: "expired" | "ok" | "notFound";
2576
+ authToken?: string | undefined;
2577
+ }>;
2578
+ };
2579
+ };
2580
+ wondeCallables: {
2581
+ "wonde-requestConnection": {
2582
+ request: z.ZodObject<{
2583
+ oId: z.ZodString;
2584
+ itManagerName: z.ZodString;
2585
+ itManagerEmail: z.ZodString;
2586
+ }, "strip", z.ZodTypeAny, {
2587
+ oId: string;
2588
+ itManagerName: string;
2589
+ itManagerEmail: string;
2590
+ }, {
2591
+ oId: string;
2592
+ itManagerName: string;
2593
+ itManagerEmail: string;
2594
+ }>;
2595
+ response: z.ZodObject<{
2596
+ taskId: z.ZodString;
2597
+ }, "strip", z.ZodTypeAny, {
2598
+ taskId: string;
2599
+ }, {
2600
+ taskId: string;
2601
+ }>;
2602
+ };
2603
+ };
2518
2604
  };
@@ -13,6 +13,8 @@ const studentLaunches_1 = require("./studentLaunches");
13
13
  const billing_1 = require("./billing");
14
14
  const jobRoles_1 = require("./jobRoles");
15
15
  const hubImpactReport_1 = require("./hubImpactReport");
16
+ const e2e_1 = require("./e2e");
17
+ const wonde_1 = require("./wonde");
16
18
  /**
17
19
  * The single source of truth for every typed callable. Add a feature group's
18
20
  * spread here and its callables are automatically covered by:
@@ -37,7 +39,9 @@ exports.allCallables = {
37
39
  ...billing_1.billingCallables,
38
40
  ...jobRoles_1.jobRolesCallables,
39
41
  ...hubImpactReport_1.hubImpactReportCallables,
42
+ ...e2e_1.e2eCallables,
43
+ ...wonde_1.wondeCallables,
40
44
  };
41
45
  /** The per-group maps, exported so tests can assert no key collisions. */
42
- exports.callableGroups = { destinationsConfigCallables: destinationsConfig_1.destinationsConfigCallables, placementCallables: placement_1.placementCallables, eventsCallables: events_1.eventsCallables, surveyCallables: surveys_1.surveyCallables, checksCallables: checks_1.checksCallables, benchmarksCallables: benchmarks_1.benchmarksCallables, apprenticeshipsCallables: apprenticeships_1.apprenticeshipsCallables, heCallables: he_1.heCallables, studentLaunchCallables: studentLaunches_1.studentLaunchCallables, billingCallables: billing_1.billingCallables, jobRolesCallables: jobRoles_1.jobRolesCallables, hubImpactReportCallables: hubImpactReport_1.hubImpactReportCallables };
46
+ exports.callableGroups = { destinationsConfigCallables: destinationsConfig_1.destinationsConfigCallables, placementCallables: placement_1.placementCallables, eventsCallables: events_1.eventsCallables, surveyCallables: surveys_1.surveyCallables, checksCallables: checks_1.checksCallables, benchmarksCallables: benchmarks_1.benchmarksCallables, apprenticeshipsCallables: apprenticeships_1.apprenticeshipsCallables, heCallables: he_1.heCallables, studentLaunchCallables: studentLaunches_1.studentLaunchCallables, billingCallables: billing_1.billingCallables, jobRolesCallables: jobRoles_1.jobRolesCallables, hubImpactReportCallables: hubImpactReport_1.hubImpactReportCallables, e2eCallables: e2e_1.e2eCallables, wondeCallables: wonde_1.wondeCallables };
43
47
  //# sourceMappingURL=registry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/callables/registry.ts"],"names":[],"mappings":";;;AAEA,6DAAiE;AACjE,2CAA+C;AAC/C,qCAAyC;AACzC,uCAA0C;AAC1C,qCAAyC;AACzC,6CAAiD;AACjD,uDAA2D;AAC3D,6BAAiC;AACjC,uDAAyD;AACzD,uCAA2C;AAC3C,yCAA6C;AAC7C,uDAA2D;AAE3D;;;;;;;;;;GAUG;AACU,QAAA,YAAY,GAAG;IACxB,GAAG,gDAA2B;IAC9B,GAAG,8BAAkB;IACrB,GAAG,wBAAe;IAClB,GAAG,yBAAe;IAClB,GAAG,wBAAe;IAClB,GAAG,gCAAmB;IACtB,GAAG,0CAAwB;IAC3B,GAAG,gBAAW;IACd,GAAG,wCAAsB;IACzB,GAAG,0BAAgB;IACnB,GAAG,4BAAiB;IACpB,GAAG,0CAAwB;CACN,CAAC;AAiB1B,0EAA0E;AAC7D,QAAA,cAAc,GAAG,EAAC,2BAA2B,EAA3B,gDAA2B,EAAE,kBAAkB,EAAlB,8BAAkB,EAAE,eAAe,EAAf,wBAAe,EAAE,eAAe,EAAf,yBAAe,EAAE,eAAe,EAAf,wBAAe,EAAE,mBAAmB,EAAnB,gCAAmB,EAAE,wBAAwB,EAAxB,0CAAwB,EAAE,WAAW,EAAX,gBAAW,EAAE,sBAAsB,EAAtB,wCAAsB,EAAE,gBAAgB,EAAhB,0BAAgB,EAAE,iBAAiB,EAAjB,4BAAiB,EAAE,wBAAwB,EAAxB,0CAAwB,EAAC,CAAC"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/callables/registry.ts"],"names":[],"mappings":";;;AAEA,6DAAiE;AACjE,2CAA+C;AAC/C,qCAAyC;AACzC,uCAA0C;AAC1C,qCAAyC;AACzC,6CAAiD;AACjD,uDAA2D;AAC3D,6BAAiC;AACjC,uDAAyD;AACzD,uCAA2C;AAC3C,yCAA6C;AAC7C,uDAA2D;AAC3D,+BAAmC;AACnC,mCAAuC;AAEvC;;;;;;;;;;GAUG;AACU,QAAA,YAAY,GAAG;IACxB,GAAG,gDAA2B;IAC9B,GAAG,8BAAkB;IACrB,GAAG,wBAAe;IAClB,GAAG,yBAAe;IAClB,GAAG,wBAAe;IAClB,GAAG,gCAAmB;IACtB,GAAG,0CAAwB;IAC3B,GAAG,gBAAW;IACd,GAAG,wCAAsB;IACzB,GAAG,0BAAgB;IACnB,GAAG,4BAAiB;IACpB,GAAG,0CAAwB;IAC3B,GAAG,kBAAY;IACf,GAAG,sBAAc;CACI,CAAC;AAiB1B,0EAA0E;AAC7D,QAAA,cAAc,GAAG,EAAC,2BAA2B,EAA3B,gDAA2B,EAAE,kBAAkB,EAAlB,8BAAkB,EAAE,eAAe,EAAf,wBAAe,EAAE,eAAe,EAAf,yBAAe,EAAE,eAAe,EAAf,wBAAe,EAAE,mBAAmB,EAAnB,gCAAmB,EAAE,wBAAwB,EAAxB,0CAAwB,EAAE,WAAW,EAAX,gBAAW,EAAE,sBAAsB,EAAtB,wCAAsB,EAAE,gBAAgB,EAAhB,0BAAgB,EAAE,iBAAiB,EAAjB,4BAAiB,EAAE,wBAAwB,EAAxB,0CAAwB,EAAE,YAAY,EAAZ,kBAAY,EAAE,cAAc,EAAd,sBAAc,EAAC,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Contracts for the Wonde connection request
4
+ * (`Placementt-Backend/functions/src/wonde.ts`).
5
+ *
6
+ * Wonde only approves a connection once the school's IT manager signs it off, so
7
+ * the school gives us their details and we raise the request with Wonde. Nothing
8
+ * is emailed from the platform: the request lands as a support task.
9
+ */
10
+ export declare const wondeCallables: {
11
+ "wonde-requestConnection": {
12
+ request: z.ZodObject<{
13
+ /** The school the connection is for. */
14
+ oId: z.ZodString;
15
+ itManagerName: z.ZodString;
16
+ itManagerEmail: z.ZodString;
17
+ }, "strip", z.ZodTypeAny, {
18
+ oId: string;
19
+ itManagerName: string;
20
+ itManagerEmail: string;
21
+ }, {
22
+ oId: string;
23
+ itManagerName: string;
24
+ itManagerEmail: string;
25
+ }>;
26
+ response: z.ZodObject<{
27
+ /** supportTasks/{id} raised, or the open one it updated. */
28
+ taskId: z.ZodString;
29
+ }, "strip", z.ZodTypeAny, {
30
+ taskId: string;
31
+ }, {
32
+ taskId: string;
33
+ }>;
34
+ };
35
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wondeCallables = void 0;
4
+ const zod_1 = require("zod");
5
+ /**
6
+ * Contracts for the Wonde connection request
7
+ * (`Placementt-Backend/functions/src/wonde.ts`).
8
+ *
9
+ * Wonde only approves a connection once the school's IT manager signs it off, so
10
+ * the school gives us their details and we raise the request with Wonde. Nothing
11
+ * is emailed from the platform: the request lands as a support task.
12
+ */
13
+ exports.wondeCallables = {
14
+ "wonde-requestConnection": {
15
+ request: zod_1.z.object({
16
+ /** The school the connection is for. */
17
+ oId: zod_1.z.string().min(1),
18
+ itManagerName: zod_1.z.string().trim().min(1).max(200),
19
+ itManagerEmail: zod_1.z.string().trim().email().max(320),
20
+ }),
21
+ response: zod_1.z.object({
22
+ /** supportTasks/{id} raised, or the open one it updated. */
23
+ taskId: zod_1.z.string(),
24
+ }),
25
+ },
26
+ };
27
+ //# sourceMappingURL=wonde.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wonde.js","sourceRoot":"","sources":["../../src/callables/wonde.ts"],"names":[],"mappings":";;;AAAA,6BAAsB;AAGtB;;;;;;;GAOG;AACU,QAAA,cAAc,GAAG;IAC1B,yBAAyB,EAAE;QACvB,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC;YACd,wCAAwC;YACxC,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACtB,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;YAChD,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC;SACrD,CAAC;QACF,QAAQ,EAAE,OAAC,CAAC,MAAM,CAAC;YACf,4DAA4D;YAC5D,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE;SACrB,CAAC;KACL;CACoB,CAAC"}
package/lib/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export * from "./activityLogging";
22
22
  export * from "./studentTools";
23
23
  export * from "./featureMarketing";
24
24
  export * from "./onboardingPathways";
25
+ export * from "./studentData";
25
26
  export * from "./lmi";
26
27
  export * from "./lmiCategories";
27
28
  export * from "./pathways";
package/lib/index.js CHANGED
@@ -42,6 +42,7 @@ __exportStar(require("./activityLogging"), exports);
42
42
  __exportStar(require("./studentTools"), exports);
43
43
  __exportStar(require("./featureMarketing"), exports);
44
44
  __exportStar(require("./onboardingPathways"), exports);
45
+ __exportStar(require("./studentData"), exports);
45
46
  __exportStar(require("./lmi"), exports);
46
47
  __exportStar(require("./lmiCategories"), exports);
47
48
  __exportStar(require("./pathways"), exports);
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AACA,sDAAoC;AACpC,gEAA8C;AAC9C,0DAAwC;AACxC,2DAAyC;AACzC,kDAAgC;AAChC,uDAAqC;AACrC,4DAA0C;AAC1C,2DAAyC;AAEzC,oDAAkC;AAClC,8CAA4B;AAC5B,kDAAgC;AAChC,kDAAgC;AAChC,qDAAmC;AACnC,mDAAiC;AACjC,qDAAmC;AACnC,qDAAmC;AACnC,sDAAoC;AACpC,kDAAgC;AAChC,mDAAiC;AACjC,+CAA6B;AAC7B,oDAAkC;AAClC,iDAA+B;AAC/B,qDAAmC;AACnC,uDAAqC;AACrC,wCAAsB;AACtB,kDAAgC;AAChC,6CAA2B;AAC3B,oDAAkC;AAClC,4CAA0B;AAC1B,8CAA4B;AAC5B,2CAAyB;AACzB,kDAAgC;AAChC,uDAAqC;AACrC,oDAAkC;AAClC,kDAAgC;AAChC,0DAAwC;AACxC,iDAA+B;AAC/B,2DAAyC;AACzC,uDAAqC;AACrC,uDAAqC;AACrC,iDAA+B;AAC/B,8CAA4B;AAC5B,qDAAmC;AACnC,0CAAwB;AACxB,kDAAgC;AAChC,yCAAuB;AACvB,4DAA0C;AAC1C,kDAAgC;AAChC,iDAA+B;AAC/B,2CAAyB;AACzB,4DAA0C;AAC1C,oEAAiD;AACjD,yEAAuD;AACvD,kEAAgD;AAChD,kEAAgD;AAChD,oEAAkD;AAClD,yGAAuF;AACvF,0GAAwF;AACxF,+FAA6E;AAC7E,4DAA0C;AAC1C,oEAAkD;AAClD,iEAA+C;AAC/C,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,8CAA4B;AAC5B,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AACA,sDAAoC;AACpC,gEAA8C;AAC9C,0DAAwC;AACxC,2DAAyC;AACzC,kDAAgC;AAChC,uDAAqC;AACrC,4DAA0C;AAC1C,2DAAyC;AAEzC,oDAAkC;AAClC,8CAA4B;AAC5B,kDAAgC;AAChC,kDAAgC;AAChC,qDAAmC;AACnC,mDAAiC;AACjC,qDAAmC;AACnC,qDAAmC;AACnC,sDAAoC;AACpC,kDAAgC;AAChC,mDAAiC;AACjC,+CAA6B;AAC7B,oDAAkC;AAClC,iDAA+B;AAC/B,qDAAmC;AACnC,uDAAqC;AACrC,gDAA8B;AAC9B,wCAAsB;AACtB,kDAAgC;AAChC,6CAA2B;AAC3B,oDAAkC;AAClC,4CAA0B;AAC1B,8CAA4B;AAC5B,2CAAyB;AACzB,kDAAgC;AAChC,uDAAqC;AACrC,oDAAkC;AAClC,kDAAgC;AAChC,0DAAwC;AACxC,iDAA+B;AAC/B,2DAAyC;AACzC,uDAAqC;AACrC,uDAAqC;AACrC,iDAA+B;AAC/B,8CAA4B;AAC5B,qDAAmC;AACnC,0CAAwB;AACxB,kDAAgC;AAChC,yCAAuB;AACvB,4DAA0C;AAC1C,kDAAgC;AAChC,iDAA+B;AAC/B,2CAAyB;AACzB,4DAA0C;AAC1C,oEAAiD;AACjD,yEAAuD;AACvD,kEAAgD;AAChD,kEAAgD;AAChD,oEAAkD;AAClD,yGAAuF;AACvF,0GAAwF;AACxF,+FAA6E;AAC7E,4DAA0C;AAC1C,oEAAkD;AAClD,iEAA+C;AAC/C,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,8CAA4B;AAC5B,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB"}
@@ -0,0 +1,55 @@
1
+ import { InstituteData } from "./typeDefinitions";
2
+ /**
3
+ * Whether a school has the student data an area of the platform needs before it
4
+ * can be set up.
5
+ *
6
+ * Destinations, Development and the careers calendar are all set up per year
7
+ * group. A school with no students, or students with no year group, cannot answer
8
+ * a single question those setups ask, so instead of the setup flow they are shown
9
+ * what the area does and asked to bring their students in first.
10
+ *
11
+ * Destinations and Development go further and need Wonde: they run all year off
12
+ * the school's roll, and a CSV goes stale the week after it is uploaded.
13
+ */
14
+ /**
15
+ * The connection token the E2E and UX-audit harness writes to stand in for a
16
+ * real Wonde connection. The platform only ever checks that a connection exists,
17
+ * so this is enough to make a test school look Wonde-connected. The backend sync
18
+ * skips any institute holding it, so nothing ever calls Wonde with it.
19
+ */
20
+ export declare const WONDE_TEST_CONNECTION_TOKEN = "e2e-test-connection";
21
+ /** What is missing, most fundamental first. */
22
+ export type StudentDataGate = "ready" | "needsStudents" | "needsWonde";
23
+ /**
24
+ * Whether the Wonde connection is the harness's stand-in rather than a real one.
25
+ *
26
+ * @param {InstituteData["wonde"]} [wonde] the institute's Wonde block.
27
+ * @return {boolean} true for a test connection.
28
+ */
29
+ export declare const isWondeTestConnection: (wonde?: Pick<NonNullable<InstituteData["wonde"]>, "connectionToken">) => boolean;
30
+ /**
31
+ * Whether the school has students with a year group on them. `yearGroups` is
32
+ * only ever added to, so the student count is checked as well: a school that
33
+ * deleted every student still has its old year groups listed.
34
+ *
35
+ * @param {Partial<InstituteData>} [institute] the school.
36
+ * @return {boolean} true when there are students to set anything up for.
37
+ */
38
+ export declare const hasStudentYearGroups: (institute?: Partial<InstituteData>) => boolean;
39
+ /**
40
+ * Whether the school has a Wonde connection. The connection id is what the sync
41
+ * runs off, so it is the thing checked, not the `hasIntegration` switch.
42
+ *
43
+ * @param {Partial<InstituteData>} [institute] the school.
44
+ * @return {boolean} true when a Wonde connection id is stored.
45
+ */
46
+ export declare const hasWondeConnection: (institute?: Partial<InstituteData>) => boolean;
47
+ /**
48
+ * What stands between the school and setting an area up.
49
+ *
50
+ * @param {Partial<InstituteData>} [institute] the school.
51
+ * @param {boolean} needsWonde whether the area needs Wonde (Destinations,
52
+ * Development) or just students with year groups (the careers calendar).
53
+ * @return {StudentDataGate} "ready", or the first thing missing.
54
+ */
55
+ export declare const studentDataGate: (institute: Partial<InstituteData> | undefined, needsWonde: boolean) => StudentDataGate;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.studentDataGate = exports.hasWondeConnection = exports.hasStudentYearGroups = exports.isWondeTestConnection = exports.WONDE_TEST_CONNECTION_TOKEN = void 0;
4
+ /**
5
+ * Whether a school has the student data an area of the platform needs before it
6
+ * can be set up.
7
+ *
8
+ * Destinations, Development and the careers calendar are all set up per year
9
+ * group. A school with no students, or students with no year group, cannot answer
10
+ * a single question those setups ask, so instead of the setup flow they are shown
11
+ * what the area does and asked to bring their students in first.
12
+ *
13
+ * Destinations and Development go further and need Wonde: they run all year off
14
+ * the school's roll, and a CSV goes stale the week after it is uploaded.
15
+ */
16
+ /**
17
+ * The connection token the E2E and UX-audit harness writes to stand in for a
18
+ * real Wonde connection. The platform only ever checks that a connection exists,
19
+ * so this is enough to make a test school look Wonde-connected. The backend sync
20
+ * skips any institute holding it, so nothing ever calls Wonde with it.
21
+ */
22
+ exports.WONDE_TEST_CONNECTION_TOKEN = "e2e-test-connection";
23
+ /**
24
+ * Whether the Wonde connection is the harness's stand-in rather than a real one.
25
+ *
26
+ * @param {InstituteData["wonde"]} [wonde] the institute's Wonde block.
27
+ * @return {boolean} true for a test connection.
28
+ */
29
+ const isWondeTestConnection = (wonde) => wonde?.connectionToken === exports.WONDE_TEST_CONNECTION_TOKEN;
30
+ exports.isWondeTestConnection = isWondeTestConnection;
31
+ /**
32
+ * Whether the school has students with a year group on them. `yearGroups` is
33
+ * only ever added to, so the student count is checked as well: a school that
34
+ * deleted every student still has its old year groups listed.
35
+ *
36
+ * @param {Partial<InstituteData>} [institute] the school.
37
+ * @return {boolean} true when there are students to set anything up for.
38
+ */
39
+ const hasStudentYearGroups = (institute) => Boolean((institute?.students ?? 0) > 0 && institute?.yearGroups?.length);
40
+ exports.hasStudentYearGroups = hasStudentYearGroups;
41
+ /**
42
+ * Whether the school has a Wonde connection. The connection id is what the sync
43
+ * runs off, so it is the thing checked, not the `hasIntegration` switch.
44
+ *
45
+ * @param {Partial<InstituteData>} [institute] the school.
46
+ * @return {boolean} true when a Wonde connection id is stored.
47
+ */
48
+ const hasWondeConnection = (institute) => Boolean(institute?.wonde?.id);
49
+ exports.hasWondeConnection = hasWondeConnection;
50
+ /**
51
+ * What stands between the school and setting an area up.
52
+ *
53
+ * @param {Partial<InstituteData>} [institute] the school.
54
+ * @param {boolean} needsWonde whether the area needs Wonde (Destinations,
55
+ * Development) or just students with year groups (the careers calendar).
56
+ * @return {StudentDataGate} "ready", or the first thing missing.
57
+ */
58
+ const studentDataGate = (institute, needsWonde) => {
59
+ if (needsWonde && !(0, exports.hasWondeConnection)(institute))
60
+ return "needsWonde";
61
+ if (!(0, exports.hasStudentYearGroups)(institute))
62
+ return "needsStudents";
63
+ return "ready";
64
+ };
65
+ exports.studentDataGate = studentDataGate;
66
+ //# sourceMappingURL=studentData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studentData.js","sourceRoot":"","sources":["../src/studentData.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;GAWG;AAEH;;;;;GAKG;AACU,QAAA,2BAA2B,GAAG,qBAAqB,CAAC;AAKjE;;;;;GAKG;AACI,MAAM,qBAAqB,GAAG,CAAC,KAAoE,EAAW,EAAE,CACnH,KAAK,EAAE,eAAe,KAAK,mCAA2B,CAAC;AAD9C,QAAA,qBAAqB,yBACyB;AAE3D;;;;;;;GAOG;AACI,MAAM,oBAAoB,GAAG,CAAC,SAAkC,EAAW,EAAE,CAChF,OAAO,CAAC,CAAC,SAAS,EAAE,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;AADhE,QAAA,oBAAoB,wBAC4C;AAE7E;;;;;;GAMG;AACI,MAAM,kBAAkB,GAAG,CAAC,SAAkC,EAAW,EAAE,CAC9E,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;AADrB,QAAA,kBAAkB,sBACG;AAElC;;;;;;;GAOG;AACI,MAAM,eAAe,GAAG,CAAC,SAA2C,EAAE,UAAmB,EAAmB,EAAE;IACjH,IAAI,UAAU,IAAI,CAAC,IAAA,0BAAkB,EAAC,SAAS,CAAC;QAAE,OAAO,YAAY,CAAC;IACtE,IAAI,CAAC,IAAA,4BAAoB,EAAC,SAAS,CAAC;QAAE,OAAO,eAAe,CAAC;IAC7D,OAAO,OAAO,CAAC;AACnB,CAAC,CAAC;AAJW,QAAA,eAAe,mBAI1B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const studentData_1 = require("./studentData");
4
+ const wonde = (id, connectionToken = "real-token") => ({ wonde: { id, connectionToken, hasIntegration: true } });
5
+ describe("hasStudentYearGroups", () => {
6
+ it("is false with no students", () => {
7
+ expect((0, studentData_1.hasStudentYearGroups)({ students: 0, yearGroups: ["7"] })).toBe(false);
8
+ });
9
+ it("is false when students have no year group", () => {
10
+ expect((0, studentData_1.hasStudentYearGroups)({ students: 30 })).toBe(false);
11
+ expect((0, studentData_1.hasStudentYearGroups)({ students: 30, yearGroups: [] })).toBe(false);
12
+ });
13
+ it("is true with students and year groups", () => {
14
+ expect((0, studentData_1.hasStudentYearGroups)({ students: 30, yearGroups: ["7", "8"] })).toBe(true);
15
+ });
16
+ it("is false with no institute", () => {
17
+ expect((0, studentData_1.hasStudentYearGroups)(undefined)).toBe(false);
18
+ });
19
+ });
20
+ describe("hasWondeConnection", () => {
21
+ it("reads the connection id", () => {
22
+ expect((0, studentData_1.hasWondeConnection)({})).toBe(false);
23
+ expect((0, studentData_1.hasWondeConnection)(wonde(""))).toBe(false);
24
+ expect((0, studentData_1.hasWondeConnection)(wonde("A123"))).toBe(true);
25
+ });
26
+ });
27
+ describe("isWondeTestConnection", () => {
28
+ it("recognises only the harness token", () => {
29
+ expect((0, studentData_1.isWondeTestConnection)({ connectionToken: studentData_1.WONDE_TEST_CONNECTION_TOKEN })).toBe(true);
30
+ expect((0, studentData_1.isWondeTestConnection)({ connectionToken: "real-token" })).toBe(false);
31
+ expect((0, studentData_1.isWondeTestConnection)(undefined)).toBe(false);
32
+ });
33
+ });
34
+ describe("studentDataGate", () => {
35
+ const withStudents = { students: 30, yearGroups: ["7"] };
36
+ it("asks for students first when Wonde is not needed", () => {
37
+ expect((0, studentData_1.studentDataGate)({}, false)).toBe("needsStudents");
38
+ expect((0, studentData_1.studentDataGate)(withStudents, false)).toBe("ready");
39
+ });
40
+ it("asks for Wonde when it is needed and missing, even with CSV students", () => {
41
+ expect((0, studentData_1.studentDataGate)(withStudents, true)).toBe("needsWonde");
42
+ });
43
+ it("asks for students when Wonde is connected but nothing has synced", () => {
44
+ expect((0, studentData_1.studentDataGate)(wonde("A123"), true)).toBe("needsStudents");
45
+ });
46
+ it("is ready with Wonde and year groups", () => {
47
+ expect((0, studentData_1.studentDataGate)({ ...wonde("A123"), ...withStudents }, true)).toBe("ready");
48
+ });
49
+ });
50
+ //# sourceMappingURL=studentData.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studentData.test.js","sourceRoot":"","sources":["../src/studentData.test.ts"],"names":[],"mappings":"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"}
@@ -1127,6 +1127,15 @@ export type InstituteData = {
1127
1127
  connectionToken: string;
1128
1128
  fields?: string[];
1129
1129
  };
1130
+ /** The school asked us to set Wonde up, from an area that needs it. Wonde
1131
+ * approves a connection through the school's IT manager, so their details are
1132
+ * what we pass on. Written by wonde-requestConnection. */
1133
+ wondeRequest?: {
1134
+ itManagerName: string;
1135
+ itManagerEmail: string;
1136
+ requestedAt: string;
1137
+ requestedBy: string;
1138
+ };
1130
1139
  microsoft?: {
1131
1140
  enabled: boolean;
1132
1141
  tenantId?: string;
@@ -4157,7 +4166,7 @@ export type SupportTask = {
4157
4166
  assignee: string;
4158
4167
  commId?: string;
4159
4168
  commType?: "leads" | "users";
4160
- source?: "customerQuery" | "general" | "seasonalTask" | "onboardingPack" | "midYearPack" | "welcomePack";
4169
+ source?: "customerQuery" | "general" | "seasonalTask" | "onboardingPack" | "midYearPack" | "welcomePack" | "wondeRequest";
4161
4170
  dueAt: string;
4162
4171
  inbox?: SupportInbox;
4163
4172
  messageId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"typeDefinitions.js","sourceRoot":"","sources":["../src/typeDefinitions.ts"],"names":[],"mappings":";;;AA4iIA,sDASC;AAeD,oDAKC;AAaD,0CAEC;AA/nED;;;;;kFAKkF;AACrE,QAAA,qBAAqB,GAAG,SAAS,CAAC;AAClC,QAAA,iBAAiB,GAAG,OAAO,CAAC;AA2iEzC;yEACyE;AAC5D,QAAA,0BAA0B,GAA2C;IAC9E,gBAAgB,EAAE,2BAA2B;IAC7C,mBAAmB,EAAE,sBAAsB;IAC3C,cAAc,EAAE,iBAAiB;IACjC,kBAAkB,EAAE,qBAAqB;IACzC,UAAU,EAAE,oCAAoC;IAChD,aAAa,EAAE,yBAAyB;IACxC,aAAa,EAAE,wBAAwB;IACvC,KAAK,EAAE,OAAO;CACjB,CAAC;AAEW,QAAA,0BAA0B,GAA+C;IAClF,QAAQ,EAAE,WAAW;IACrB,KAAK,EAAE,YAAY;IACnB,KAAK,EAAE,OAAO;CACjB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,SAAgB,qBAAqB,CAAC,SAAsC;IACxE,QAAQ,SAAS,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC;QAC5B,KAAK,OAAO,CAAC,CAAC,OAAO,OAAO,CAAC;QAC7B,KAAK,WAAW,CAAC;QACjB,KAAK,OAAO,CAAC;QACb,KAAK,OAAO,CAAC;QACb,OAAO,CAAC,CAAC,OAAO,UAAU,CAAC;IAC3B,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,oBAAoB,CAChC,IAAwC,EACxC,MAA6C;IAE7C,OAAO,IAAI,EAAE,WAAW,IAAI,MAAM,EAAE,YAAY,IAAI,KAAK,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,eAAe,CAAC,KAA6D;IACzF,OAAO,GAAG,KAAK,CAAC,cAAc,IAAI,EAAE,IAAI,KAAK,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;AAC/E,CAAC"}
1
+ {"version":3,"file":"typeDefinitions.js","sourceRoot":"","sources":["../src/typeDefinitions.ts"],"names":[],"mappings":";;;AAqjIA,sDASC;AAeD,oDAKC;AAaD,0CAEC;AA/nED;;;;;kFAKkF;AACrE,QAAA,qBAAqB,GAAG,SAAS,CAAC;AAClC,QAAA,iBAAiB,GAAG,OAAO,CAAC;AA2iEzC;yEACyE;AAC5D,QAAA,0BAA0B,GAA2C;IAC9E,gBAAgB,EAAE,2BAA2B;IAC7C,mBAAmB,EAAE,sBAAsB;IAC3C,cAAc,EAAE,iBAAiB;IACjC,kBAAkB,EAAE,qBAAqB;IACzC,UAAU,EAAE,oCAAoC;IAChD,aAAa,EAAE,yBAAyB;IACxC,aAAa,EAAE,wBAAwB;IACvC,KAAK,EAAE,OAAO;CACjB,CAAC;AAEW,QAAA,0BAA0B,GAA+C;IAClF,QAAQ,EAAE,WAAW;IACrB,KAAK,EAAE,YAAY;IACnB,KAAK,EAAE,OAAO;CACjB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,SAAgB,qBAAqB,CAAC,SAAsC;IACxE,QAAQ,SAAS,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC;QAC5B,KAAK,OAAO,CAAC,CAAC,OAAO,OAAO,CAAC;QAC7B,KAAK,WAAW,CAAC;QACjB,KAAK,OAAO,CAAC;QACb,KAAK,OAAO,CAAC;QACb,OAAO,CAAC,CAAC,OAAO,UAAU,CAAC;IAC3B,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,oBAAoB,CAChC,IAAwC,EACxC,MAA6C;IAE7C,OAAO,IAAI,EAAE,WAAW,IAAI,MAAM,EAAE,YAAY,IAAI,KAAK,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,eAAe,CAAC,KAA6D;IACzF,OAAO,GAAG,KAAK,CAAC,cAAc,IAAI,EAAE,IAAI,KAAK,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;AAC/E,CAAC"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "private": false,
3
3
  "name": "placementt-core",
4
4
  "author": "Placementt",
5
- "version": "1.400.1111",
5
+ "version": "1.400.1113",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
8
8
  "scripts": {
@@ -20,7 +20,8 @@ describe("callable registry", () => {
20
20
 
21
21
  it("names every callable as `<module>-<export>`", () => {
22
22
  for (const name of names) {
23
- expect(name).toMatch(/^[a-zA-Z]+-[a-zA-Z]+$/);
23
+ // Letters first; digits allowed after, for modules like `e2e`.
24
+ expect(name).toMatch(/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z][a-zA-Z0-9]*$/);
24
25
  }
25
26
  });
26
27
 
@@ -0,0 +1,25 @@
1
+ import {z} from "zod";
2
+ import {CallableGroup} from "./types";
3
+
4
+ /**
5
+ * Contracts for the end-to-end and UX-audit harness
6
+ * (`Placementt-Backend/functions/src/e2e.ts`).
7
+ *
8
+ * `redeemSignInLink` is public: the token is the credential. Links are only
9
+ * ever minted by the harness (service account, straight into Firestore) and
10
+ * only ever sign in a reserved `e2e.*@careerthread.co.uk` user, so the UX audit
11
+ * can walk a registered school without anyone typing a password.
12
+ */
13
+ export const e2eCallables = {
14
+ "e2e-redeemSignInLink": {
15
+ request: z.object({
16
+ /** The raw token from `/e2eSignIn/:token`. Stored only as a SHA-256 hash. */
17
+ token: z.string().min(32).max(128).regex(/^[A-Za-z0-9_-]+$/),
18
+ }),
19
+ response: z.object({
20
+ code: z.enum(["ok", "notFound", "expired"]),
21
+ /** Firebase custom token for the linked e2e user. Only set when code is "ok". */
22
+ authToken: z.string().optional(),
23
+ }),
24
+ },
25
+ } satisfies CallableGroup;
@@ -9,3 +9,5 @@ export * from "./he";
9
9
  export * from "./studentLaunches";
10
10
  export * from "./billing";
11
11
  export * from "./hubImpactReport";
12
+ export * from "./e2e";
13
+ export * from "./wonde";
@@ -12,6 +12,8 @@ import {studentLaunchCallables} from "./studentLaunches";
12
12
  import {billingCallables} from "./billing";
13
13
  import {jobRolesCallables} from "./jobRoles";
14
14
  import {hubImpactReportCallables} from "./hubImpactReport";
15
+ import {e2eCallables} from "./e2e";
16
+ import {wondeCallables} from "./wonde";
15
17
 
16
18
  /**
17
19
  * The single source of truth for every typed callable. Add a feature group's
@@ -37,6 +39,8 @@ export const allCallables = {
37
39
  ...billingCallables,
38
40
  ...jobRolesCallables,
39
41
  ...hubImpactReportCallables,
42
+ ...e2eCallables,
43
+ ...wondeCallables,
40
44
  } satisfies CallableGroup;
41
45
 
42
46
  /** Every registered callable name. */
@@ -55,4 +59,4 @@ export type CallableContracts = {
55
59
  };
56
60
 
57
61
  /** The per-group maps, exported so tests can assert no key collisions. */
58
- export const callableGroups = {destinationsConfigCallables, placementCallables, eventsCallables, surveyCallables, checksCallables, benchmarksCallables, apprenticeshipsCallables, heCallables, studentLaunchCallables, billingCallables, jobRolesCallables, hubImpactReportCallables};
62
+ export const callableGroups = {destinationsConfigCallables, placementCallables, eventsCallables, surveyCallables, checksCallables, benchmarksCallables, apprenticeshipsCallables, heCallables, studentLaunchCallables, billingCallables, jobRolesCallables, hubImpactReportCallables, e2eCallables, wondeCallables};
@@ -0,0 +1,25 @@
1
+ import {z} from "zod";
2
+ import {CallableGroup} from "./types";
3
+
4
+ /**
5
+ * Contracts for the Wonde connection request
6
+ * (`Placementt-Backend/functions/src/wonde.ts`).
7
+ *
8
+ * Wonde only approves a connection once the school's IT manager signs it off, so
9
+ * the school gives us their details and we raise the request with Wonde. Nothing
10
+ * is emailed from the platform: the request lands as a support task.
11
+ */
12
+ export const wondeCallables = {
13
+ "wonde-requestConnection": {
14
+ request: z.object({
15
+ /** The school the connection is for. */
16
+ oId: z.string().min(1),
17
+ itManagerName: z.string().trim().min(1).max(200),
18
+ itManagerEmail: z.string().trim().email().max(320),
19
+ }),
20
+ response: z.object({
21
+ /** supportTasks/{id} raised, or the open one it updated. */
22
+ taskId: z.string(),
23
+ }),
24
+ },
25
+ } satisfies CallableGroup;
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export * from "./activityLogging";
24
24
  export * from "./studentTools";
25
25
  export * from "./featureMarketing";
26
26
  export * from "./onboardingPathways";
27
+ export * from "./studentData";
27
28
  export * from "./lmi";
28
29
  export * from "./lmiCategories";
29
30
  export * from "./pathways";
@@ -0,0 +1,67 @@
1
+ import {InstituteData} from "./typeDefinitions";
2
+ import {
3
+ WONDE_TEST_CONNECTION_TOKEN,
4
+ hasStudentYearGroups,
5
+ hasWondeConnection,
6
+ isWondeTestConnection,
7
+ studentDataGate,
8
+ } from "./studentData";
9
+
10
+ const wonde = (id: string, connectionToken = "real-token") =>
11
+ ({wonde: {id, connectionToken, hasIntegration: true}} as unknown as Partial<InstituteData>);
12
+
13
+ describe("hasStudentYearGroups", () => {
14
+ it("is false with no students", () => {
15
+ expect(hasStudentYearGroups({students: 0, yearGroups: ["7"]})).toBe(false);
16
+ });
17
+
18
+ it("is false when students have no year group", () => {
19
+ expect(hasStudentYearGroups({students: 30})).toBe(false);
20
+ expect(hasStudentYearGroups({students: 30, yearGroups: []})).toBe(false);
21
+ });
22
+
23
+ it("is true with students and year groups", () => {
24
+ expect(hasStudentYearGroups({students: 30, yearGroups: ["7", "8"]})).toBe(true);
25
+ });
26
+
27
+ it("is false with no institute", () => {
28
+ expect(hasStudentYearGroups(undefined)).toBe(false);
29
+ });
30
+ });
31
+
32
+ describe("hasWondeConnection", () => {
33
+ it("reads the connection id", () => {
34
+ expect(hasWondeConnection({})).toBe(false);
35
+ expect(hasWondeConnection(wonde(""))).toBe(false);
36
+ expect(hasWondeConnection(wonde("A123"))).toBe(true);
37
+ });
38
+ });
39
+
40
+ describe("isWondeTestConnection", () => {
41
+ it("recognises only the harness token", () => {
42
+ expect(isWondeTestConnection({connectionToken: WONDE_TEST_CONNECTION_TOKEN})).toBe(true);
43
+ expect(isWondeTestConnection({connectionToken: "real-token"})).toBe(false);
44
+ expect(isWondeTestConnection(undefined)).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe("studentDataGate", () => {
49
+ const withStudents = {students: 30, yearGroups: ["7"]};
50
+
51
+ it("asks for students first when Wonde is not needed", () => {
52
+ expect(studentDataGate({}, false)).toBe("needsStudents");
53
+ expect(studentDataGate(withStudents, false)).toBe("ready");
54
+ });
55
+
56
+ it("asks for Wonde when it is needed and missing, even with CSV students", () => {
57
+ expect(studentDataGate(withStudents, true)).toBe("needsWonde");
58
+ });
59
+
60
+ it("asks for students when Wonde is connected but nothing has synced", () => {
61
+ expect(studentDataGate(wonde("A123"), true)).toBe("needsStudents");
62
+ });
63
+
64
+ it("is ready with Wonde and year groups", () => {
65
+ expect(studentDataGate({...wonde("A123"), ...withStudents}, true)).toBe("ready");
66
+ });
67
+ });
@@ -0,0 +1,69 @@
1
+ import {InstituteData} from "./typeDefinitions";
2
+
3
+ /**
4
+ * Whether a school has the student data an area of the platform needs before it
5
+ * can be set up.
6
+ *
7
+ * Destinations, Development and the careers calendar are all set up per year
8
+ * group. A school with no students, or students with no year group, cannot answer
9
+ * a single question those setups ask, so instead of the setup flow they are shown
10
+ * what the area does and asked to bring their students in first.
11
+ *
12
+ * Destinations and Development go further and need Wonde: they run all year off
13
+ * the school's roll, and a CSV goes stale the week after it is uploaded.
14
+ */
15
+
16
+ /**
17
+ * The connection token the E2E and UX-audit harness writes to stand in for a
18
+ * real Wonde connection. The platform only ever checks that a connection exists,
19
+ * so this is enough to make a test school look Wonde-connected. The backend sync
20
+ * skips any institute holding it, so nothing ever calls Wonde with it.
21
+ */
22
+ export const WONDE_TEST_CONNECTION_TOKEN = "e2e-test-connection";
23
+
24
+ /** What is missing, most fundamental first. */
25
+ export type StudentDataGate = "ready"|"needsStudents"|"needsWonde";
26
+
27
+ /**
28
+ * Whether the Wonde connection is the harness's stand-in rather than a real one.
29
+ *
30
+ * @param {InstituteData["wonde"]} [wonde] the institute's Wonde block.
31
+ * @return {boolean} true for a test connection.
32
+ */
33
+ export const isWondeTestConnection = (wonde?: Pick<NonNullable<InstituteData["wonde"]>, "connectionToken">): boolean =>
34
+ wonde?.connectionToken === WONDE_TEST_CONNECTION_TOKEN;
35
+
36
+ /**
37
+ * Whether the school has students with a year group on them. `yearGroups` is
38
+ * only ever added to, so the student count is checked as well: a school that
39
+ * deleted every student still has its old year groups listed.
40
+ *
41
+ * @param {Partial<InstituteData>} [institute] the school.
42
+ * @return {boolean} true when there are students to set anything up for.
43
+ */
44
+ export const hasStudentYearGroups = (institute?: Partial<InstituteData>): boolean =>
45
+ Boolean((institute?.students ?? 0) > 0 && institute?.yearGroups?.length);
46
+
47
+ /**
48
+ * Whether the school has a Wonde connection. The connection id is what the sync
49
+ * runs off, so it is the thing checked, not the `hasIntegration` switch.
50
+ *
51
+ * @param {Partial<InstituteData>} [institute] the school.
52
+ * @return {boolean} true when a Wonde connection id is stored.
53
+ */
54
+ export const hasWondeConnection = (institute?: Partial<InstituteData>): boolean =>
55
+ Boolean(institute?.wonde?.id);
56
+
57
+ /**
58
+ * What stands between the school and setting an area up.
59
+ *
60
+ * @param {Partial<InstituteData>} [institute] the school.
61
+ * @param {boolean} needsWonde whether the area needs Wonde (Destinations,
62
+ * Development) or just students with year groups (the careers calendar).
63
+ * @return {StudentDataGate} "ready", or the first thing missing.
64
+ */
65
+ export const studentDataGate = (institute: Partial<InstituteData>|undefined, needsWonde: boolean): StudentDataGate => {
66
+ if (needsWonde && !hasWondeConnection(institute)) return "needsWonde";
67
+ if (!hasStudentYearGroups(institute)) return "needsStudents";
68
+ return "ready";
69
+ };
@@ -1155,6 +1155,15 @@ export type InstituteData = {
1155
1155
  connectionToken: string,
1156
1156
  fields?: string[]
1157
1157
  },
1158
+ /** The school asked us to set Wonde up, from an area that needs it. Wonde
1159
+ * approves a connection through the school's IT manager, so their details are
1160
+ * what we pass on. Written by wonde-requestConnection. */
1161
+ wondeRequest?: {
1162
+ itManagerName: string,
1163
+ itManagerEmail: string,
1164
+ requestedAt: string, // ISO string
1165
+ requestedBy: string, // uid
1166
+ },
1158
1167
  microsoft?: {
1159
1168
  enabled: boolean,
1160
1169
  tenantId?: string,
@@ -4302,7 +4311,7 @@ export type SupportTask = {
4302
4311
  assignee: string; // uid of the assigned team member
4303
4312
  commId?: string; // optional link to a Communication doc
4304
4313
  commType?: "leads" | "users"; // collection the comm lives in
4305
- source?: "customerQuery" | "general" | "seasonalTask" | "onboardingPack" | "midYearPack" | "welcomePack";
4314
+ source?: "customerQuery" | "general" | "seasonalTask" | "onboardingPack" | "midYearPack" | "welcomePack" | "wondeRequest";
4306
4315
  dueAt: string; // ISO string
4307
4316
  inbox?: SupportInbox; // which task inbox triggered this (absent for non-email tasks)
4308
4317
  messageId?: string; // Graph message ID — used to prevent duplicates