placementt-core 1.400.1071 → 1.400.1072
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 +175 -0
- package/lib/pricing.js +117 -0
- package/lib/pricing.js.map +1 -1
- package/package.json +1 -1
- package/src/pricing.ts +204 -1
package/lib/pricing.d.ts
CHANGED
|
@@ -1,24 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 2026 pricing model.
|
|
3
|
+
*
|
|
4
|
+
* A quote is a base fee plus a per-product fee, and every per-product fee is
|
|
5
|
+
* graduated: the tiers are bands, not lookups, so a school with 1,250 students
|
|
6
|
+
* pays the tier 1 rate on its first 1,000 and the tier 2 rate on the next 250.
|
|
7
|
+
* Anything above the final threshold keeps paying the final tier's rate.
|
|
8
|
+
*
|
|
9
|
+
* Work experience is charged per placement (Y10 and Y12 are quoted separately,
|
|
10
|
+
* and a year group left blank is a year group not taking the product); events
|
|
11
|
+
* and destinations are charged per student. Only events and destinations carry
|
|
12
|
+
* a partial-product discount — that is how the pricing sheet has it, and it is
|
|
13
|
+
* why `partialProductDiscount` names those two rather than being one number.
|
|
14
|
+
*
|
|
15
|
+
* A trust is priced as one account on the average school terms, multiplied by
|
|
16
|
+
* the number of schools it brings on, and then earns a discount on the whole
|
|
17
|
+
* quote — a smaller one below the threshold, a larger one at or above it.
|
|
18
|
+
*
|
|
19
|
+
* Every figure lives in `PricingConfig` so it can be stored in Firestore and
|
|
20
|
+
* adjusted without a deploy. `DEFAULT_PRICING_CONFIG` is the fallback and the
|
|
21
|
+
* seed for that document.
|
|
22
|
+
*/
|
|
23
|
+
/** One band of the graduated fee, with the rate each product charges in it. */
|
|
1
24
|
export type PricingTier = {
|
|
25
|
+
/** Upper bound of the band, in students (events and destinations). */
|
|
2
26
|
students: number;
|
|
27
|
+
/** Upper bound of the band, in placements (work experience). */
|
|
3
28
|
placements: number;
|
|
29
|
+
/** £ per placement. */
|
|
4
30
|
wex: number;
|
|
31
|
+
/** £ per student. */
|
|
5
32
|
events: number;
|
|
33
|
+
/** £ per student. */
|
|
6
34
|
destinations: number;
|
|
7
35
|
};
|
|
8
36
|
export type PricingConfig = {
|
|
37
|
+
/** No school is quoted below this, however little the products come to. */
|
|
9
38
|
minFee: number;
|
|
39
|
+
/** Charged on every quote. Not itemised to the customer. */
|
|
10
40
|
baseFee: number;
|
|
41
|
+
/** In ascending order of threshold. The last band's rate runs to infinity. */
|
|
11
42
|
tiers: PricingTier[];
|
|
43
|
+
/** Taken off a single product's fee, as a fraction (0.2 = 20% off). */
|
|
12
44
|
partialProductDiscount: {
|
|
13
45
|
events: number;
|
|
14
46
|
destinations: number;
|
|
15
47
|
};
|
|
48
|
+
/** Taken off the whole quote when a school takes the full platform. */
|
|
16
49
|
fullPlatformDiscount: number;
|
|
50
|
+
/** Taken off the whole quote for a trust, on how many schools it brings. */
|
|
17
51
|
trust: {
|
|
52
|
+
/** Schools at which the larger discount takes over. */
|
|
18
53
|
threshold: number;
|
|
54
|
+
/** Fraction off a trust below the threshold. */
|
|
19
55
|
discount: number;
|
|
56
|
+
/** Fraction off a trust at or above it. */
|
|
20
57
|
largeDiscount: number;
|
|
21
58
|
};
|
|
59
|
+
/** Careers hubs are a flat platform fee plus a fee for each school. */
|
|
22
60
|
hub: {
|
|
23
61
|
baseFee: number;
|
|
24
62
|
perSchoolFee: number;
|
|
@@ -26,93 +64,182 @@ export type PricingConfig = {
|
|
|
26
64
|
};
|
|
27
65
|
/** The pricing sheet's figures, as agreed for 2026. */
|
|
28
66
|
export declare const DEFAULT_PRICING_CONFIG: PricingConfig;
|
|
67
|
+
/** Where the adjustable copy of the config lives. */
|
|
29
68
|
export declare const PRICING_CONFIG_PATH: string[];
|
|
30
69
|
export type SchoolPricingInput = {
|
|
70
|
+
/** Roll size. Drives the events and destinations fees. */
|
|
31
71
|
students?: number;
|
|
72
|
+
/** Y10 placements. Undefined means the year group isn't taking work experience. */
|
|
32
73
|
wexY10?: number;
|
|
74
|
+
/** Y12 placements. Undefined means the year group isn't taking work experience. */
|
|
33
75
|
wexY12?: number;
|
|
34
76
|
events?: boolean;
|
|
77
|
+
/** Discount the events fee on its own. */
|
|
35
78
|
eventsDiscount?: boolean;
|
|
36
79
|
destinations?: boolean;
|
|
80
|
+
/** Discount the destinations fee on its own. */
|
|
37
81
|
destinationsDiscount?: boolean;
|
|
82
|
+
/** Discount the whole quote, for a school taking the full platform. */
|
|
38
83
|
fullPlatformDiscount?: boolean;
|
|
84
|
+
/** Schools in the trust being quoted. When a trust is quoted, its school-level
|
|
85
|
+
* terms are multiplied by this count before any trust discount is taken. */
|
|
39
86
|
schools?: number;
|
|
87
|
+
/** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off).
|
|
88
|
+
* Taken after the full-platform discount and before the minimum fee. */
|
|
40
89
|
bespokeDiscount?: number;
|
|
90
|
+
/** A price agreed with the customer, which replaces everything above it —
|
|
91
|
+
* the discounts, the minimum fee and the total the products came to. */
|
|
41
92
|
quoteOverride?: number;
|
|
42
93
|
};
|
|
43
94
|
export type PricingLineKey = "base" | "wexY10" | "wexY12" | "events" | "destinations" | "hubBase" | "hubSchools";
|
|
44
95
|
export type PricingLine = {
|
|
45
96
|
key: PricingLineKey;
|
|
46
97
|
label: string;
|
|
98
|
+
/** What the product costs after any partial-product discount. */
|
|
47
99
|
amount: number;
|
|
100
|
+
/** The fee before its partial-product discount, when one was applied. */
|
|
48
101
|
undiscounted?: number;
|
|
102
|
+
/** How the amount was reached, band by band. Empty for the base fee. */
|
|
49
103
|
bands: PricingBand[];
|
|
50
104
|
};
|
|
105
|
+
/** One band's contribution to a product's fee, for showing the working. */
|
|
51
106
|
export type PricingBand = {
|
|
107
|
+
/** 1-indexed, matching the tier's position in the config. */
|
|
52
108
|
tier: number;
|
|
109
|
+
/** Units charged in this band. */
|
|
53
110
|
quantity: number;
|
|
54
111
|
rate: number;
|
|
55
112
|
amount: number;
|
|
56
113
|
};
|
|
57
114
|
export type SchoolPricingResult = {
|
|
58
115
|
lines: PricingLine[];
|
|
116
|
+
/** The lines added up, before the full-platform discount. */
|
|
59
117
|
subtotal: number;
|
|
118
|
+
/** £ taken off the subtotal by the full-platform discount. */
|
|
60
119
|
fullPlatformDiscountApplied: number;
|
|
120
|
+
/** £ taken off by the trust discount, after the full-platform one. */
|
|
61
121
|
trustDiscountApplied: number;
|
|
122
|
+
/** The rate that trust discount was taken at, as a fraction. */
|
|
62
123
|
trustDiscountRate: number;
|
|
124
|
+
/** £ taken off by the negotiated discount, after the trust one. */
|
|
63
125
|
bespokeDiscountApplied: number;
|
|
126
|
+
/** True when the minimum fee, not the products, set the price. */
|
|
64
127
|
minFeeApplied: boolean;
|
|
128
|
+
/** True when an agreed price replaced the calculated one. */
|
|
65
129
|
overrideApplied: boolean;
|
|
66
130
|
total: number;
|
|
67
131
|
};
|
|
68
132
|
export type HubPricingResult = {
|
|
69
133
|
baseFee: number;
|
|
134
|
+
/** The per-school fee multiplied by the number of schools. */
|
|
70
135
|
schoolFees: number;
|
|
71
136
|
total: number;
|
|
72
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* What a trust comes off its quote for its size.
|
|
140
|
+
*
|
|
141
|
+
* A trust is more than one school, so a count below two is a single school and
|
|
142
|
+
* earns nothing — the discount is for bringing schools with you.
|
|
143
|
+
*
|
|
144
|
+
* @param {number} [schools] schools in the trust.
|
|
145
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
146
|
+
* @return {number} the discount, as a fraction of the quote.
|
|
147
|
+
*/
|
|
73
148
|
export declare function trustDiscountRate(schools?: number, config?: PricingConfig): number;
|
|
149
|
+
/**
|
|
150
|
+
* Price a school, trust or hub-school quote.
|
|
151
|
+
*
|
|
152
|
+
* Products the school isn't taking are left out of `lines` entirely, so the
|
|
153
|
+
* breakdown only ever shows what is being charged for.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} input the roll, the year groups and the products taken.
|
|
156
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
157
|
+
* @return {object} the total, and the working that reached it.
|
|
158
|
+
*/
|
|
74
159
|
export declare function calculateSchoolPrice(input: SchoolPricingInput, config?: PricingConfig): SchoolPricingResult;
|
|
160
|
+
/**
|
|
161
|
+
* Price a careers hub: the platform fee, plus a fee for every school on it.
|
|
162
|
+
*
|
|
163
|
+
* @param {number} schools how many schools the hub brings on.
|
|
164
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
165
|
+
* @return {object} the platform fee, the school fees and the total.
|
|
166
|
+
*/
|
|
75
167
|
export declare function calculateHubPrice(schools: number, config?: PricingConfig): HubPricingResult;
|
|
168
|
+
/**
|
|
169
|
+
* Fill a stored config out with the defaults.
|
|
170
|
+
*
|
|
171
|
+
* The Firestore document is hand-edited, so treat every branch of it as
|
|
172
|
+
* possibly absent — a config saved before a tier was added should still price,
|
|
173
|
+
* rather than throwing on a missing rate.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} [stored] whatever the Firestore document holds.
|
|
176
|
+
* @return {object} a complete config, safe to price with.
|
|
177
|
+
*/
|
|
76
178
|
export declare function mergePricingConfig(stored?: Partial<PricingConfig>): PricingConfig;
|
|
179
|
+
/** The terms agreed with one customer. Stored at `institutes/{oId}.pricing`. */
|
|
77
180
|
export type InstitutePricing = {
|
|
181
|
+
/** The roll we quoted on. Defaults to the institute's own student count. */
|
|
78
182
|
students?: number;
|
|
183
|
+
/** Y10 placements quoted. Undefined means the year group isn't on work experience. */
|
|
79
184
|
wexY10?: number;
|
|
185
|
+
/** Y12 placements quoted. Undefined means the year group isn't on work experience. */
|
|
80
186
|
wexY12?: number;
|
|
187
|
+
/** Take the partial-product discount off the events fee. */
|
|
81
188
|
eventsDiscount?: boolean;
|
|
189
|
+
/** Take the partial-product discount off the destinations fee. */
|
|
82
190
|
destinationsDiscount?: boolean;
|
|
191
|
+
/** Take the full-platform discount off the whole quote. */
|
|
83
192
|
fullPlatformDiscount?: boolean;
|
|
193
|
+
/** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off). */
|
|
84
194
|
bespokeDiscount?: number;
|
|
195
|
+
/** Why the negotiated discount or the agreed price was given. Admin-only. */
|
|
85
196
|
discountReason?: string;
|
|
197
|
+
/** A price agreed with the customer, which replaces the calculated one. */
|
|
86
198
|
quoteOverride?: number;
|
|
199
|
+
/** Schools in the trust, which set its discount. Trust accounts only. */
|
|
87
200
|
schools?: number;
|
|
201
|
+
/** Schools on the hub. Hub accounts only. */
|
|
88
202
|
hubSchools?: number;
|
|
203
|
+
/** The quote as last saved, for the customer to see. */
|
|
89
204
|
quote?: InstituteQuoteSnapshot;
|
|
90
205
|
};
|
|
206
|
+
/** A priced institute: the same shape for a school, a trust and a hub. */
|
|
91
207
|
export type InstituteQuote = {
|
|
92
208
|
kind: "school" | "hub";
|
|
209
|
+
/** Every fee charged, with the working behind it. */
|
|
93
210
|
lines: PricingLine[];
|
|
211
|
+
/** The lines added up, before any whole-quote discount. */
|
|
94
212
|
subtotal: number;
|
|
95
213
|
fullPlatformDiscountApplied: number;
|
|
96
214
|
trustDiscountApplied: number;
|
|
215
|
+
/** The rate the trust discount was taken at, as a fraction. */
|
|
97
216
|
trustDiscountRate: number;
|
|
98
217
|
bespokeDiscountApplied: number;
|
|
99
218
|
minFeeApplied: boolean;
|
|
100
219
|
overrideApplied: boolean;
|
|
101
220
|
total: number;
|
|
102
221
|
};
|
|
222
|
+
/** What the customer is shown: the total, and what makes it up. No rates. */
|
|
103
223
|
export type InstituteQuoteSnapshot = {
|
|
104
224
|
lines: {
|
|
105
225
|
key: PricingLineKey;
|
|
106
226
|
label: string;
|
|
107
227
|
amount: number;
|
|
108
228
|
}[];
|
|
229
|
+
/** The lines added up. */
|
|
109
230
|
subtotal: number;
|
|
231
|
+
/** Everything taken off the lines to reach the total, as one figure. */
|
|
110
232
|
discountTotal: number;
|
|
233
|
+
/** True when the total is a floor or an agreed price above what the lines
|
|
234
|
+
* come to, so the breakdown is shown as an illustration, not a sum. */
|
|
111
235
|
minimum: boolean;
|
|
112
236
|
total: number;
|
|
237
|
+
/** ISO date the quote was last worked out. */
|
|
113
238
|
updated: string;
|
|
114
239
|
};
|
|
240
|
+
/** What the quote needs to know about the institute being priced. */
|
|
115
241
|
export type InstituteQuoteInput = {
|
|
242
|
+
/** What the institute has, as resolved by `resolveProducts`. */
|
|
116
243
|
products: {
|
|
117
244
|
wex?: boolean;
|
|
118
245
|
events?: boolean;
|
|
@@ -120,12 +247,60 @@ export type InstituteQuoteInput = {
|
|
|
120
247
|
isHub?: boolean;
|
|
121
248
|
isTrust?: boolean;
|
|
122
249
|
};
|
|
250
|
+
/** The institute's roll, when the agreed terms don't name one. */
|
|
123
251
|
students?: number;
|
|
252
|
+
/** Schools in the trust, when the agreed terms don't name a number. */
|
|
124
253
|
schools?: number;
|
|
254
|
+
/** Schools on the hub, when the agreed terms don't name a number. */
|
|
125
255
|
hubSchools?: number;
|
|
256
|
+
/** The terms agreed with this customer. */
|
|
126
257
|
pricing?: InstitutePricing;
|
|
127
258
|
};
|
|
259
|
+
/**
|
|
260
|
+
* Price a school and present it as an institute quote.
|
|
261
|
+
*
|
|
262
|
+
* @param {object} input the roll, the year groups, the products and the terms.
|
|
263
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
264
|
+
* @return {object} the quote, ready to render.
|
|
265
|
+
*/
|
|
128
266
|
export declare function schoolQuote(input: SchoolPricingInput, config?: PricingConfig): InstituteQuote;
|
|
267
|
+
/**
|
|
268
|
+
* Price a careers hub as an institute quote: the platform fee, then the schools.
|
|
269
|
+
*
|
|
270
|
+
* Hubs have no products to discount individually and no minimum fee to fall back
|
|
271
|
+
* on, so the only terms that reach them are the negotiated discount and an
|
|
272
|
+
* agreed price.
|
|
273
|
+
*
|
|
274
|
+
* @param {number} schools how many schools the hub brings on.
|
|
275
|
+
* @param {object} [pricing] the terms agreed with this hub.
|
|
276
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
277
|
+
* @return {object} the quote, ready to render.
|
|
278
|
+
*/
|
|
129
279
|
export declare function hubQuote(schools: number, pricing?: InstitutePricing, config?: PricingConfig): InstituteQuote;
|
|
280
|
+
/**
|
|
281
|
+
* Price an institute on what it actually has and what was agreed with it.
|
|
282
|
+
*
|
|
283
|
+
* The products come from `resolveProducts` rather than the raw flags, so a trust
|
|
284
|
+
* is priced on the union of its schools' products the same way it is gated on
|
|
285
|
+
* them, and it is a trust — not any school with a number against it — that earns
|
|
286
|
+
* the trust discount. A year group with no placements agreed is a year group not on work
|
|
287
|
+
* experience — except where the institute has work experience and neither year
|
|
288
|
+
* group has been filled in, which is a quote waiting to be finished rather than
|
|
289
|
+
* a school not taking the product, so Y10 is charged at zero to say so.
|
|
290
|
+
*
|
|
291
|
+
* @param {object} input the institute's products, roll and agreed terms.
|
|
292
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
293
|
+
* @return {object} the quote, ready to render.
|
|
294
|
+
*/
|
|
130
295
|
export declare function instituteQuote(input: InstituteQuoteInput, config?: PricingConfig): InstituteQuote;
|
|
296
|
+
/**
|
|
297
|
+
* Reduce a quote to what the customer sees: the fees, and one discount figure.
|
|
298
|
+
*
|
|
299
|
+
* The base fee isn't itemised to customers, so it is folded into the first fee
|
|
300
|
+
* they are shown — and where there is nothing to fold it into, it stands alone
|
|
301
|
+
* as the platform fee it is.
|
|
302
|
+
*
|
|
303
|
+
* @param {object} quote the priced institute.
|
|
304
|
+
* @return {object} the snapshot to store on the institute.
|
|
305
|
+
*/
|
|
131
306
|
export declare function quoteSnapshot(quote: InstituteQuote): InstituteQuoteSnapshot;
|
package/lib/pricing.js
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The 2026 pricing model.
|
|
4
|
+
*
|
|
5
|
+
* A quote is a base fee plus a per-product fee, and every per-product fee is
|
|
6
|
+
* graduated: the tiers are bands, not lookups, so a school with 1,250 students
|
|
7
|
+
* pays the tier 1 rate on its first 1,000 and the tier 2 rate on the next 250.
|
|
8
|
+
* Anything above the final threshold keeps paying the final tier's rate.
|
|
9
|
+
*
|
|
10
|
+
* Work experience is charged per placement (Y10 and Y12 are quoted separately,
|
|
11
|
+
* and a year group left blank is a year group not taking the product); events
|
|
12
|
+
* and destinations are charged per student. Only events and destinations carry
|
|
13
|
+
* a partial-product discount — that is how the pricing sheet has it, and it is
|
|
14
|
+
* why `partialProductDiscount` names those two rather than being one number.
|
|
15
|
+
*
|
|
16
|
+
* A trust is priced as one account on the average school terms, multiplied by
|
|
17
|
+
* the number of schools it brings on, and then earns a discount on the whole
|
|
18
|
+
* quote — a smaller one below the threshold, a larger one at or above it.
|
|
19
|
+
*
|
|
20
|
+
* Every figure lives in `PricingConfig` so it can be stored in Firestore and
|
|
21
|
+
* adjusted without a deploy. `DEFAULT_PRICING_CONFIG` is the fallback and the
|
|
22
|
+
* seed for that document.
|
|
23
|
+
*/
|
|
2
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
25
|
exports.PRICING_CONFIG_PATH = exports.DEFAULT_PRICING_CONFIG = void 0;
|
|
4
26
|
exports.trustDiscountRate = trustDiscountRate;
|
|
@@ -23,7 +45,16 @@ exports.DEFAULT_PRICING_CONFIG = {
|
|
|
23
45
|
trust: { threshold: 10, discount: 0.05, largeDiscount: 0.1 },
|
|
24
46
|
hub: { baseFee: 2000, perSchoolFee: 150 },
|
|
25
47
|
};
|
|
48
|
+
/** Where the adjustable copy of the config lives. */
|
|
26
49
|
exports.PRICING_CONFIG_PATH = ["adminConfig", "pricing"];
|
|
50
|
+
/**
|
|
51
|
+
* Charge `quantity` across the bands, each band at its own rate, with anything
|
|
52
|
+
* above the final threshold staying on the final band's rate.
|
|
53
|
+
*
|
|
54
|
+
* @param {number} quantity students or placements, depending on the product.
|
|
55
|
+
* @param {Array} bands each band's upper threshold and its rate.
|
|
56
|
+
* @return {Array} one entry per band that charged something.
|
|
57
|
+
*/
|
|
27
58
|
function graduated(quantity, bands) {
|
|
28
59
|
if (!(quantity > 0) || !bands.length)
|
|
29
60
|
return [];
|
|
@@ -35,6 +66,8 @@ function graduated(quantity, bands) {
|
|
|
35
66
|
if (inBand > 0)
|
|
36
67
|
lines.push({ tier: index + 1, quantity: inBand, rate: band.rate, amount: inBand * band.rate });
|
|
37
68
|
});
|
|
69
|
+
// Everything past the last threshold keeps paying the last rate, so it belongs
|
|
70
|
+
// in the last band rather than as a second row at the same rate.
|
|
38
71
|
const last = bands[bands.length - 1];
|
|
39
72
|
const overflow = Math.max(quantity - last.limit, 0);
|
|
40
73
|
if (overflow > 0) {
|
|
@@ -52,12 +85,32 @@ function graduated(quantity, bands) {
|
|
|
52
85
|
const sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
53
86
|
const clampFraction = (value) => Math.min(Math.max(value || 0, 0), 1);
|
|
54
87
|
const bandsTotal = (bands) => sum(bands.map((b) => b.amount));
|
|
88
|
+
/**
|
|
89
|
+
* What a trust comes off its quote for its size.
|
|
90
|
+
*
|
|
91
|
+
* A trust is more than one school, so a count below two is a single school and
|
|
92
|
+
* earns nothing — the discount is for bringing schools with you.
|
|
93
|
+
*
|
|
94
|
+
* @param {number} [schools] schools in the trust.
|
|
95
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
96
|
+
* @return {number} the discount, as a fraction of the quote.
|
|
97
|
+
*/
|
|
55
98
|
function trustDiscountRate(schools, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
56
99
|
const count = schools || 0;
|
|
57
100
|
if (count < 2)
|
|
58
101
|
return 0;
|
|
59
102
|
return count >= config.trust.threshold ? config.trust.largeDiscount : config.trust.discount;
|
|
60
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Price a school, trust or hub-school quote.
|
|
106
|
+
*
|
|
107
|
+
* Products the school isn't taking are left out of `lines` entirely, so the
|
|
108
|
+
* breakdown only ever shows what is being charged for.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} input the roll, the year groups and the products taken.
|
|
111
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
112
|
+
* @return {object} the total, and the working that reached it.
|
|
113
|
+
*/
|
|
61
114
|
function calculateSchoolPrice(input, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
62
115
|
const schoolCount = Math.max(input.schools || 0, 0);
|
|
63
116
|
const students = input.students || 0;
|
|
@@ -114,10 +167,27 @@ function calculateSchoolPrice(input, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
|
114
167
|
total: overridden ? input.quoteOverride : floored,
|
|
115
168
|
};
|
|
116
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Price a careers hub: the platform fee, plus a fee for every school on it.
|
|
172
|
+
*
|
|
173
|
+
* @param {number} schools how many schools the hub brings on.
|
|
174
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
175
|
+
* @return {object} the platform fee, the school fees and the total.
|
|
176
|
+
*/
|
|
117
177
|
function calculateHubPrice(schools, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
118
178
|
const schoolFees = Math.max(schools || 0, 0) * config.hub.perSchoolFee;
|
|
119
179
|
return { baseFee: config.hub.baseFee, schoolFees, total: config.hub.baseFee + schoolFees };
|
|
120
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Fill a stored config out with the defaults.
|
|
183
|
+
*
|
|
184
|
+
* The Firestore document is hand-edited, so treat every branch of it as
|
|
185
|
+
* possibly absent — a config saved before a tier was added should still price,
|
|
186
|
+
* rather than throwing on a missing rate.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} [stored] whatever the Firestore document holds.
|
|
189
|
+
* @return {object} a complete config, safe to price with.
|
|
190
|
+
*/
|
|
121
191
|
function mergePricingConfig(stored) {
|
|
122
192
|
const d = exports.DEFAULT_PRICING_CONFIG;
|
|
123
193
|
if (!stored)
|
|
@@ -132,9 +202,28 @@ function mergePricingConfig(stored) {
|
|
|
132
202
|
hub: { ...d.hub, ...stored.hub },
|
|
133
203
|
};
|
|
134
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Price a school and present it as an institute quote.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} input the roll, the year groups, the products and the terms.
|
|
209
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
210
|
+
* @return {object} the quote, ready to render.
|
|
211
|
+
*/
|
|
135
212
|
function schoolQuote(input, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
136
213
|
return { kind: "school", ...calculateSchoolPrice(input, config) };
|
|
137
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Price a careers hub as an institute quote: the platform fee, then the schools.
|
|
217
|
+
*
|
|
218
|
+
* Hubs have no products to discount individually and no minimum fee to fall back
|
|
219
|
+
* on, so the only terms that reach them are the negotiated discount and an
|
|
220
|
+
* agreed price.
|
|
221
|
+
*
|
|
222
|
+
* @param {number} schools how many schools the hub brings on.
|
|
223
|
+
* @param {object} [pricing] the terms agreed with this hub.
|
|
224
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
225
|
+
* @return {object} the quote, ready to render.
|
|
226
|
+
*/
|
|
138
227
|
function hubQuote(schools, pricing, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
139
228
|
const count = Math.max(schools || 0, 0);
|
|
140
229
|
const hub = calculateHubPrice(count, config);
|
|
@@ -165,6 +254,21 @@ function hubQuote(schools, pricing, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
|
165
254
|
total: overridden ? pricing?.quoteOverride : negotiated,
|
|
166
255
|
};
|
|
167
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Price an institute on what it actually has and what was agreed with it.
|
|
259
|
+
*
|
|
260
|
+
* The products come from `resolveProducts` rather than the raw flags, so a trust
|
|
261
|
+
* is priced on the union of its schools' products the same way it is gated on
|
|
262
|
+
* them, and it is a trust — not any school with a number against it — that earns
|
|
263
|
+
* the trust discount. A year group with no placements agreed is a year group not on work
|
|
264
|
+
* experience — except where the institute has work experience and neither year
|
|
265
|
+
* group has been filled in, which is a quote waiting to be finished rather than
|
|
266
|
+
* a school not taking the product, so Y10 is charged at zero to say so.
|
|
267
|
+
*
|
|
268
|
+
* @param {object} input the institute's products, roll and agreed terms.
|
|
269
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
270
|
+
* @return {object} the quote, ready to render.
|
|
271
|
+
*/
|
|
168
272
|
function instituteQuote(input, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
169
273
|
const pricing = input.pricing;
|
|
170
274
|
if (input.products.isHub) {
|
|
@@ -188,6 +292,16 @@ function instituteQuote(input, config = exports.DEFAULT_PRICING_CONFIG) {
|
|
|
188
292
|
quoteOverride: pricing?.quoteOverride,
|
|
189
293
|
}, config);
|
|
190
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Reduce a quote to what the customer sees: the fees, and one discount figure.
|
|
297
|
+
*
|
|
298
|
+
* The base fee isn't itemised to customers, so it is folded into the first fee
|
|
299
|
+
* they are shown — and where there is nothing to fold it into, it stands alone
|
|
300
|
+
* as the platform fee it is.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} quote the priced institute.
|
|
303
|
+
* @return {object} the snapshot to store on the institute.
|
|
304
|
+
*/
|
|
191
305
|
function quoteSnapshot(quote) {
|
|
192
306
|
const itemised = quote.lines.filter((l) => l.key !== "base");
|
|
193
307
|
const base = quote.lines.find((l) => l.key === "base")?.amount || 0;
|
|
@@ -202,6 +316,9 @@ function quoteSnapshot(quote) {
|
|
|
202
316
|
return {
|
|
203
317
|
lines,
|
|
204
318
|
subtotal,
|
|
319
|
+
// The customer is shown the fees and one discount, so whatever separates
|
|
320
|
+
// the two — the whole-quote discounts, an agreed price below them — is
|
|
321
|
+
// that one figure, and a total above the fees is flagged instead.
|
|
205
322
|
discountTotal: Math.max(subtotal - quote.total, 0),
|
|
206
323
|
minimum: quote.total > subtotal,
|
|
207
324
|
total: quote.total,
|
package/lib/pricing.js.map
CHANGED
|
@@ -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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"}
|
package/package.json
CHANGED
package/src/pricing.ts
CHANGED
|
@@ -1,23 +1,61 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The 2026 pricing model.
|
|
3
|
+
*
|
|
4
|
+
* A quote is a base fee plus a per-product fee, and every per-product fee is
|
|
5
|
+
* graduated: the tiers are bands, not lookups, so a school with 1,250 students
|
|
6
|
+
* pays the tier 1 rate on its first 1,000 and the tier 2 rate on the next 250.
|
|
7
|
+
* Anything above the final threshold keeps paying the final tier's rate.
|
|
8
|
+
*
|
|
9
|
+
* Work experience is charged per placement (Y10 and Y12 are quoted separately,
|
|
10
|
+
* and a year group left blank is a year group not taking the product); events
|
|
11
|
+
* and destinations are charged per student. Only events and destinations carry
|
|
12
|
+
* a partial-product discount — that is how the pricing sheet has it, and it is
|
|
13
|
+
* why `partialProductDiscount` names those two rather than being one number.
|
|
14
|
+
*
|
|
15
|
+
* A trust is priced as one account on the average school terms, multiplied by
|
|
16
|
+
* the number of schools it brings on, and then earns a discount on the whole
|
|
17
|
+
* quote — a smaller one below the threshold, a larger one at or above it.
|
|
18
|
+
*
|
|
19
|
+
* Every figure lives in `PricingConfig` so it can be stored in Firestore and
|
|
20
|
+
* adjusted without a deploy. `DEFAULT_PRICING_CONFIG` is the fallback and the
|
|
21
|
+
* seed for that document.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** One band of the graduated fee, with the rate each product charges in it. */
|
|
2
25
|
export type PricingTier = {
|
|
26
|
+
/** Upper bound of the band, in students (events and destinations). */
|
|
3
27
|
students: number,
|
|
28
|
+
/** Upper bound of the band, in placements (work experience). */
|
|
4
29
|
placements: number,
|
|
30
|
+
/** £ per placement. */
|
|
5
31
|
wex: number,
|
|
32
|
+
/** £ per student. */
|
|
6
33
|
events: number,
|
|
34
|
+
/** £ per student. */
|
|
7
35
|
destinations: number,
|
|
8
36
|
};
|
|
9
37
|
|
|
10
38
|
export type PricingConfig = {
|
|
39
|
+
/** No school is quoted below this, however little the products come to. */
|
|
11
40
|
minFee: number,
|
|
41
|
+
/** Charged on every quote. Not itemised to the customer. */
|
|
12
42
|
baseFee: number,
|
|
43
|
+
/** In ascending order of threshold. The last band's rate runs to infinity. */
|
|
13
44
|
tiers: PricingTier[],
|
|
45
|
+
/** Taken off a single product's fee, as a fraction (0.2 = 20% off). */
|
|
14
46
|
partialProductDiscount: {events: number, destinations: number},
|
|
47
|
+
/** Taken off the whole quote when a school takes the full platform. */
|
|
15
48
|
fullPlatformDiscount: number,
|
|
49
|
+
/** Taken off the whole quote for a trust, on how many schools it brings. */
|
|
16
50
|
trust: {
|
|
51
|
+
/** Schools at which the larger discount takes over. */
|
|
17
52
|
threshold: number,
|
|
53
|
+
/** Fraction off a trust below the threshold. */
|
|
18
54
|
discount: number,
|
|
55
|
+
/** Fraction off a trust at or above it. */
|
|
19
56
|
largeDiscount: number,
|
|
20
57
|
},
|
|
58
|
+
/** Careers hubs are a flat platform fee plus a fee for each school. */
|
|
21
59
|
hub: {baseFee: number, perSchoolFee: number},
|
|
22
60
|
};
|
|
23
61
|
|
|
@@ -36,19 +74,32 @@ export const DEFAULT_PRICING_CONFIG: PricingConfig = {
|
|
|
36
74
|
hub: {baseFee: 2000, perSchoolFee: 150},
|
|
37
75
|
};
|
|
38
76
|
|
|
77
|
+
/** Where the adjustable copy of the config lives. */
|
|
39
78
|
export const PRICING_CONFIG_PATH = ["adminConfig", "pricing"];
|
|
40
79
|
|
|
41
80
|
export type SchoolPricingInput = {
|
|
81
|
+
/** Roll size. Drives the events and destinations fees. */
|
|
42
82
|
students?: number,
|
|
83
|
+
/** Y10 placements. Undefined means the year group isn't taking work experience. */
|
|
43
84
|
wexY10?: number,
|
|
85
|
+
/** Y12 placements. Undefined means the year group isn't taking work experience. */
|
|
44
86
|
wexY12?: number,
|
|
45
87
|
events?: boolean,
|
|
88
|
+
/** Discount the events fee on its own. */
|
|
46
89
|
eventsDiscount?: boolean,
|
|
47
90
|
destinations?: boolean,
|
|
91
|
+
/** Discount the destinations fee on its own. */
|
|
48
92
|
destinationsDiscount?: boolean,
|
|
93
|
+
/** Discount the whole quote, for a school taking the full platform. */
|
|
49
94
|
fullPlatformDiscount?: boolean,
|
|
95
|
+
/** Schools in the trust being quoted. When a trust is quoted, its school-level
|
|
96
|
+
* terms are multiplied by this count before any trust discount is taken. */
|
|
50
97
|
schools?: number,
|
|
98
|
+
/** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off).
|
|
99
|
+
* Taken after the full-platform discount and before the minimum fee. */
|
|
51
100
|
bespokeDiscount?: number,
|
|
101
|
+
/** A price agreed with the customer, which replaces everything above it —
|
|
102
|
+
* the discounts, the minimum fee and the total the products came to. */
|
|
52
103
|
quoteOverride?: number,
|
|
53
104
|
};
|
|
54
105
|
|
|
@@ -58,13 +109,19 @@ export type PricingLineKey =
|
|
|
58
109
|
export type PricingLine = {
|
|
59
110
|
key: PricingLineKey,
|
|
60
111
|
label: string,
|
|
112
|
+
/** What the product costs after any partial-product discount. */
|
|
61
113
|
amount: number,
|
|
114
|
+
/** The fee before its partial-product discount, when one was applied. */
|
|
62
115
|
undiscounted?: number,
|
|
116
|
+
/** How the amount was reached, band by band. Empty for the base fee. */
|
|
63
117
|
bands: PricingBand[],
|
|
64
118
|
};
|
|
65
119
|
|
|
120
|
+
/** One band's contribution to a product's fee, for showing the working. */
|
|
66
121
|
export type PricingBand = {
|
|
122
|
+
/** 1-indexed, matching the tier's position in the config. */
|
|
67
123
|
tier: number,
|
|
124
|
+
/** Units charged in this band. */
|
|
68
125
|
quantity: number,
|
|
69
126
|
rate: number,
|
|
70
127
|
amount: number,
|
|
@@ -72,22 +129,38 @@ export type PricingBand = {
|
|
|
72
129
|
|
|
73
130
|
export type SchoolPricingResult = {
|
|
74
131
|
lines: PricingLine[],
|
|
132
|
+
/** The lines added up, before the full-platform discount. */
|
|
75
133
|
subtotal: number,
|
|
134
|
+
/** £ taken off the subtotal by the full-platform discount. */
|
|
76
135
|
fullPlatformDiscountApplied: number,
|
|
136
|
+
/** £ taken off by the trust discount, after the full-platform one. */
|
|
77
137
|
trustDiscountApplied: number,
|
|
138
|
+
/** The rate that trust discount was taken at, as a fraction. */
|
|
78
139
|
trustDiscountRate: number,
|
|
140
|
+
/** £ taken off by the negotiated discount, after the trust one. */
|
|
79
141
|
bespokeDiscountApplied: number,
|
|
142
|
+
/** True when the minimum fee, not the products, set the price. */
|
|
80
143
|
minFeeApplied: boolean,
|
|
144
|
+
/** True when an agreed price replaced the calculated one. */
|
|
81
145
|
overrideApplied: boolean,
|
|
82
146
|
total: number,
|
|
83
147
|
};
|
|
84
148
|
|
|
85
149
|
export type HubPricingResult = {
|
|
86
150
|
baseFee: number,
|
|
151
|
+
/** The per-school fee multiplied by the number of schools. */
|
|
87
152
|
schoolFees: number,
|
|
88
153
|
total: number,
|
|
89
154
|
};
|
|
90
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Charge `quantity` across the bands, each band at its own rate, with anything
|
|
158
|
+
* above the final threshold staying on the final band's rate.
|
|
159
|
+
*
|
|
160
|
+
* @param {number} quantity students or placements, depending on the product.
|
|
161
|
+
* @param {Array} bands each band's upper threshold and its rate.
|
|
162
|
+
* @return {Array} one entry per band that charged something.
|
|
163
|
+
*/
|
|
91
164
|
function graduated(quantity: number, bands: {limit: number, rate: number}[]): PricingBand[] {
|
|
92
165
|
if (!(quantity > 0) || !bands.length) return [];
|
|
93
166
|
|
|
@@ -100,6 +173,8 @@ function graduated(quantity: number, bands: {limit: number, rate: number}[]): Pr
|
|
|
100
173
|
if (inBand > 0) lines.push({tier: index + 1, quantity: inBand, rate: band.rate, amount: inBand * band.rate});
|
|
101
174
|
});
|
|
102
175
|
|
|
176
|
+
// Everything past the last threshold keeps paying the last rate, so it belongs
|
|
177
|
+
// in the last band rather than as a second row at the same rate.
|
|
103
178
|
const last = bands[bands.length - 1];
|
|
104
179
|
const overflow = Math.max(quantity - last.limit, 0);
|
|
105
180
|
if (overflow > 0) {
|
|
@@ -121,6 +196,16 @@ const clampFraction = (value?: number) => Math.min(Math.max(value || 0, 0), 1);
|
|
|
121
196
|
|
|
122
197
|
const bandsTotal = (bands: PricingBand[]) => sum(bands.map((b) => b.amount));
|
|
123
198
|
|
|
199
|
+
/**
|
|
200
|
+
* What a trust comes off its quote for its size.
|
|
201
|
+
*
|
|
202
|
+
* A trust is more than one school, so a count below two is a single school and
|
|
203
|
+
* earns nothing — the discount is for bringing schools with you.
|
|
204
|
+
*
|
|
205
|
+
* @param {number} [schools] schools in the trust.
|
|
206
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
207
|
+
* @return {number} the discount, as a fraction of the quote.
|
|
208
|
+
*/
|
|
124
209
|
export function trustDiscountRate(
|
|
125
210
|
schools?: number,
|
|
126
211
|
config: PricingConfig = DEFAULT_PRICING_CONFIG,
|
|
@@ -130,6 +215,16 @@ export function trustDiscountRate(
|
|
|
130
215
|
return count >= config.trust.threshold ? config.trust.largeDiscount : config.trust.discount;
|
|
131
216
|
}
|
|
132
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Price a school, trust or hub-school quote.
|
|
220
|
+
*
|
|
221
|
+
* Products the school isn't taking are left out of `lines` entirely, so the
|
|
222
|
+
* breakdown only ever shows what is being charged for.
|
|
223
|
+
*
|
|
224
|
+
* @param {object} input the roll, the year groups and the products taken.
|
|
225
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
226
|
+
* @return {object} the total, and the working that reached it.
|
|
227
|
+
*/
|
|
133
228
|
export function calculateSchoolPrice(
|
|
134
229
|
input: SchoolPricingInput,
|
|
135
230
|
config: PricingConfig = DEFAULT_PRICING_CONFIG,
|
|
@@ -196,6 +291,13 @@ export function calculateSchoolPrice(
|
|
|
196
291
|
};
|
|
197
292
|
}
|
|
198
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Price a careers hub: the platform fee, plus a fee for every school on it.
|
|
296
|
+
*
|
|
297
|
+
* @param {number} schools how many schools the hub brings on.
|
|
298
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
299
|
+
* @return {object} the platform fee, the school fees and the total.
|
|
300
|
+
*/
|
|
199
301
|
export function calculateHubPrice(
|
|
200
302
|
schools: number,
|
|
201
303
|
config: PricingConfig = DEFAULT_PRICING_CONFIG,
|
|
@@ -204,6 +306,16 @@ export function calculateHubPrice(
|
|
|
204
306
|
return {baseFee: config.hub.baseFee, schoolFees, total: config.hub.baseFee + schoolFees};
|
|
205
307
|
}
|
|
206
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Fill a stored config out with the defaults.
|
|
311
|
+
*
|
|
312
|
+
* The Firestore document is hand-edited, so treat every branch of it as
|
|
313
|
+
* possibly absent — a config saved before a tier was added should still price,
|
|
314
|
+
* rather than throwing on a missing rate.
|
|
315
|
+
*
|
|
316
|
+
* @param {object} [stored] whatever the Firestore document holds.
|
|
317
|
+
* @return {object} a complete config, safe to price with.
|
|
318
|
+
*/
|
|
207
319
|
export function mergePricingConfig(stored?: Partial<PricingConfig>): PricingConfig {
|
|
208
320
|
const d = DEFAULT_PRICING_CONFIG;
|
|
209
321
|
if (!stored) return d;
|
|
@@ -219,27 +331,59 @@ export function mergePricingConfig(stored?: Partial<PricingConfig>): PricingConf
|
|
|
219
331
|
};
|
|
220
332
|
}
|
|
221
333
|
|
|
334
|
+
/* -------------------------------------------------------------------------
|
|
335
|
+
* An institute's own quote.
|
|
336
|
+
*
|
|
337
|
+
* The calculator above prices a hypothetical school. What an actual customer
|
|
338
|
+
* pays is that calculation plus the terms agreed with them — the year groups
|
|
339
|
+
* and roll we quoted on, which discounts we gave, and any price we simply shook
|
|
340
|
+
* hands on — so those live on the institute, in `InstitutePricing`, and the
|
|
341
|
+
* quote is worked out from the two together.
|
|
342
|
+
*
|
|
343
|
+
* A school never reads the pricing configuration itself (it is admin data, and
|
|
344
|
+
* a school has no business seeing the tiers), so the agreed quote is snapshotted
|
|
345
|
+
* onto the institute as `InstitutePricing.quote` whenever an admin saves. That
|
|
346
|
+
* snapshot is what the school's own billing page shows.
|
|
347
|
+
* ---------------------------------------------------------------------- */
|
|
348
|
+
|
|
349
|
+
/** The terms agreed with one customer. Stored at `institutes/{oId}.pricing`. */
|
|
222
350
|
export type InstitutePricing = {
|
|
351
|
+
/** The roll we quoted on. Defaults to the institute's own student count. */
|
|
223
352
|
students?: number,
|
|
353
|
+
/** Y10 placements quoted. Undefined means the year group isn't on work experience. */
|
|
224
354
|
wexY10?: number,
|
|
355
|
+
/** Y12 placements quoted. Undefined means the year group isn't on work experience. */
|
|
225
356
|
wexY12?: number,
|
|
357
|
+
/** Take the partial-product discount off the events fee. */
|
|
226
358
|
eventsDiscount?: boolean,
|
|
359
|
+
/** Take the partial-product discount off the destinations fee. */
|
|
227
360
|
destinationsDiscount?: boolean,
|
|
361
|
+
/** Take the full-platform discount off the whole quote. */
|
|
228
362
|
fullPlatformDiscount?: boolean,
|
|
363
|
+
/** A negotiated discount off the whole quote, as a fraction (0.1 = 10% off). */
|
|
229
364
|
bespokeDiscount?: number,
|
|
365
|
+
/** Why the negotiated discount or the agreed price was given. Admin-only. */
|
|
230
366
|
discountReason?: string,
|
|
367
|
+
/** A price agreed with the customer, which replaces the calculated one. */
|
|
231
368
|
quoteOverride?: number,
|
|
369
|
+
/** Schools in the trust, which set its discount. Trust accounts only. */
|
|
232
370
|
schools?: number,
|
|
371
|
+
/** Schools on the hub. Hub accounts only. */
|
|
233
372
|
hubSchools?: number,
|
|
373
|
+
/** The quote as last saved, for the customer to see. */
|
|
234
374
|
quote?: InstituteQuoteSnapshot,
|
|
235
375
|
};
|
|
236
376
|
|
|
377
|
+
/** A priced institute: the same shape for a school, a trust and a hub. */
|
|
237
378
|
export type InstituteQuote = {
|
|
238
379
|
kind: "school"|"hub",
|
|
380
|
+
/** Every fee charged, with the working behind it. */
|
|
239
381
|
lines: PricingLine[],
|
|
382
|
+
/** The lines added up, before any whole-quote discount. */
|
|
240
383
|
subtotal: number,
|
|
241
384
|
fullPlatformDiscountApplied: number,
|
|
242
385
|
trustDiscountApplied: number,
|
|
386
|
+
/** The rate the trust discount was taken at, as a fraction. */
|
|
243
387
|
trustDiscountRate: number,
|
|
244
388
|
bespokeDiscountApplied: number,
|
|
245
389
|
minFeeApplied: boolean,
|
|
@@ -247,23 +391,42 @@ export type InstituteQuote = {
|
|
|
247
391
|
total: number,
|
|
248
392
|
};
|
|
249
393
|
|
|
394
|
+
/** What the customer is shown: the total, and what makes it up. No rates. */
|
|
250
395
|
export type InstituteQuoteSnapshot = {
|
|
251
396
|
lines: {key: PricingLineKey, label: string, amount: number}[],
|
|
397
|
+
/** The lines added up. */
|
|
252
398
|
subtotal: number,
|
|
399
|
+
/** Everything taken off the lines to reach the total, as one figure. */
|
|
253
400
|
discountTotal: number,
|
|
401
|
+
/** True when the total is a floor or an agreed price above what the lines
|
|
402
|
+
* come to, so the breakdown is shown as an illustration, not a sum. */
|
|
254
403
|
minimum: boolean,
|
|
255
404
|
total: number,
|
|
405
|
+
/** ISO date the quote was last worked out. */
|
|
256
406
|
updated: string,
|
|
257
407
|
};
|
|
258
408
|
|
|
409
|
+
/** What the quote needs to know about the institute being priced. */
|
|
259
410
|
export type InstituteQuoteInput = {
|
|
411
|
+
/** What the institute has, as resolved by `resolveProducts`. */
|
|
260
412
|
products: {wex?: boolean, events?: boolean, destinations?: boolean, isHub?: boolean, isTrust?: boolean},
|
|
413
|
+
/** The institute's roll, when the agreed terms don't name one. */
|
|
261
414
|
students?: number,
|
|
415
|
+
/** Schools in the trust, when the agreed terms don't name a number. */
|
|
262
416
|
schools?: number,
|
|
417
|
+
/** Schools on the hub, when the agreed terms don't name a number. */
|
|
263
418
|
hubSchools?: number,
|
|
419
|
+
/** The terms agreed with this customer. */
|
|
264
420
|
pricing?: InstitutePricing,
|
|
265
421
|
};
|
|
266
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Price a school and present it as an institute quote.
|
|
425
|
+
*
|
|
426
|
+
* @param {object} input the roll, the year groups, the products and the terms.
|
|
427
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
428
|
+
* @return {object} the quote, ready to render.
|
|
429
|
+
*/
|
|
267
430
|
export function schoolQuote(
|
|
268
431
|
input: SchoolPricingInput,
|
|
269
432
|
config: PricingConfig = DEFAULT_PRICING_CONFIG,
|
|
@@ -271,6 +434,18 @@ export function schoolQuote(
|
|
|
271
434
|
return {kind: "school", ...calculateSchoolPrice(input, config)};
|
|
272
435
|
}
|
|
273
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Price a careers hub as an institute quote: the platform fee, then the schools.
|
|
439
|
+
*
|
|
440
|
+
* Hubs have no products to discount individually and no minimum fee to fall back
|
|
441
|
+
* on, so the only terms that reach them are the negotiated discount and an
|
|
442
|
+
* agreed price.
|
|
443
|
+
*
|
|
444
|
+
* @param {number} schools how many schools the hub brings on.
|
|
445
|
+
* @param {object} [pricing] the terms agreed with this hub.
|
|
446
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
447
|
+
* @return {object} the quote, ready to render.
|
|
448
|
+
*/
|
|
274
449
|
export function hubQuote(
|
|
275
450
|
schools: number,
|
|
276
451
|
pricing?: InstitutePricing,
|
|
@@ -309,6 +484,21 @@ export function hubQuote(
|
|
|
309
484
|
};
|
|
310
485
|
}
|
|
311
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Price an institute on what it actually has and what was agreed with it.
|
|
489
|
+
*
|
|
490
|
+
* The products come from `resolveProducts` rather than the raw flags, so a trust
|
|
491
|
+
* is priced on the union of its schools' products the same way it is gated on
|
|
492
|
+
* them, and it is a trust — not any school with a number against it — that earns
|
|
493
|
+
* the trust discount. A year group with no placements agreed is a year group not on work
|
|
494
|
+
* experience — except where the institute has work experience and neither year
|
|
495
|
+
* group has been filled in, which is a quote waiting to be finished rather than
|
|
496
|
+
* a school not taking the product, so Y10 is charged at zero to say so.
|
|
497
|
+
*
|
|
498
|
+
* @param {object} input the institute's products, roll and agreed terms.
|
|
499
|
+
* @param {object} [config] the figures to price with. Defaults to the 2026 sheet.
|
|
500
|
+
* @return {object} the quote, ready to render.
|
|
501
|
+
*/
|
|
312
502
|
export function instituteQuote(
|
|
313
503
|
input: InstituteQuoteInput,
|
|
314
504
|
config: PricingConfig = DEFAULT_PRICING_CONFIG,
|
|
@@ -339,6 +529,16 @@ export function instituteQuote(
|
|
|
339
529
|
}, config);
|
|
340
530
|
}
|
|
341
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Reduce a quote to what the customer sees: the fees, and one discount figure.
|
|
534
|
+
*
|
|
535
|
+
* The base fee isn't itemised to customers, so it is folded into the first fee
|
|
536
|
+
* they are shown — and where there is nothing to fold it into, it stands alone
|
|
537
|
+
* as the platform fee it is.
|
|
538
|
+
*
|
|
539
|
+
* @param {object} quote the priced institute.
|
|
540
|
+
* @return {object} the snapshot to store on the institute.
|
|
541
|
+
*/
|
|
342
542
|
export function quoteSnapshot(quote: InstituteQuote): InstituteQuoteSnapshot {
|
|
343
543
|
const itemised = quote.lines.filter((l) => l.key !== "base");
|
|
344
544
|
const base = quote.lines.find((l) => l.key === "base")?.amount || 0;
|
|
@@ -356,6 +556,9 @@ export function quoteSnapshot(quote: InstituteQuote): InstituteQuoteSnapshot {
|
|
|
356
556
|
return {
|
|
357
557
|
lines,
|
|
358
558
|
subtotal,
|
|
559
|
+
// The customer is shown the fees and one discount, so whatever separates
|
|
560
|
+
// the two — the whole-quote discounts, an agreed price below them — is
|
|
561
|
+
// that one figure, and a total above the fees is flagged instead.
|
|
359
562
|
discountTotal: Math.max(subtotal - quote.total, 0),
|
|
360
563
|
minimum: quote.total > subtotal,
|
|
361
564
|
total: quote.total,
|