placementt-core 1.400.1235 → 1.400.1237
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/callables/billing.d.ts +190 -0
- package/lib/callables/billing.js +30 -0
- package/lib/callables/billing.js.map +1 -1
- package/lib/callables/events.d.ts +136 -2
- package/lib/callables/events.js +67 -0
- package/lib/callables/events.js.map +1 -1
- package/lib/callables/registry.d.ts +636 -4
- package/lib/invoicing.d.ts +24 -0
- package/lib/invoicing.js +22 -1
- package/lib/invoicing.js.map +1 -1
- package/lib/invoicing.test.js +23 -0
- package/lib/invoicing.test.js.map +1 -1
- package/lib/pricing.d.ts +22 -1
- package/lib/pricing.js +43 -0
- package/lib/pricing.js.map +1 -1
- package/lib/pricing.test.js +25 -0
- package/lib/pricing.test.js.map +1 -1
- package/lib/typeDefinitions.d.ts +39 -1
- package/lib/typeDefinitions.js.map +1 -1
- package/package.json +1 -1
- package/src/callables/billing.ts +30 -0
- package/src/callables/events.ts +67 -0
- package/src/invoicing.test.ts +28 -1
- package/src/invoicing.ts +33 -1
- package/src/pricing.test.ts +30 -0
- package/src/pricing.ts +59 -1
- package/src/typeDefinitions.ts +34 -1
package/src/callables/events.ts
CHANGED
|
@@ -92,6 +92,73 @@ export const eventsCallables = {
|
|
|
92
92
|
// Public, same credential as above. The changes an email-only school makes on its invite page.
|
|
93
93
|
// Slot bookings go through events-bookEventSlots and tasks through the same callables employers
|
|
94
94
|
// use (events-submitExternalForm, events-updateFileView, events-uploadRequiredFile).
|
|
95
|
+
// Public: an event's parent sign-up link. Without eventParentId, the event and the places left, for
|
|
96
|
+
// a parent signing up (notAccepting once the organiser closes sign-ups or the places run out). With it,
|
|
97
|
+
// that parent's booking too: the externalEventParents id is the credential, as for non-user schools.
|
|
98
|
+
// details: {event, eventParent?, organiserName, branding, forms, files, schools (hub events: names of
|
|
99
|
+
// schools on the event, for the parent to pick), placesLeft: {adults?, children?}}.
|
|
100
|
+
"events-getParentSignUp": {
|
|
101
|
+
request: z.object({
|
|
102
|
+
eventId: z.string().min(1),
|
|
103
|
+
eventParentId: z.string().min(1).optional(),
|
|
104
|
+
}),
|
|
105
|
+
response: z.object({
|
|
106
|
+
code: z.enum(["found", "notFound", "notAccepting"]),
|
|
107
|
+
details: z.any().optional(),
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
// Public, same credential. "signUp" makes the externalEventParents doc and emails the parent their
|
|
111
|
+
// booking link; an email already signed up is emailed its link instead ("emailed"). Tasks are the
|
|
112
|
+
// event's parentInvites: uploads go to storage at instituteEvents/:oId/:eventId/parent/:eventParentId/:index
|
|
113
|
+
// first, then "upload" records them.
|
|
114
|
+
"events-updateParentSignUp": {
|
|
115
|
+
request: z.discriminatedUnion("action", [
|
|
116
|
+
z.object({
|
|
117
|
+
action: z.literal("signUp"),
|
|
118
|
+
eventId: z.string().min(1),
|
|
119
|
+
forename: z.string().trim().min(1),
|
|
120
|
+
surname: z.string().trim().min(1),
|
|
121
|
+
email: z.string().trim().email(),
|
|
122
|
+
schoolName: optionalField(z.string().trim()),
|
|
123
|
+
adults: z.number().int().min(0),
|
|
124
|
+
children: z.number().int().min(0),
|
|
125
|
+
}),
|
|
126
|
+
z.object({
|
|
127
|
+
action: z.literal("update"),
|
|
128
|
+
eventParentId: z.string().min(1),
|
|
129
|
+
adults: z.number().int().min(0),
|
|
130
|
+
children: z.number().int().min(0),
|
|
131
|
+
schoolName: optionalField(z.string().trim()),
|
|
132
|
+
}),
|
|
133
|
+
z.object({
|
|
134
|
+
action: z.literal("cancel"),
|
|
135
|
+
eventParentId: z.string().min(1),
|
|
136
|
+
}),
|
|
137
|
+
z.object({
|
|
138
|
+
action: z.literal("submitForm"),
|
|
139
|
+
eventParentId: z.string().min(1),
|
|
140
|
+
formId: z.string().min(1),
|
|
141
|
+
formData: z.record(z.unknown()),
|
|
142
|
+
}),
|
|
143
|
+
z.object({
|
|
144
|
+
action: z.literal("viewFile"),
|
|
145
|
+
eventParentId: z.string().min(1),
|
|
146
|
+
fileId: z.string().min(1),
|
|
147
|
+
}),
|
|
148
|
+
z.object({
|
|
149
|
+
action: z.literal("upload"),
|
|
150
|
+
eventParentId: z.string().min(1),
|
|
151
|
+
index: z.number().int().min(0),
|
|
152
|
+
fileName: z.string().min(1),
|
|
153
|
+
}),
|
|
154
|
+
]),
|
|
155
|
+
response: z.object({
|
|
156
|
+
code: z.enum(["updated", "emailed", "full"]),
|
|
157
|
+
eventParentId: z.string().optional(),
|
|
158
|
+
// "upload": the uploaded file's download URL.
|
|
159
|
+
url: z.string().optional(),
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
95
162
|
"events-updateNonUserSchoolInvite": {
|
|
96
163
|
request: z.discriminatedUnion("action", [
|
|
97
164
|
z.object({
|
package/src/invoicing.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {invoiceYearLabel, addDays, calculateInvoice, discountAmount, invoiceFromQuote} from "./invoicing";
|
|
1
|
+
import {invoiceYearLabel, addDays, calculateInvoice, discountAmount, invoiceFromQuote, isLastYearCohort} from "./invoicing";
|
|
2
2
|
import {instituteQuote} from "./pricing";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -147,3 +147,30 @@ describe("dates", () => {
|
|
|
147
147
|
expect(addDays("2026-01-30", 3)).toBe("2026-02-02");
|
|
148
148
|
});
|
|
149
149
|
});
|
|
150
|
+
|
|
151
|
+
describe("isLastYearCohort", () => {
|
|
152
|
+
const today = new Date("2026-11-03T12:00:00Z");
|
|
153
|
+
|
|
154
|
+
it("reads a cohort's academic year in either form", () => {
|
|
155
|
+
expect(isLastYearCohort({academicYear: "2025/26"}, today)).toBe(true);
|
|
156
|
+
expect(isLastYearCohort({academicYear: "2025-26"}, today)).toBe(true);
|
|
157
|
+
expect(isLastYearCohort({academicYear: "2026/27"}, today)).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("falls back to when the placements ended", () => {
|
|
161
|
+
expect(isLastYearCohort({endPlacements: "2026-07-10"}, today)).toBe(true);
|
|
162
|
+
expect(isLastYearCohort({endPlacements: "2025-07-10"}, today)).toBe(false);
|
|
163
|
+
expect(isLastYearCohort({}, today)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("returning school invoice rows", () => {
|
|
168
|
+
it("shows the returning discount as the row's own discount", () => {
|
|
169
|
+
const quote = instituteQuote({products: {wex: true}, students: 800, pricing: {wexY10: 40, returningUnplaced: 10}});
|
|
170
|
+
const draft = invoiceFromQuote(quote, {instituteName: "Test School", today: new Date("2026-11-03")});
|
|
171
|
+
const row = draft.lines.find((l) => l.key === "wexY10");
|
|
172
|
+
expect(row?.description).toContain("10 placements discounted");
|
|
173
|
+
expect(row?.discount?.type).toBe("amount");
|
|
174
|
+
expect(calculateInvoice(draft).net).toBeCloseTo(quote.total, 2);
|
|
175
|
+
});
|
|
176
|
+
});
|
package/src/invoicing.ts
CHANGED
|
@@ -192,9 +192,12 @@ export function invoiceFromQuote(
|
|
|
192
192
|
// was never discounted, and a percentage would take a slice of it.
|
|
193
193
|
const saved = line.undiscounted === undefined ? 0 : round((line.undiscounted - line.amount) * schools);
|
|
194
194
|
|
|
195
|
+
const returning = line.returningPlacements ?
|
|
196
|
+
` (returning school: ${line.returningPlacements} placement${line.returningPlacements === 1 ? "" : "s"} discounted for students without a placement last year)` : "";
|
|
197
|
+
|
|
195
198
|
return {
|
|
196
199
|
key: line.key,
|
|
197
|
-
description: `${line.label} — CareerThread ${invoiceYearLabel(today)}`,
|
|
200
|
+
description: `${line.label} — CareerThread ${invoiceYearLabel(today)}${returning}`,
|
|
198
201
|
quantity: perUnit ? perUnit.quantity : schools,
|
|
199
202
|
unitAmount: round(perUnit ? perUnit.rate : listPrice),
|
|
200
203
|
...(saved > 0 ? {discount: {type: "amount" as const, value: saved}} : {}),
|
|
@@ -262,6 +265,14 @@ export type InvoiceRecord = {
|
|
|
262
265
|
draft?: InvoiceDraft,
|
|
263
266
|
/** Admin uid, when created from the admin panel. */
|
|
264
267
|
createdBy?: string,
|
|
268
|
+
/** The latest draft emailed to an admin to check, before it went to the customer. */
|
|
269
|
+
draftSentTo?: string,
|
|
270
|
+
/** ISO timestamp. */
|
|
271
|
+
draftSentAt?: string,
|
|
272
|
+
/** Who the approved invoice was emailed to, by us rather than by Xero. */
|
|
273
|
+
sentTo?: string,
|
|
274
|
+
/** ISO timestamp. */
|
|
275
|
+
sentAt?: string,
|
|
265
276
|
/** ISO timestamps. */
|
|
266
277
|
createdAt: string,
|
|
267
278
|
updatedAt: string,
|
|
@@ -274,3 +285,24 @@ export const CUSTOMER_VISIBLE_INVOICE_STATUSES: XeroInvoiceStatus[] = ["AUTHORIS
|
|
|
274
285
|
|
|
275
286
|
/** Where the sync keeps its high-water mark. */
|
|
276
287
|
export const BILLING_SYNC_PATH = ["adminConfig", "billingSync"];
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Whether a work experience cohort ran in the academic year before `today`'s,
|
|
291
|
+
* which is the year a returning school's unplaced students are counted from.
|
|
292
|
+
*
|
|
293
|
+
* Cohorts made by the recurring set-up carry their academic year ("2025/26");
|
|
294
|
+
* older ones are placed by when their placements ended.
|
|
295
|
+
*
|
|
296
|
+
* @param {object} cohort the cohort's year and placement dates.
|
|
297
|
+
* @param {Date} [today] the invoice date. Now when absent.
|
|
298
|
+
* @return {boolean} true for last year's cohorts.
|
|
299
|
+
*/
|
|
300
|
+
export function isLastYearCohort(
|
|
301
|
+
cohort: {academicYear?: string, endPlacements?: string, startPlacements?: string},
|
|
302
|
+
today: Date = new Date(),
|
|
303
|
+
): boolean {
|
|
304
|
+
const lastStart = currentAcademicStartYear(today) - 1;
|
|
305
|
+
if (cohort.academicYear) return cohort.academicYear.replace("/", "-") === academicYearLabel(lastStart);
|
|
306
|
+
const ended = (cohort.endPlacements || cohort.startPlacements || "").split("T")[0];
|
|
307
|
+
return Boolean(ended) && ended >= `${lastStart}-09-01` && ended < `${lastStart + 1}-09-01`;
|
|
308
|
+
}
|
package/src/pricing.test.ts
CHANGED
|
@@ -336,3 +336,33 @@ describe("quoteSnapshot", () => {
|
|
|
336
336
|
expect(snapshot.discountTotal).toBe(0);
|
|
337
337
|
});
|
|
338
338
|
});
|
|
339
|
+
|
|
340
|
+
describe("returning school discount", () => {
|
|
341
|
+
const base: SchoolPricingInput = {students: 800, wexY10: 30, wexY12: 10};
|
|
342
|
+
|
|
343
|
+
it("charges last year's unplaced students' placements at half their price", () => {
|
|
344
|
+
const full = calculateSchoolPrice(base);
|
|
345
|
+
const returning = calculateSchoolPrice({...base, returningUnplaced: 8});
|
|
346
|
+
const wex = (r: typeof full) => r.lines.filter((l) => l.key.startsWith("wex"));
|
|
347
|
+
|
|
348
|
+
const fullY10 = wex(full)[0].amount;
|
|
349
|
+
const fullY12 = wex(full)[1].amount;
|
|
350
|
+
// 8 of 40 placements: 6 in Y10, 2 in Y12, each at half its average rate.
|
|
351
|
+
expect(wex(returning)[0].returningPlacements).toBe(6);
|
|
352
|
+
expect(wex(returning)[1].returningPlacements).toBe(2);
|
|
353
|
+
expect(wex(returning)[0].amount).toBeCloseTo(fullY10 - 6 * (fullY10 / 30) * 0.5, 6);
|
|
354
|
+
expect(wex(returning)[1].amount).toBeCloseTo(fullY12 - 2 * (fullY12 / 10) * 0.5, 6);
|
|
355
|
+
expect(wex(returning)[0].undiscounted).toBeCloseTo(fullY10, 6);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("never discounts more placements than are quoted", () => {
|
|
359
|
+
const all = calculateSchoolPrice({...base, returningUnplaced: 500});
|
|
360
|
+
const wex = all.lines.filter((l) => l.key.startsWith("wex"));
|
|
361
|
+
expect(wex.reduce((t, l) => t + (l.returningPlacements || 0), 0)).toBe(40);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("does nothing without work experience", () => {
|
|
365
|
+
const quote = instituteQuote({products: {events: true}, students: 800, pricing: {returningUnplaced: 10}});
|
|
366
|
+
expect(quote.lines.some((l) => l.returningPlacements)).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
});
|
package/src/pricing.ts
CHANGED
|
@@ -59,6 +59,10 @@ export type PricingConfig = {
|
|
|
59
59
|
},
|
|
60
60
|
/** Careers hubs are a flat platform fee plus a fee for each school. */
|
|
61
61
|
hub: {baseFee: number, perSchoolFee: number},
|
|
62
|
+
/** Taken off a returning school's placement fee for each student who was on
|
|
63
|
+
* work experience last year and didn't get a placement, as a fraction of
|
|
64
|
+
* that placement's price (0.5 = those placements are half price). */
|
|
65
|
+
returningUnplacedDiscount: number,
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
/** The pricing sheet's figures, as agreed for 2026. */
|
|
@@ -74,6 +78,7 @@ export const DEFAULT_PRICING_CONFIG: PricingConfig = {
|
|
|
74
78
|
fullPlatformDiscount: 0.1,
|
|
75
79
|
trust: {threshold: 10, discount: 0.05, largeDiscount: 0.1},
|
|
76
80
|
hub: {baseFee: 2000, perSchoolFee: 150},
|
|
81
|
+
returningUnplacedDiscount: 0.5,
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
/** Where the adjustable copy of the config lives. */
|
|
@@ -103,6 +108,9 @@ export type SchoolPricingInput = {
|
|
|
103
108
|
/** A price agreed with the customer, which replaces everything above it —
|
|
104
109
|
* the discounts, the minimum fee and the total the products came to. */
|
|
105
110
|
quoteOverride?: number,
|
|
111
|
+
/** Students on work experience last year who never got a placement. That
|
|
112
|
+
* many of this year's placements are charged at the returning discount. */
|
|
113
|
+
returningUnplaced?: number,
|
|
106
114
|
};
|
|
107
115
|
|
|
108
116
|
export type PricingLineKey =
|
|
@@ -113,8 +121,10 @@ export type PricingLine = {
|
|
|
113
121
|
label: string,
|
|
114
122
|
/** What the product costs after any partial-product discount. */
|
|
115
123
|
amount: number,
|
|
116
|
-
/** The fee before its partial-product discount, when one was applied. */
|
|
124
|
+
/** The fee before its partial-product or returning discount, when one was applied. */
|
|
117
125
|
undiscounted?: number,
|
|
126
|
+
/** Placements on this line charged at the returning discount. */
|
|
127
|
+
returningPlacements?: number,
|
|
118
128
|
/** How the amount was reached, band by band. Empty for the base fee. */
|
|
119
129
|
bands: PricingBand[],
|
|
120
130
|
};
|
|
@@ -224,6 +234,44 @@ export function trustDiscountRate(
|
|
|
224
234
|
return count >= config.trust.threshold ? config.trust.largeDiscount : config.trust.discount;
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Charge last year's unplaced students' placements at the returning discount.
|
|
239
|
+
*
|
|
240
|
+
* A school that paid for students who never got a placement shouldn't pay full
|
|
241
|
+
* price for them again. Those placements are spread across the year groups in
|
|
242
|
+
* proportion to each one's placements, and priced at that year group's average
|
|
243
|
+
* rate per placement, so the graduated bands still decide what a placement is
|
|
244
|
+
* worth. Never more placements than this year's quote has.
|
|
245
|
+
*
|
|
246
|
+
* @param {Array} lines the fees so far; the work experience ones are changed in place.
|
|
247
|
+
* @param {object} input the quote's inputs.
|
|
248
|
+
* @param {object} config the figures to price with.
|
|
249
|
+
*/
|
|
250
|
+
function applyReturningDiscount(lines: PricingLine[], input: SchoolPricingInput, config: PricingConfig) {
|
|
251
|
+
const wexLines = lines.filter((l) => l.key === "wexY10" || l.key === "wexY12");
|
|
252
|
+
const placements = sum(wexLines.map((l) => sum(l.bands.map((b) => b.quantity))));
|
|
253
|
+
const unplaced = Math.min(Math.max(Math.floor(input.returningUnplaced || 0), 0), placements);
|
|
254
|
+
const rate = clampFraction(config.returningUnplacedDiscount);
|
|
255
|
+
if (!unplaced || !rate) return;
|
|
256
|
+
|
|
257
|
+
let remaining = unplaced;
|
|
258
|
+
wexLines.forEach((line, index) => {
|
|
259
|
+
const linePlacements = sum(line.bands.map((b) => b.quantity));
|
|
260
|
+
if (!linePlacements || !line.amount) return;
|
|
261
|
+
// The last year group takes whatever rounding left over, so the
|
|
262
|
+
// placements discounted always add up to the unplaced count.
|
|
263
|
+
const share = index === wexLines.length - 1 ?
|
|
264
|
+
Math.min(remaining, linePlacements) :
|
|
265
|
+
Math.min(Math.round(unplaced * linePlacements / placements), linePlacements, remaining);
|
|
266
|
+
if (!share) return;
|
|
267
|
+
remaining -= share;
|
|
268
|
+
const undiscounted = line.amount;
|
|
269
|
+
line.amount = undiscounted - share * (undiscounted / linePlacements) * rate;
|
|
270
|
+
line.undiscounted = undiscounted;
|
|
271
|
+
line.returningPlacements = share;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
227
275
|
/**
|
|
228
276
|
* Price a school, trust or hub-school quote.
|
|
229
277
|
*
|
|
@@ -257,6 +305,7 @@ export function calculateSchoolPrice(
|
|
|
257
305
|
};
|
|
258
306
|
addWex("wexY10", "Work experience: Y10", input.wexY10);
|
|
259
307
|
addWex("wexY12", "Work experience: Y12", input.wexY12);
|
|
308
|
+
applyReturningDiscount(lines, input, config);
|
|
260
309
|
|
|
261
310
|
const addPerStudent = (key: "events"|"destinations", label: string, taken?: boolean, discounted?: boolean) => {
|
|
262
311
|
if (!taken) return;
|
|
@@ -339,6 +388,7 @@ export function mergePricingConfig(stored?: Partial<PricingConfig>): PricingConf
|
|
|
339
388
|
tiers: stored.tiers?.length ? stored.tiers.map((t, i) => ({...(d.tiers[i] || d.tiers[d.tiers.length - 1]), ...t})) : d.tiers,
|
|
340
389
|
partialProductDiscount: {...d.partialProductDiscount, ...stored.partialProductDiscount},
|
|
341
390
|
fullPlatformDiscount: stored.fullPlatformDiscount ?? d.fullPlatformDiscount,
|
|
391
|
+
returningUnplacedDiscount: stored.returningUnplacedDiscount ?? d.returningUnplacedDiscount,
|
|
342
392
|
trust: {...d.trust, ...stored.trust},
|
|
343
393
|
hub: {...d.hub, ...stored.hub},
|
|
344
394
|
};
|
|
@@ -379,12 +429,19 @@ export type InstitutePricing = {
|
|
|
379
429
|
discountReason?: string,
|
|
380
430
|
/** A price agreed with the customer, which replaces the calculated one. */
|
|
381
431
|
quoteOverride?: number,
|
|
432
|
+
/** Students on last year's work experience cohorts who never got a placement,
|
|
433
|
+
* charged at the returning discount this year. One school's, for a trust. */
|
|
434
|
+
returningUnplaced?: number,
|
|
382
435
|
/** Schools in the trust, which set its discount. Trust accounts only. */
|
|
383
436
|
schools?: number,
|
|
384
437
|
/** Schools on the hub. Hub accounts only. */
|
|
385
438
|
hubSchools?: number,
|
|
386
439
|
/** The quote as last saved, for the customer to see. */
|
|
387
440
|
quote?: InstituteQuoteSnapshot,
|
|
441
|
+
/** Academic years ("2026-27") an admin marked as invoiced outside the panel,
|
|
442
|
+
* for a customer billed some other way (old pricing, an invoice that can't
|
|
443
|
+
* be matched to them in Xero). Admin-only. */
|
|
444
|
+
markedInvoiced?: {[year: string]: {at: string, note?: string}},
|
|
388
445
|
};
|
|
389
446
|
|
|
390
447
|
/** A priced institute: the same shape for a school, a trust and a hub. */
|
|
@@ -550,6 +607,7 @@ export function instituteQuote(
|
|
|
550
607
|
fullPlatformDiscount: pricing?.fullPlatformDiscount,
|
|
551
608
|
bespokeDiscount: pricing?.bespokeDiscount,
|
|
552
609
|
quoteOverride: pricing?.quoteOverride,
|
|
610
|
+
returningUnplaced: wex ? pricing?.returningUnplaced : undefined,
|
|
553
611
|
}, config);
|
|
554
612
|
}
|
|
555
613
|
|
package/src/typeDefinitions.ts
CHANGED
|
@@ -2564,6 +2564,16 @@ export type ExternalEvent = {
|
|
|
2564
2564
|
exists?: boolean,
|
|
2565
2565
|
perMatchTasks?: boolean,
|
|
2566
2566
|
},
|
|
2567
|
+
// Parents signing up to an event with parentAttendance set, from its public link
|
|
2568
|
+
// (/event/parents/:oId/:eventId). Each sign-up is an externalEventParents doc.
|
|
2569
|
+
acceptingParents?: boolean,
|
|
2570
|
+
maxParentAdults?: number, // Adult places. Sign-ups close once they're taken.
|
|
2571
|
+
maxParentChildren?: number, // Child places, as above.
|
|
2572
|
+
parentInvites?: {
|
|
2573
|
+
forms?: {form: string, relatedToUpload?: string, visibleTo?: ("employers"|"students"|"parents"|"school")[]}[],
|
|
2574
|
+
files?: string[],
|
|
2575
|
+
requiredFiles?: {fileName: string, fileType: string, visibleTo?: ("employers"|"students"|"parents"|"school")[]}[],
|
|
2576
|
+
},
|
|
2567
2577
|
employerInvites: {
|
|
2568
2578
|
forms?: {form: string, relatedToUpload?: string, visibleTo?: ("employers"|"students"|"parents"|"school")[]}[],
|
|
2569
2579
|
files?: string[],
|
|
@@ -2609,7 +2619,7 @@ export type ExternalEvent = {
|
|
|
2609
2619
|
provider?: string|false,
|
|
2610
2620
|
school?: string|false,
|
|
2611
2621
|
},
|
|
2612
|
-
completedStages: ("basicDetails"|"eventConfig"|"selectEmployers"|"employerInviteForms"|"sendEmployerInvites"|"setupStudentInvites"|"setupSchoolInvites"|"review"|"created"|"launched")[]
|
|
2622
|
+
completedStages: ("basicDetails"|"eventConfig"|"selectEmployers"|"employerInviteForms"|"sendEmployerInvites"|"setupStudentInvites"|"setupSchoolInvites"|"setupParentInvites"|"review"|"created"|"launched")[]
|
|
2613
2623
|
}&Address
|
|
2614
2624
|
|
|
2615
2625
|
|
|
@@ -3589,6 +3599,29 @@ export type ExternalEventCareersSchool = {
|
|
|
3589
3599
|
}
|
|
3590
3600
|
|
|
3591
3601
|
|
|
3602
|
+
// A parent or carer signed up to an event from its public link, with how many adults and children
|
|
3603
|
+
// they're bringing. They have no account: the doc id is the credential for their booking page
|
|
3604
|
+
// (/event/parents/:oId/:eventId/:eventParentId), as for a school invited by email.
|
|
3605
|
+
export type ExternalEventParent = {
|
|
3606
|
+
oId: string,
|
|
3607
|
+
eventId: string,
|
|
3608
|
+
visibleTo: string[],
|
|
3609
|
+
created: string,
|
|
3610
|
+
status: "confirmed"|"cancelled"|"removed", // "cancelled" by the parent, "removed" by the organiser
|
|
3611
|
+
forename: string,
|
|
3612
|
+
surname: string,
|
|
3613
|
+
email: string,
|
|
3614
|
+
adults: number,
|
|
3615
|
+
children: number,
|
|
3616
|
+
// Careers hub events only: the school their children go to, as they gave it.
|
|
3617
|
+
schoolName?: string,
|
|
3618
|
+
completedInvite?: {
|
|
3619
|
+
filesViewed?: string[],
|
|
3620
|
+
formsCompleted?: {[key: string]: unknown},
|
|
3621
|
+
filesUploaded?: {[key: number]: string},
|
|
3622
|
+
},
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3592
3625
|
export type ExternalEventStudent = {
|
|
3593
3626
|
oId: string,
|
|
3594
3627
|
uid: string,
|