placementt-core 1.400.1052 → 1.400.1053

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/pricing.d.ts CHANGED
@@ -68,8 +68,14 @@ export type SchoolPricingInput = {
68
68
  destinationsDiscount?: boolean;
69
69
  /** Discount the whole quote, for a school taking the full platform. */
70
70
  fullPlatformDiscount?: boolean;
71
+ /** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off).
72
+ * Taken after the full-platform discount and before the minimum fee. */
73
+ bespokeDiscount?: number;
74
+ /** A price agreed with the customer, which replaces everything above it —
75
+ * the discounts, the minimum fee and the total the products came to. */
76
+ quoteOverride?: number;
71
77
  };
72
- export type PricingLineKey = "base" | "wexY10" | "wexY12" | "events" | "destinations";
78
+ export type PricingLineKey = "base" | "wexY10" | "wexY12" | "events" | "destinations" | "hubBase" | "hubSchools";
73
79
  export type PricingLine = {
74
80
  key: PricingLineKey;
75
81
  label: string;
@@ -95,8 +101,12 @@ export type SchoolPricingResult = {
95
101
  subtotal: number;
96
102
  /** £ taken off the subtotal by the full-platform discount. */
97
103
  fullPlatformDiscountApplied: number;
104
+ /** £ taken off by the negotiated discount, after the full-platform one. */
105
+ bespokeDiscountApplied: number;
98
106
  /** True when the minimum fee, not the products, set the price. */
99
107
  minFeeApplied: boolean;
108
+ /** True when an agreed price replaced the calculated one. */
109
+ overrideApplied: boolean;
100
110
  total: number;
101
111
  };
102
112
  export type HubPricingResult = {
@@ -135,3 +145,122 @@ export declare function calculateHubPrice(schools: number, config?: PricingConfi
135
145
  * @return {object} a complete config, safe to price with.
136
146
  */
137
147
  export declare function mergePricingConfig(stored?: Partial<PricingConfig>): PricingConfig;
148
+ /** The terms agreed with one customer. Stored at `institutes/{oId}.pricing`. */
149
+ export type InstitutePricing = {
150
+ /** The roll we quoted on. Defaults to the institute's own student count. */
151
+ students?: number;
152
+ /** Y10 placements quoted. Undefined means the year group isn't on work experience. */
153
+ wexY10?: number;
154
+ /** Y12 placements quoted. Undefined means the year group isn't on work experience. */
155
+ wexY12?: number;
156
+ /** Take the partial-product discount off the events fee. */
157
+ eventsDiscount?: boolean;
158
+ /** Take the partial-product discount off the destinations fee. */
159
+ destinationsDiscount?: boolean;
160
+ /** Take the full-platform discount off the whole quote. */
161
+ fullPlatformDiscount?: boolean;
162
+ /** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off). */
163
+ bespokeDiscount?: number;
164
+ /** Why the negotiated discount or the agreed price was given. Admin-only. */
165
+ discountReason?: string;
166
+ /** A price agreed with the customer, which replaces the calculated one. */
167
+ quoteOverride?: number;
168
+ /** Schools on the hub. Hub accounts only. */
169
+ hubSchools?: number;
170
+ /** The quote as last saved, for the customer to see. */
171
+ quote?: InstituteQuoteSnapshot;
172
+ };
173
+ /** A priced institute: the same shape for a school, a trust and a hub. */
174
+ export type InstituteQuote = {
175
+ kind: "school" | "hub";
176
+ /** Every fee charged, with the working behind it. */
177
+ lines: PricingLine[];
178
+ /** The lines added up, before any whole-quote discount. */
179
+ subtotal: number;
180
+ fullPlatformDiscountApplied: number;
181
+ bespokeDiscountApplied: number;
182
+ minFeeApplied: boolean;
183
+ overrideApplied: boolean;
184
+ total: number;
185
+ };
186
+ /** What the customer is shown: the total, and what makes it up. No rates. */
187
+ export type InstituteQuoteSnapshot = {
188
+ lines: {
189
+ key: PricingLineKey;
190
+ label: string;
191
+ amount: number;
192
+ }[];
193
+ /** The lines added up. */
194
+ subtotal: number;
195
+ /** Everything taken off the lines to reach the total, as one figure. */
196
+ discountTotal: number;
197
+ /** True when the total is a floor or an agreed price above what the lines
198
+ * come to, so the breakdown is shown as an illustration, not a sum. */
199
+ minimum: boolean;
200
+ total: number;
201
+ /** ISO date the quote was last worked out. */
202
+ updated: string;
203
+ };
204
+ /** What the quote needs to know about the institute being priced. */
205
+ export type InstituteQuoteInput = {
206
+ /** What the institute has, as resolved by `resolveProducts`. */
207
+ products: {
208
+ wex?: boolean;
209
+ events?: boolean;
210
+ destinations?: boolean;
211
+ isHub?: boolean;
212
+ };
213
+ /** The institute's roll, when the agreed terms don't name one. */
214
+ students?: number;
215
+ /** Schools on the hub, when the agreed terms don't name a number. */
216
+ hubSchools?: number;
217
+ /** The terms agreed with this customer. */
218
+ pricing?: InstitutePricing;
219
+ };
220
+ /**
221
+ * Price a school and present it as an institute quote.
222
+ *
223
+ * @param {object} input the roll, the year groups, the products and the terms.
224
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
225
+ * @return {object} the quote, ready to render.
226
+ */
227
+ export declare function schoolQuote(input: SchoolPricingInput, config?: PricingConfig): InstituteQuote;
228
+ /**
229
+ * Price a careers hub as an institute quote: the platform fee, then the schools.
230
+ *
231
+ * Hubs have no products to discount individually and no minimum fee to fall back
232
+ * on, so the only terms that reach them are the negotiated discount and an
233
+ * agreed price.
234
+ *
235
+ * @param {number} schools how many schools the hub brings on.
236
+ * @param {object} [pricing] the terms agreed with this hub.
237
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
238
+ * @return {object} the quote, ready to render.
239
+ */
240
+ export declare function hubQuote(schools: number, pricing?: InstitutePricing, config?: PricingConfig): InstituteQuote;
241
+ /**
242
+ * Price an institute on what it actually has and what was agreed with it.
243
+ *
244
+ * The products come from `resolveProducts` rather than the raw flags, so a trust
245
+ * is priced on the union of its schools' products the same way it is gated on
246
+ * them. A year group with no placements agreed is a year group not on work
247
+ * experience — except where the institute has work experience and neither year
248
+ * group has been filled in, which is a quote waiting to be finished rather than
249
+ * a school not taking the product, so Y10 is charged at zero to say so.
250
+ *
251
+ * @param {object} input the institute's products, roll and agreed terms.
252
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
253
+ * @return {object} the quote, ready to render.
254
+ */
255
+ export declare function instituteQuote(input: InstituteQuoteInput, config?: PricingConfig): InstituteQuote;
256
+ /**
257
+ * Reduce a quote to what the customer sees: the fees, and one discount figure.
258
+ *
259
+ * The base fee isn't itemised to customers, so it is folded into the first fee
260
+ * they are shown — and where there is nothing to fold it into, it stands alone
261
+ * as the platform fee it is.
262
+ *
263
+ * @param {object} quote the priced institute.
264
+ * @return {object} the snapshot to store on the institute.
265
+ */
266
+ export declare function quoteSnapshot(quote: InstituteQuote): InstituteQuoteSnapshot;
package/lib/pricing.js CHANGED
@@ -22,6 +22,10 @@ exports.PRICING_CONFIG_PATH = exports.DEFAULT_PRICING_CONFIG = void 0;
22
22
  exports.calculateSchoolPrice = calculateSchoolPrice;
23
23
  exports.calculateHubPrice = calculateHubPrice;
24
24
  exports.mergePricingConfig = mergePricingConfig;
25
+ exports.schoolQuote = schoolQuote;
26
+ exports.hubQuote = hubQuote;
27
+ exports.instituteQuote = instituteQuote;
28
+ exports.quoteSnapshot = quoteSnapshot;
25
29
  /** The pricing sheet's figures, as agreed for 2026. */
26
30
  exports.DEFAULT_PRICING_CONFIG = {
27
31
  minFee: 500,
@@ -73,6 +77,7 @@ function graduated(quantity, bands) {
73
77
  return lines;
74
78
  }
75
79
  const sum = (values) => values.reduce((total, value) => total + value, 0);
80
+ const clampFraction = (value) => Math.min(Math.max(value || 0, 0), 1);
76
81
  const bandsTotal = (bands) => sum(bands.map((b) => b.amount));
77
82
  /**
78
83
  * Price a school, trust or hub-school quote.
@@ -110,13 +115,19 @@ function calculateSchoolPrice(input, config = exports.DEFAULT_PRICING_CONFIG) {
110
115
  addPerStudent("destinations", "Destinations", input.destinations, input.destinationsDiscount);
111
116
  const subtotal = sum(lines.map((l) => l.amount));
112
117
  const discounted = input.fullPlatformDiscount ? subtotal * (1 - config.fullPlatformDiscount) : subtotal;
113
- const total = Math.max(discounted, config.minFee);
118
+ const negotiated = discounted * (1 - clampFraction(input.bespokeDiscount));
119
+ const floored = Math.max(negotiated, config.minFee);
120
+ const overridden = input.quoteOverride !== undefined && input.quoteOverride >= 0;
114
121
  return {
115
122
  lines,
116
123
  subtotal,
117
124
  fullPlatformDiscountApplied: subtotal - discounted,
118
- minFeeApplied: total > discounted,
119
- total,
125
+ bespokeDiscountApplied: discounted - negotiated,
126
+ // An agreed price is the price, so neither the floor nor the discounts
127
+ // are what set it — they are only reported when the calculation stands.
128
+ minFeeApplied: !overridden && floored > negotiated,
129
+ overrideApplied: overridden,
130
+ total: overridden ? input.quoteOverride : floored,
120
131
  };
121
132
  }
122
133
  /**
@@ -153,4 +164,120 @@ function mergePricingConfig(stored) {
153
164
  hub: { ...d.hub, ...stored.hub },
154
165
  };
155
166
  }
167
+ /**
168
+ * Price a school and present it as an institute quote.
169
+ *
170
+ * @param {object} input the roll, the year groups, the products and the terms.
171
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
172
+ * @return {object} the quote, ready to render.
173
+ */
174
+ function schoolQuote(input, config = exports.DEFAULT_PRICING_CONFIG) {
175
+ return { kind: "school", ...calculateSchoolPrice(input, config) };
176
+ }
177
+ /**
178
+ * Price a careers hub as an institute quote: the platform fee, then the schools.
179
+ *
180
+ * Hubs have no products to discount individually and no minimum fee to fall back
181
+ * on, so the only terms that reach them are the negotiated discount and an
182
+ * agreed price.
183
+ *
184
+ * @param {number} schools how many schools the hub brings on.
185
+ * @param {object} [pricing] the terms agreed with this hub.
186
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
187
+ * @return {object} the quote, ready to render.
188
+ */
189
+ function hubQuote(schools, pricing, config = exports.DEFAULT_PRICING_CONFIG) {
190
+ const count = Math.max(schools || 0, 0);
191
+ const hub = calculateHubPrice(count, config);
192
+ const lines = [
193
+ { key: "hubBase", label: "Platform fee", amount: hub.baseFee, bands: [] },
194
+ {
195
+ key: "hubSchools",
196
+ label: "School fees",
197
+ amount: hub.schoolFees,
198
+ bands: count > 0 ?
199
+ [{ tier: 1, quantity: count, rate: config.hub.perSchoolFee, amount: hub.schoolFees }] :
200
+ [],
201
+ },
202
+ ];
203
+ const negotiated = hub.total * (1 - clampFraction(pricing?.bespokeDiscount));
204
+ const overridden = pricing?.quoteOverride !== undefined && pricing.quoteOverride >= 0;
205
+ return {
206
+ kind: "hub",
207
+ lines,
208
+ subtotal: hub.total,
209
+ fullPlatformDiscountApplied: 0,
210
+ bespokeDiscountApplied: hub.total - negotiated,
211
+ minFeeApplied: false,
212
+ overrideApplied: overridden,
213
+ total: overridden ? pricing?.quoteOverride : negotiated,
214
+ };
215
+ }
216
+ /**
217
+ * Price an institute on what it actually has and what was agreed with it.
218
+ *
219
+ * The products come from `resolveProducts` rather than the raw flags, so a trust
220
+ * is priced on the union of its schools' products the same way it is gated on
221
+ * them. A year group with no placements agreed is a year group not on work
222
+ * experience — except where the institute has work experience and neither year
223
+ * group has been filled in, which is a quote waiting to be finished rather than
224
+ * a school not taking the product, so Y10 is charged at zero to say so.
225
+ *
226
+ * @param {object} input the institute's products, roll and agreed terms.
227
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
228
+ * @return {object} the quote, ready to render.
229
+ */
230
+ function instituteQuote(input, config = exports.DEFAULT_PRICING_CONFIG) {
231
+ const pricing = input.pricing;
232
+ if (input.products.isHub) {
233
+ return hubQuote(pricing?.hubSchools ?? input.hubSchools ?? 0, pricing, config);
234
+ }
235
+ const wex = Boolean(input.products.wex);
236
+ const noYearGroupQuoted = pricing?.wexY10 === undefined && pricing?.wexY12 === undefined;
237
+ return schoolQuote({
238
+ students: pricing?.students ?? input.students,
239
+ wexY10: wex ? (noYearGroupQuoted ? 0 : pricing?.wexY10) : undefined,
240
+ wexY12: wex ? pricing?.wexY12 : undefined,
241
+ events: Boolean(input.products.events),
242
+ eventsDiscount: pricing?.eventsDiscount,
243
+ destinations: Boolean(input.products.destinations),
244
+ destinationsDiscount: pricing?.destinationsDiscount,
245
+ fullPlatformDiscount: pricing?.fullPlatformDiscount,
246
+ bespokeDiscount: pricing?.bespokeDiscount,
247
+ quoteOverride: pricing?.quoteOverride,
248
+ }, config);
249
+ }
250
+ /**
251
+ * Reduce a quote to what the customer sees: the fees, and one discount figure.
252
+ *
253
+ * The base fee isn't itemised to customers, so it is folded into the first fee
254
+ * they are shown — and where there is nothing to fold it into, it stands alone
255
+ * as the platform fee it is.
256
+ *
257
+ * @param {object} quote the priced institute.
258
+ * @return {object} the snapshot to store on the institute.
259
+ */
260
+ function quoteSnapshot(quote) {
261
+ const itemised = quote.lines.filter((l) => l.key !== "base");
262
+ const base = quote.lines.find((l) => l.key === "base")?.amount || 0;
263
+ const lines = itemised.length ?
264
+ itemised.map((line, index) => ({
265
+ key: line.key,
266
+ label: line.label,
267
+ amount: index === 0 ? line.amount + base : line.amount,
268
+ })) :
269
+ [{ key: "base", label: "Platform fee", amount: base }];
270
+ const subtotal = sum(lines.map((l) => l.amount));
271
+ return {
272
+ lines,
273
+ subtotal,
274
+ // The customer is shown the fees and one discount, so whatever separates
275
+ // the two — the whole-quote discounts, an agreed price below them — is
276
+ // that one figure, and a total above the fees is flagged instead.
277
+ discountTotal: Math.max(subtotal - quote.total, 0),
278
+ minimum: quote.total > subtotal,
279
+ total: quote.total,
280
+ updated: new Date().toISOString().split("T")[0],
281
+ };
282
+ }
156
283
  //# sourceMappingURL=pricing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
@@ -116,4 +116,82 @@ describe("mergePricingConfig", () => {
116
116
  expect(merged.tiers[2]).toEqual(pricing_1.DEFAULT_PRICING_CONFIG.tiers[2]);
117
117
  });
118
118
  });
119
+ describe("the negotiated terms", () => {
120
+ it("takes the bespoke discount after the full-platform one", () => {
121
+ const base = { students: 2000, events: true, destinations: true, fullPlatformDiscount: true };
122
+ const plain = (0, pricing_1.calculateSchoolPrice)(base);
123
+ const cut = (0, pricing_1.calculateSchoolPrice)({ ...base, bespokeDiscount: 0.1 });
124
+ expect(cut.total).toBeCloseTo(plain.total * 0.9, 6);
125
+ expect(cut.bespokeDiscountApplied).toBeCloseTo(plain.total * 0.1, 6);
126
+ });
127
+ it("still holds the minimum fee against a bespoke discount", () => {
128
+ const result = (0, pricing_1.calculateSchoolPrice)({ students: 900, events: true, bespokeDiscount: 0.9 });
129
+ expect(result.total).toBe(pricing_1.DEFAULT_PRICING_CONFIG.minFee);
130
+ expect(result.minFeeApplied).toBe(true);
131
+ });
132
+ it("lets an agreed price replace the calculation, floor and all", () => {
133
+ const result = (0, pricing_1.calculateSchoolPrice)({ students: 2000, events: true, quoteOverride: 100 });
134
+ expect(result.total).toBe(100);
135
+ expect(result.overrideApplied).toBe(true);
136
+ // The floor didn't set the price, so it isn't reported as having done.
137
+ expect(result.minFeeApplied).toBe(false);
138
+ });
139
+ });
140
+ describe("instituteQuote", () => {
141
+ const products = (over = {}) => ({ wex: false, events: false, destinations: false, isHub: false, ...over });
142
+ it("prices a school on what it has, at the roll agreed with it", () => {
143
+ const quote = (0, pricing_1.instituteQuote)({
144
+ products: products({ events: true }),
145
+ students: 3000,
146
+ pricing: { students: 1250, eventsDiscount: true },
147
+ });
148
+ expect(quote.kind).toBe("school");
149
+ expect(quote.total).toBeCloseTo(830, 2);
150
+ });
151
+ it("charges a work experience school nothing until its year groups are quoted", () => {
152
+ const quote = (0, pricing_1.instituteQuote)({ products: products({ wex: true }), students: 1000 });
153
+ expect(quote.lines.map((l) => l.key)).toEqual(["base", "wexY10"]);
154
+ expect(quote.lines.find((l) => l.key === "wexY10")?.amount).toBe(0);
155
+ });
156
+ it("leaves out a year group once the other one is quoted", () => {
157
+ const quote = (0, pricing_1.instituteQuote)({ products: products({ wex: true }), pricing: { wexY12: 100 } });
158
+ expect(quote.lines.map((l) => l.key)).toEqual(["base", "wexY12"]);
159
+ });
160
+ it("prices a hub on its schools, and honours an agreed price", () => {
161
+ const quote = (0, pricing_1.instituteQuote)({ products: products({ isHub: true }), pricing: { hubSchools: 50 } });
162
+ expect(quote.kind).toBe("hub");
163
+ expect(quote.total).toBe(9500);
164
+ expect((0, pricing_1.instituteQuote)({
165
+ products: products({ isHub: true }),
166
+ pricing: { hubSchools: 50, quoteOverride: 8000 },
167
+ }).total).toBe(8000);
168
+ });
169
+ });
170
+ describe("quoteSnapshot", () => {
171
+ it("folds the base fee into the first fee the customer is shown", () => {
172
+ const snapshot = (0, pricing_1.quoteSnapshot)((0, pricing_1.instituteQuote)({
173
+ products: { events: true },
174
+ pricing: { students: 2500 },
175
+ }));
176
+ // 1,100 of events plus the 250 base, shown as one events line.
177
+ expect(snapshot.lines).toEqual([{ key: "events", label: "Employer events", amount: 1350 }]);
178
+ expect(snapshot.total).toBe(1350);
179
+ expect(snapshot.discountTotal).toBe(0);
180
+ });
181
+ it("reports everything between the fees and the total as one discount", () => {
182
+ const quote = (0, pricing_1.instituteQuote)({
183
+ products: { events: true, destinations: true },
184
+ pricing: { students: 2000, fullPlatformDiscount: true, bespokeDiscount: 0.1 },
185
+ });
186
+ const snapshot = (0, pricing_1.quoteSnapshot)(quote);
187
+ expect(snapshot.subtotal - snapshot.discountTotal).toBeCloseTo(snapshot.total, 6);
188
+ expect(snapshot.minimum).toBe(false);
189
+ });
190
+ it("flags a total the fees don't add up to, rather than inventing a discount", () => {
191
+ const snapshot = (0, pricing_1.quoteSnapshot)((0, pricing_1.instituteQuote)({ products: { events: true }, pricing: { students: 90 } }));
192
+ expect(snapshot.total).toBe(pricing_1.DEFAULT_PRICING_CONFIG.minFee);
193
+ expect(snapshot.minimum).toBe(true);
194
+ expect(snapshot.discountTotal).toBe(0);
195
+ });
196
+ });
119
197
  //# sourceMappingURL=pricing.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.test.js","sourceRoot":"","sources":["../src/pricing.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pricing.test.js","sourceRoot":"","sources":["../src/pricing.test.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { Descendant } from "slate";
3
3
  import { emailTemplates } from "./constants";
4
4
  import type { ChecklistItem, GiftHistory } from "./customerCareConfig";
5
5
  import type { SurveyGroupSetupMap, SurveyKind, SurveySlot } from "./surveys/types";
6
+ import type { InstitutePricing } from "./pricing";
6
7
  export type FilterObject = {
7
8
  [key: string]: {
8
9
  label: string;
@@ -1055,6 +1056,10 @@ export type InstituteData = {
1055
1056
  blockAllEmails?: boolean;
1056
1057
  splitBilling?: boolean;
1057
1058
  adminBillingContactId?: string;
1059
+ /** The terms agreed with this customer, and the quote they came to. Written
1060
+ * from the admin panel; the institute's own billing page reads the quote
1061
+ * snapshot inside it rather than pricing itself. See ./pricing. */
1062
+ pricing?: InstitutePricing;
1058
1063
  employerEvents?: boolean;
1059
1064
  workExperience?: boolean;
1060
1065
  network?: boolean;
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.1052",
5
+ "version": "1.400.1053",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
8
8
  "scripts": {
@@ -1,4 +1,4 @@
1
- import {calculateHubPrice, calculateSchoolPrice, DEFAULT_PRICING_CONFIG, mergePricingConfig, SchoolPricingInput} from "./pricing";
1
+ import {calculateHubPrice, calculateSchoolPrice, DEFAULT_PRICING_CONFIG, InstituteQuoteInput, instituteQuote, mergePricingConfig, quoteSnapshot, SchoolPricingInput} from "./pricing";
2
2
 
3
3
  /**
4
4
  * The worked examples from the 2026 pricing sheet, kept here as the contract:
@@ -145,3 +145,103 @@ describe("mergePricingConfig", () => {
145
145
  expect(merged.tiers[2]).toEqual(DEFAULT_PRICING_CONFIG.tiers[2]);
146
146
  });
147
147
  });
148
+
149
+ describe("the negotiated terms", () => {
150
+ it("takes the bespoke discount after the full-platform one", () => {
151
+ const base: SchoolPricingInput = {students: 2000, events: true, destinations: true, fullPlatformDiscount: true};
152
+ const plain = calculateSchoolPrice(base);
153
+ const cut = calculateSchoolPrice({...base, bespokeDiscount: 0.1});
154
+
155
+ expect(cut.total).toBeCloseTo(plain.total * 0.9, 6);
156
+ expect(cut.bespokeDiscountApplied).toBeCloseTo(plain.total * 0.1, 6);
157
+ });
158
+
159
+ it("still holds the minimum fee against a bespoke discount", () => {
160
+ const result = calculateSchoolPrice({students: 900, events: true, bespokeDiscount: 0.9});
161
+
162
+ expect(result.total).toBe(DEFAULT_PRICING_CONFIG.minFee);
163
+ expect(result.minFeeApplied).toBe(true);
164
+ });
165
+
166
+ it("lets an agreed price replace the calculation, floor and all", () => {
167
+ const result = calculateSchoolPrice({students: 2000, events: true, quoteOverride: 100});
168
+
169
+ expect(result.total).toBe(100);
170
+ expect(result.overrideApplied).toBe(true);
171
+ // The floor didn't set the price, so it isn't reported as having done.
172
+ expect(result.minFeeApplied).toBe(false);
173
+ });
174
+ });
175
+
176
+ describe("instituteQuote", () => {
177
+ const products = (over: Partial<InstituteQuoteInput["products"]> = {}) =>
178
+ ({wex: false, events: false, destinations: false, isHub: false, ...over});
179
+
180
+ it("prices a school on what it has, at the roll agreed with it", () => {
181
+ const quote = instituteQuote({
182
+ products: products({events: true}),
183
+ students: 3000,
184
+ pricing: {students: 1250, eventsDiscount: true},
185
+ });
186
+
187
+ expect(quote.kind).toBe("school");
188
+ expect(quote.total).toBeCloseTo(830, 2);
189
+ });
190
+
191
+ it("charges a work experience school nothing until its year groups are quoted", () => {
192
+ const quote = instituteQuote({products: products({wex: true}), students: 1000});
193
+
194
+ expect(quote.lines.map((l) => l.key)).toEqual(["base", "wexY10"]);
195
+ expect(quote.lines.find((l) => l.key === "wexY10")?.amount).toBe(0);
196
+ });
197
+
198
+ it("leaves out a year group once the other one is quoted", () => {
199
+ const quote = instituteQuote({products: products({wex: true}), pricing: {wexY12: 100}});
200
+
201
+ expect(quote.lines.map((l) => l.key)).toEqual(["base", "wexY12"]);
202
+ });
203
+
204
+ it("prices a hub on its schools, and honours an agreed price", () => {
205
+ const quote = instituteQuote({products: products({isHub: true}), pricing: {hubSchools: 50}});
206
+
207
+ expect(quote.kind).toBe("hub");
208
+ expect(quote.total).toBe(9500);
209
+ expect(instituteQuote({
210
+ products: products({isHub: true}),
211
+ pricing: {hubSchools: 50, quoteOverride: 8000},
212
+ }).total).toBe(8000);
213
+ });
214
+ });
215
+
216
+ describe("quoteSnapshot", () => {
217
+ it("folds the base fee into the first fee the customer is shown", () => {
218
+ const snapshot = quoteSnapshot(instituteQuote({
219
+ products: {events: true},
220
+ pricing: {students: 2500},
221
+ }));
222
+
223
+ // 1,100 of events plus the 250 base, shown as one events line.
224
+ expect(snapshot.lines).toEqual([{key: "events", label: "Employer events", amount: 1350}]);
225
+ expect(snapshot.total).toBe(1350);
226
+ expect(snapshot.discountTotal).toBe(0);
227
+ });
228
+
229
+ it("reports everything between the fees and the total as one discount", () => {
230
+ const quote = instituteQuote({
231
+ products: {events: true, destinations: true},
232
+ pricing: {students: 2000, fullPlatformDiscount: true, bespokeDiscount: 0.1},
233
+ });
234
+ const snapshot = quoteSnapshot(quote);
235
+
236
+ expect(snapshot.subtotal - snapshot.discountTotal).toBeCloseTo(snapshot.total, 6);
237
+ expect(snapshot.minimum).toBe(false);
238
+ });
239
+
240
+ it("flags a total the fees don't add up to, rather than inventing a discount", () => {
241
+ const snapshot = quoteSnapshot(instituteQuote({products: {events: true}, pricing: {students: 90}}));
242
+
243
+ expect(snapshot.total).toBe(DEFAULT_PRICING_CONFIG.minFee);
244
+ expect(snapshot.minimum).toBe(true);
245
+ expect(snapshot.discountTotal).toBe(0);
246
+ });
247
+ });
package/src/pricing.ts CHANGED
@@ -78,9 +78,16 @@ export type SchoolPricingInput = {
78
78
  destinationsDiscount?: boolean,
79
79
  /** Discount the whole quote, for a school taking the full platform. */
80
80
  fullPlatformDiscount?: boolean,
81
+ /** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off).
82
+ * Taken after the full-platform discount and before the minimum fee. */
83
+ bespokeDiscount?: number,
84
+ /** A price agreed with the customer, which replaces everything above it —
85
+ * the discounts, the minimum fee and the total the products came to. */
86
+ quoteOverride?: number,
81
87
  };
82
88
 
83
- export type PricingLineKey = "base"|"wexY10"|"wexY12"|"events"|"destinations";
89
+ export type PricingLineKey =
90
+ "base"|"wexY10"|"wexY12"|"events"|"destinations"|"hubBase"|"hubSchools";
84
91
 
85
92
  export type PricingLine = {
86
93
  key: PricingLineKey,
@@ -109,8 +116,12 @@ export type SchoolPricingResult = {
109
116
  subtotal: number,
110
117
  /** £ taken off the subtotal by the full-platform discount. */
111
118
  fullPlatformDiscountApplied: number,
119
+ /** £ taken off by the negotiated discount, after the full-platform one. */
120
+ bespokeDiscountApplied: number,
112
121
  /** True when the minimum fee, not the products, set the price. */
113
122
  minFeeApplied: boolean,
123
+ /** True when an agreed price replaced the calculated one. */
124
+ overrideApplied: boolean,
114
125
  total: number,
115
126
  };
116
127
 
@@ -160,6 +171,8 @@ function graduated(quantity: number, bands: {limit: number, rate: number}[]): Pr
160
171
 
161
172
  const sum = (values: number[]) => values.reduce((total, value) => total + value, 0);
162
173
 
174
+ const clampFraction = (value?: number) => Math.min(Math.max(value || 0, 0), 1);
175
+
163
176
  const bandsTotal = (bands: PricingBand[]) => sum(bands.map((b) => b.amount));
164
177
 
165
178
  /**
@@ -205,14 +218,20 @@ export function calculateSchoolPrice(
205
218
 
206
219
  const subtotal = sum(lines.map((l) => l.amount));
207
220
  const discounted = input.fullPlatformDiscount ? subtotal * (1 - config.fullPlatformDiscount) : subtotal;
208
- const total = Math.max(discounted, config.minFee);
221
+ const negotiated = discounted * (1 - clampFraction(input.bespokeDiscount));
222
+ const floored = Math.max(negotiated, config.minFee);
223
+ const overridden = input.quoteOverride !== undefined && input.quoteOverride >= 0;
209
224
 
210
225
  return {
211
226
  lines,
212
227
  subtotal,
213
228
  fullPlatformDiscountApplied: subtotal - discounted,
214
- minFeeApplied: total > discounted,
215
- total,
229
+ bespokeDiscountApplied: discounted - negotiated,
230
+ // An agreed price is the price, so neither the floor nor the discounts
231
+ // are what set it — they are only reported when the calculation stands.
232
+ minFeeApplied: !overridden && floored > negotiated,
233
+ overrideApplied: overridden,
234
+ total: overridden ? (input.quoteOverride as number) : floored,
216
235
  };
217
236
  }
218
237
 
@@ -254,3 +273,224 @@ export function mergePricingConfig(stored?: Partial<PricingConfig>): PricingConf
254
273
  hub: {...d.hub, ...stored.hub},
255
274
  };
256
275
  }
276
+
277
+ /* -------------------------------------------------------------------------
278
+ * An institute's own quote.
279
+ *
280
+ * The calculator above prices a hypothetical school. What an actual customer
281
+ * pays is that calculation plus the terms agreed with them — the year groups
282
+ * and roll we quoted on, which discounts we gave, and any price we simply shook
283
+ * hands on — so those live on the institute, in `InstitutePricing`, and the
284
+ * quote is worked out from the two together.
285
+ *
286
+ * A school never reads the pricing configuration itself (it is admin data, and
287
+ * a school has no business seeing the tiers), so the agreed quote is snapshotted
288
+ * onto the institute as `InstitutePricing.quote` whenever an admin saves. That
289
+ * snapshot is what the school's own billing page shows.
290
+ * ---------------------------------------------------------------------- */
291
+
292
+ /** The terms agreed with one customer. Stored at `institutes/{oId}.pricing`. */
293
+ export type InstitutePricing = {
294
+ /** The roll we quoted on. Defaults to the institute's own student count. */
295
+ students?: number,
296
+ /** Y10 placements quoted. Undefined means the year group isn't on work experience. */
297
+ wexY10?: number,
298
+ /** Y12 placements quoted. Undefined means the year group isn't on work experience. */
299
+ wexY12?: number,
300
+ /** Take the partial-product discount off the events fee. */
301
+ eventsDiscount?: boolean,
302
+ /** Take the partial-product discount off the destinations fee. */
303
+ destinationsDiscount?: boolean,
304
+ /** Take the full-platform discount off the whole quote. */
305
+ fullPlatformDiscount?: boolean,
306
+ /** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off). */
307
+ bespokeDiscount?: number,
308
+ /** Why the negotiated discount or the agreed price was given. Admin-only. */
309
+ discountReason?: string,
310
+ /** A price agreed with the customer, which replaces the calculated one. */
311
+ quoteOverride?: number,
312
+ /** Schools on the hub. Hub accounts only. */
313
+ hubSchools?: number,
314
+ /** The quote as last saved, for the customer to see. */
315
+ quote?: InstituteQuoteSnapshot,
316
+ };
317
+
318
+ /** A priced institute: the same shape for a school, a trust and a hub. */
319
+ export type InstituteQuote = {
320
+ kind: "school"|"hub",
321
+ /** Every fee charged, with the working behind it. */
322
+ lines: PricingLine[],
323
+ /** The lines added up, before any whole-quote discount. */
324
+ subtotal: number,
325
+ fullPlatformDiscountApplied: number,
326
+ bespokeDiscountApplied: number,
327
+ minFeeApplied: boolean,
328
+ overrideApplied: boolean,
329
+ total: number,
330
+ };
331
+
332
+ /** What the customer is shown: the total, and what makes it up. No rates. */
333
+ export type InstituteQuoteSnapshot = {
334
+ lines: {key: PricingLineKey, label: string, amount: number}[],
335
+ /** The lines added up. */
336
+ subtotal: number,
337
+ /** Everything taken off the lines to reach the total, as one figure. */
338
+ discountTotal: number,
339
+ /** True when the total is a floor or an agreed price above what the lines
340
+ * come to, so the breakdown is shown as an illustration, not a sum. */
341
+ minimum: boolean,
342
+ total: number,
343
+ /** ISO date the quote was last worked out. */
344
+ updated: string,
345
+ };
346
+
347
+ /** What the quote needs to know about the institute being priced. */
348
+ export type InstituteQuoteInput = {
349
+ /** What the institute has, as resolved by `resolveProducts`. */
350
+ products: {wex?: boolean, events?: boolean, destinations?: boolean, isHub?: boolean},
351
+ /** The institute's roll, when the agreed terms don't name one. */
352
+ students?: number,
353
+ /** Schools on the hub, when the agreed terms don't name a number. */
354
+ hubSchools?: number,
355
+ /** The terms agreed with this customer. */
356
+ pricing?: InstitutePricing,
357
+ };
358
+
359
+ /**
360
+ * Price a school and present it as an institute quote.
361
+ *
362
+ * @param {object} input the roll, the year groups, the products and the terms.
363
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
364
+ * @return {object} the quote, ready to render.
365
+ */
366
+ export function schoolQuote(
367
+ input: SchoolPricingInput,
368
+ config: PricingConfig = DEFAULT_PRICING_CONFIG,
369
+ ): InstituteQuote {
370
+ return {kind: "school", ...calculateSchoolPrice(input, config)};
371
+ }
372
+
373
+ /**
374
+ * Price a careers hub as an institute quote: the platform fee, then the schools.
375
+ *
376
+ * Hubs have no products to discount individually and no minimum fee to fall back
377
+ * on, so the only terms that reach them are the negotiated discount and an
378
+ * agreed price.
379
+ *
380
+ * @param {number} schools how many schools the hub brings on.
381
+ * @param {object} [pricing] the terms agreed with this hub.
382
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
383
+ * @return {object} the quote, ready to render.
384
+ */
385
+ export function hubQuote(
386
+ schools: number,
387
+ pricing?: InstitutePricing,
388
+ config: PricingConfig = DEFAULT_PRICING_CONFIG,
389
+ ): InstituteQuote {
390
+ const count = Math.max(schools || 0, 0);
391
+ const hub = calculateHubPrice(count, config);
392
+
393
+ const lines: PricingLine[] = [
394
+ {key: "hubBase", label: "Platform fee", amount: hub.baseFee, bands: []},
395
+ {
396
+ key: "hubSchools",
397
+ label: "School fees",
398
+ amount: hub.schoolFees,
399
+ bands: count > 0 ?
400
+ [{tier: 1, quantity: count, rate: config.hub.perSchoolFee, amount: hub.schoolFees}] :
401
+ [],
402
+ },
403
+ ];
404
+
405
+ const negotiated = hub.total * (1 - clampFraction(pricing?.bespokeDiscount));
406
+ const overridden = pricing?.quoteOverride !== undefined && pricing.quoteOverride >= 0;
407
+
408
+ return {
409
+ kind: "hub",
410
+ lines,
411
+ subtotal: hub.total,
412
+ fullPlatformDiscountApplied: 0,
413
+ bespokeDiscountApplied: hub.total - negotiated,
414
+ minFeeApplied: false,
415
+ overrideApplied: overridden,
416
+ total: overridden ? (pricing?.quoteOverride as number) : negotiated,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Price an institute on what it actually has and what was agreed with it.
422
+ *
423
+ * The products come from `resolveProducts` rather than the raw flags, so a trust
424
+ * is priced on the union of its schools' products the same way it is gated on
425
+ * them. A year group with no placements agreed is a year group not on work
426
+ * experience — except where the institute has work experience and neither year
427
+ * group has been filled in, which is a quote waiting to be finished rather than
428
+ * a school not taking the product, so Y10 is charged at zero to say so.
429
+ *
430
+ * @param {object} input the institute's products, roll and agreed terms.
431
+ * @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
432
+ * @return {object} the quote, ready to render.
433
+ */
434
+ export function instituteQuote(
435
+ input: InstituteQuoteInput,
436
+ config: PricingConfig = DEFAULT_PRICING_CONFIG,
437
+ ): InstituteQuote {
438
+ const pricing = input.pricing;
439
+
440
+ if (input.products.isHub) {
441
+ return hubQuote(pricing?.hubSchools ?? input.hubSchools ?? 0, pricing, config);
442
+ }
443
+
444
+ const wex = Boolean(input.products.wex);
445
+ const noYearGroupQuoted = pricing?.wexY10 === undefined && pricing?.wexY12 === undefined;
446
+
447
+ return schoolQuote({
448
+ students: pricing?.students ?? input.students,
449
+ wexY10: wex ? (noYearGroupQuoted ? 0 : pricing?.wexY10) : undefined,
450
+ wexY12: wex ? pricing?.wexY12 : undefined,
451
+ events: Boolean(input.products.events),
452
+ eventsDiscount: pricing?.eventsDiscount,
453
+ destinations: Boolean(input.products.destinations),
454
+ destinationsDiscount: pricing?.destinationsDiscount,
455
+ fullPlatformDiscount: pricing?.fullPlatformDiscount,
456
+ bespokeDiscount: pricing?.bespokeDiscount,
457
+ quoteOverride: pricing?.quoteOverride,
458
+ }, config);
459
+ }
460
+
461
+ /**
462
+ * Reduce a quote to what the customer sees: the fees, and one discount figure.
463
+ *
464
+ * The base fee isn't itemised to customers, so it is folded into the first fee
465
+ * they are shown — and where there is nothing to fold it into, it stands alone
466
+ * as the platform fee it is.
467
+ *
468
+ * @param {object} quote the priced institute.
469
+ * @return {object} the snapshot to store on the institute.
470
+ */
471
+ export function quoteSnapshot(quote: InstituteQuote): InstituteQuoteSnapshot {
472
+ const itemised = quote.lines.filter((l) => l.key !== "base");
473
+ const base = quote.lines.find((l) => l.key === "base")?.amount || 0;
474
+
475
+ const lines = itemised.length ?
476
+ itemised.map((line, index) => ({
477
+ key: line.key,
478
+ label: line.label,
479
+ amount: index === 0 ? line.amount + base : line.amount,
480
+ })) :
481
+ [{key: "base" as PricingLineKey, label: "Platform fee", amount: base}];
482
+
483
+ const subtotal = sum(lines.map((l) => l.amount));
484
+
485
+ return {
486
+ lines,
487
+ subtotal,
488
+ // The customer is shown the fees and one discount, so whatever separates
489
+ // the two — the whole-quote discounts, an agreed price below them — is
490
+ // that one figure, and a total above the fees is flagged instead.
491
+ discountTotal: Math.max(subtotal - quote.total, 0),
492
+ minimum: quote.total > subtotal,
493
+ total: quote.total,
494
+ updated: new Date().toISOString().split("T")[0],
495
+ };
496
+ }
@@ -3,6 +3,7 @@ import {Descendant} from "slate";
3
3
  import {emailTemplates} from "./constants";
4
4
  import type {ChecklistItem, GiftHistory} from "./customerCareConfig";
5
5
  import type {SurveyGroupSetupMap, SurveyKind, SurveySlot} from "./surveys/types";
6
+ import type {InstitutePricing} from "./pricing";
6
7
 
7
8
  export type FilterObject = {
8
9
  [key: string]: {
@@ -1077,6 +1078,10 @@ export type InstituteData = {
1077
1078
  blockAllEmails?: boolean,
1078
1079
  splitBilling?: boolean,
1079
1080
  adminBillingContactId?: string,
1081
+ /** The terms agreed with this customer, and the quote they came to. Written
1082
+ * from the admin panel; the institute's own billing page reads the quote
1083
+ * snapshot inside it rather than pricing itself. See ./pricing. */
1084
+ pricing?: InstitutePricing,
1080
1085
  // Packages institutes can be signed up to
1081
1086
  employerEvents?: boolean,
1082
1087
  workExperience?: boolean,