@cyc-seattle/platform 1.50.0 → 1.52.0
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/README.md +14 -14
- package/dist/planning/bonus.d.ts +9 -7
- package/dist/planning/bonus.js +109 -71
- package/dist/planning/engine.js +41 -18
- package/dist/planning/lines.js +1 -1
- package/dist/planning/revenue.js +1 -1
- package/dist/planning/terms.d.ts +11 -0
- package/dist/planning/terms.js +22 -0
- package/dist/planning/types.d.ts +25 -5
- package/dist/schema/index.d.ts +62 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,20 +101,20 @@ deployer needs first.
|
|
|
101
101
|
authenticates with Workload Identity Federation and stores no credentials. Each job runs in a
|
|
102
102
|
GitHub environment that holds its identity and state bucket:
|
|
103
103
|
|
|
104
|
-
| Event | Jobs, in order
|
|
105
|
-
| -------------------------- |
|
|
106
|
-
| Pull request | `ci`
|
|
107
|
-
| Merge queue | `ci`, then `dev`: `mise run deploy` as `deploy-dev
|
|
108
|
-
| Push to `main` | `prod-preview`, then `prod`, which waits for the `maintainers` approval
|
|
109
|
-
| Run by hand, from a branch | `ci`, then `dev`
|
|
110
|
-
| Run by hand, from `main` | `prod-preview`, then `prod`
|
|
111
|
-
|
|
112
|
-
Dev is shared, so only the merge queue deploys to it (#69, #80). PRs queued together deploy
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
branch on dev before queueing it, run `gh workflow run pull-request.yml --ref <branch>`.
|
|
104
|
+
| Event | Jobs, in order |
|
|
105
|
+
| -------------------------- | ----------------------------------------------------------------------- |
|
|
106
|
+
| Pull request | `ci` |
|
|
107
|
+
| Merge queue | `ci`, then `dev`: `mise run deploy` as `deploy-dev`; `dev-passed` |
|
|
108
|
+
| Push to `main` | `prod-preview`, then `prod`, which waits for the `maintainers` approval |
|
|
109
|
+
| Run by hand, from a branch | `ci`, then `dev` |
|
|
110
|
+
| Run by hand, from `main` | `prod-preview`, then `prod` |
|
|
111
|
+
|
|
112
|
+
Dev is shared, so only the merge queue deploys to it (#69, #80). PRs queued together deploy as
|
|
113
|
+
one batch: each queued PR's run skips dev when a newer PR is queued behind it, and the newest
|
|
114
|
+
deploys them all. The queue requires `dev-passed`, so a batch merges only once its dev deploy passes, and `main` gets the exact commit the queue tested, in one push. If the
|
|
115
|
+
newest PR fails, the queue drops it and the rest of the batch deploys without it. A queued PR of only docs or CI skips `dev`. The queue
|
|
116
|
+
doesn't run the integration tests (#161); a run by hand does. To try a
|
|
117
|
+
branch on dev and test it before queueing it, run `gh workflow run pull-request.yml --ref <branch>`.
|
|
118
118
|
|
|
119
119
|
`prod-preview` runs `mise run preview` as `deploy-runner` and writes the diff to the job summary.
|
|
120
120
|
`prod` runs `mise run deploy` on the same commit once approved. It runs only when the preview shows
|
package/dist/planning/bonus.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Context } from "./context.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { type OfferingTerms } from "./terms.ts";
|
|
3
|
+
import type { BonusSummary } from "./types.ts";
|
|
3
4
|
/**
|
|
4
|
-
* Records each bonus owner's `bonus_rate` × its products' net
|
|
5
|
-
* pools or before them.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Records each bonus owner's `bonus_rate` × its products' net in each term, above zero, after the
|
|
6
|
+
* pools or before them. A term's loss doesn't lower another term's bonus. Offerings in no term, and
|
|
7
|
+
* anything not on an offering, make one more base, for the year. Every base is worked out before any
|
|
8
|
+
* bonus is booked, so one bonus never counts in another's base. Each bonus is divided among that
|
|
9
|
+
* base's offerings by revenue, with the year's payroll tax on it, and neither is counted in MTDC.
|
|
10
|
+
* Returns each bonus, by owner and term.
|
|
9
11
|
*/
|
|
10
|
-
export declare function bookBonuses(ctx: Context):
|
|
12
|
+
export declare function bookBonuses(ctx: Context, terms: OfferingTerms): BonusSummary[];
|
package/dist/planning/bonus.js
CHANGED
|
@@ -1,77 +1,99 @@
|
|
|
1
1
|
import { revenue } from "./measures.js";
|
|
2
|
+
import { compareTerms } from "./terms.js";
|
|
2
3
|
/**
|
|
3
|
-
* Records each bonus owner's `bonus_rate` × its products' net
|
|
4
|
-
* pools or before them.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Records each bonus owner's `bonus_rate` × its products' net in each term, above zero, after the
|
|
5
|
+
* pools or before them. A term's loss doesn't lower another term's bonus. Offerings in no term, and
|
|
6
|
+
* anything not on an offering, make one more base, for the year. Every base is worked out before any
|
|
7
|
+
* bonus is booked, so one bonus never counts in another's base. Each bonus is divided among that
|
|
8
|
+
* base's offerings by revenue, with the year's payroll tax on it, and neither is counted in MTDC.
|
|
9
|
+
* Returns each bonus, by owner and term.
|
|
8
10
|
*/
|
|
9
|
-
export function bookBonuses(ctx) {
|
|
10
|
-
const bonuses = ctx.data.plan_bonus_owners.map((owner) => ({ owner,
|
|
11
|
+
export function bookBonuses(ctx, terms) {
|
|
12
|
+
const bonuses = ctx.data.plan_bonus_owners.map((owner) => ({ owner, bases: basesOf(ctx, owner, terms) }));
|
|
11
13
|
const taxRate = ctx.rates?.payroll_tax_rate ?? 0;
|
|
12
14
|
const taxAccount = taxRate > 0 && bonuses.length > 0 ? ctx.costAccount("payroll_tax") : undefined;
|
|
13
|
-
const
|
|
14
|
-
for (const { owner,
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
// Its net came from elsewhere, such as revenue booked on a product, so its offerings share it evenly.
|
|
30
|
-
ctx.warn("bonus_without_revenue", `${owner.role_name}'s offerings have no revenue of their own, so its bonus is split evenly among them`, owner.key);
|
|
31
|
-
for (const key of base.offeringKeys)
|
|
32
|
-
weights.set(key, 1);
|
|
33
|
-
}
|
|
34
|
-
const total = [...weights.values()].reduce((t, w) => t + w, 0);
|
|
35
|
-
if (owner.position_key != null) {
|
|
36
|
-
byPosition.set(owner.position_key, (byPosition.get(owner.position_key) ?? 0) + bonus);
|
|
37
|
-
}
|
|
38
|
-
for (const [key, w] of weights) {
|
|
39
|
-
const amount = (bonus * w) / total;
|
|
40
|
-
ctx.book({
|
|
41
|
-
cost_center_type: "offering",
|
|
42
|
-
cost_center_key: key,
|
|
43
|
-
account_id: owner.account_id,
|
|
44
|
-
function: ctx.functionFor(owner.account_id, null, "offering"),
|
|
45
|
-
amount,
|
|
46
|
-
line_key: `bonus:${owner.key}`,
|
|
47
|
-
line_name: `Bonus: ${owner.role_name}`,
|
|
48
|
-
explanation: `${owner.role_name}: ${owner.bonus_rate} × net of its products`,
|
|
49
|
-
stage: "bonus",
|
|
50
|
-
mtdc_share: 0,
|
|
51
|
-
});
|
|
52
|
-
if (taxAccount === undefined)
|
|
53
|
-
continue;
|
|
54
|
-
ctx.book({
|
|
55
|
-
cost_center_type: "offering",
|
|
56
|
-
cost_center_key: key,
|
|
57
|
-
account_id: taxAccount,
|
|
58
|
-
function: ctx.functionFor(owner.account_id, null, "offering"),
|
|
59
|
-
amount: amount * taxRate,
|
|
60
|
-
line_key: `bonus_payroll_tax:${owner.key}`,
|
|
61
|
-
line_name: `Payroll tax on bonus: ${owner.role_name}`,
|
|
62
|
-
explanation: `${taxRate} × ${owner.role_name}'s bonus`,
|
|
63
|
-
stage: "bonus",
|
|
64
|
-
mtdc_share: 0,
|
|
15
|
+
const summaries = [];
|
|
16
|
+
for (const { owner, bases } of bonuses) {
|
|
17
|
+
const area = areaOf(ctx, owner);
|
|
18
|
+
for (const base of bases) {
|
|
19
|
+
const bonus = owner.bonus_rate * Math.max(0, base.net);
|
|
20
|
+
const booked = bonus === 0 ? 0 : bookBonus(ctx, owner, base, bonus, taxRate, taxAccount);
|
|
21
|
+
summaries.push({
|
|
22
|
+
bonus_owner_key: owner.key,
|
|
23
|
+
role_name: owner.role_name,
|
|
24
|
+
position_key: owner.position_key ?? null,
|
|
25
|
+
program_area_id: area,
|
|
26
|
+
term_key: base.term?.key ?? null,
|
|
27
|
+
bonus_rate: owner.bonus_rate,
|
|
28
|
+
net_cents: Math.round(base.net),
|
|
29
|
+
bonus_cents: Math.round(booked),
|
|
65
30
|
});
|
|
66
31
|
}
|
|
67
32
|
}
|
|
68
|
-
return
|
|
33
|
+
return summaries;
|
|
34
|
+
}
|
|
35
|
+
/** Books one term's bonus on its offerings, by revenue. Returns what it booked. */
|
|
36
|
+
function bookBonus(ctx, owner, base, bonus, taxRate, taxAccount) {
|
|
37
|
+
const weights = new Map();
|
|
38
|
+
for (const key of base.offeringKeys) {
|
|
39
|
+
const r = revenue(ctx, key);
|
|
40
|
+
if (r > 0)
|
|
41
|
+
weights.set(key, r);
|
|
42
|
+
}
|
|
43
|
+
if (weights.size === 0) {
|
|
44
|
+
if (base.offeringKeys.length === 0) {
|
|
45
|
+
ctx.warn("bonus_without_offerings", `${owner.role_name}'s bonus has no offerings this year to charge it to, so it's left out`, owner.key);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
// Its net came from elsewhere, such as revenue booked on a product, so its offerings share it evenly.
|
|
49
|
+
ctx.warn("bonus_without_revenue", `${owner.role_name}'s offerings have no revenue of their own, so its bonus is split evenly among them`, owner.key);
|
|
50
|
+
for (const key of base.offeringKeys)
|
|
51
|
+
weights.set(key, 1);
|
|
52
|
+
}
|
|
53
|
+
const total = [...weights.values()].reduce((t, w) => t + w, 0);
|
|
54
|
+
for (const [key, w] of weights) {
|
|
55
|
+
const amount = (bonus * w) / total;
|
|
56
|
+
ctx.book({
|
|
57
|
+
cost_center_type: "offering",
|
|
58
|
+
cost_center_key: key,
|
|
59
|
+
account_id: owner.account_id,
|
|
60
|
+
function: ctx.functionFor(owner.account_id, null, "offering"),
|
|
61
|
+
amount,
|
|
62
|
+
line_key: `bonus:${owner.key}`,
|
|
63
|
+
line_name: `Bonus: ${owner.role_name}`,
|
|
64
|
+
explanation: `${owner.role_name}: ${owner.bonus_rate} × net of its products ${base.term === null ? "in no term" : `in ${base.term.name}`}`,
|
|
65
|
+
stage: "bonus",
|
|
66
|
+
mtdc_share: 0,
|
|
67
|
+
});
|
|
68
|
+
if (taxAccount === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
ctx.book({
|
|
71
|
+
cost_center_type: "offering",
|
|
72
|
+
cost_center_key: key,
|
|
73
|
+
account_id: taxAccount,
|
|
74
|
+
function: ctx.functionFor(owner.account_id, null, "offering"),
|
|
75
|
+
amount: amount * taxRate,
|
|
76
|
+
line_key: `bonus_payroll_tax:${owner.key}`,
|
|
77
|
+
line_name: `Payroll tax on bonus: ${owner.role_name}`,
|
|
78
|
+
explanation: `${taxRate} × ${owner.role_name}'s bonus`,
|
|
79
|
+
stage: "bonus",
|
|
80
|
+
mtdc_share: 0,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return bonus;
|
|
84
|
+
}
|
|
85
|
+
/** The program area of all the owner's products, or null when they're in more than one or none. */
|
|
86
|
+
function areaOf(ctx, owner) {
|
|
87
|
+
const areas = new Set(ctx.data.plan_bonus_owner_products
|
|
88
|
+
.filter((row) => row.bonus_owner_id === owner.id)
|
|
89
|
+
.map((row) => ctx.products.get(row.product_id)?.program_area_id ?? null));
|
|
90
|
+
return areas.size === 1 ? ([...areas][0] ?? null) : null;
|
|
69
91
|
}
|
|
70
92
|
/**
|
|
71
|
-
* The owner's products' net
|
|
72
|
-
* area's products, and warns about any other.
|
|
93
|
+
* The owner's products' net in each term they have an offering in, with those offerings, in term
|
|
94
|
+
* order and then no term. An area plan's bonus counts only that area's products, and warns about any other.
|
|
73
95
|
*/
|
|
74
|
-
function
|
|
96
|
+
function basesOf(ctx, owner, terms) {
|
|
75
97
|
const plan = ctx.planOfVersion.get(owner.version_id);
|
|
76
98
|
const products = new Set();
|
|
77
99
|
for (const row of ctx.data.plan_bonus_owner_products) {
|
|
@@ -84,21 +106,37 @@ function baseOf(ctx, owner) {
|
|
|
84
106
|
}
|
|
85
107
|
products.add(row.product_id);
|
|
86
108
|
}
|
|
87
|
-
const
|
|
109
|
+
const bases = new Map();
|
|
110
|
+
const baseFor = (term) => {
|
|
111
|
+
const base = bases.get(term?.key ?? null) ?? { term, net: 0, offeringKeys: [] };
|
|
112
|
+
bases.set(term?.key ?? null, base);
|
|
113
|
+
return base;
|
|
114
|
+
};
|
|
115
|
+
const owned = new Set();
|
|
116
|
+
for (const o of ctx.data.plan_offerings) {
|
|
117
|
+
if (!products.has(o.product_id))
|
|
118
|
+
continue;
|
|
119
|
+
owned.add(o.key);
|
|
120
|
+
baseFor(terms.get(o.key) ?? null).offeringKeys.push(o.key);
|
|
121
|
+
}
|
|
88
122
|
const afterPools = (owner.basis ?? "after_shared_costs") === "after_shared_costs";
|
|
89
|
-
let net = 0;
|
|
90
123
|
for (const e of ctx.entries) {
|
|
91
124
|
if (e.stage === "bonus" || (!afterPools && e.stage === "pool") || e.cost_center_key == null)
|
|
92
125
|
continue;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
let term;
|
|
127
|
+
if (e.cost_center_type === "offering" && owned.has(e.cost_center_key)) {
|
|
128
|
+
term = terms.get(e.cost_center_key) ?? null;
|
|
129
|
+
}
|
|
130
|
+
else if (e.cost_center_type === "product" && products.has(e.cost_center_key)) {
|
|
131
|
+
term = null;
|
|
132
|
+
}
|
|
133
|
+
else
|
|
96
134
|
continue;
|
|
97
135
|
const type = ctx.accountType(e.account_id);
|
|
98
136
|
if (type === "revenue")
|
|
99
|
-
net += e.amount;
|
|
137
|
+
baseFor(term).net += e.amount;
|
|
100
138
|
else if (type === "expense")
|
|
101
|
-
net -= e.amount;
|
|
139
|
+
baseFor(term).net -= e.amount;
|
|
102
140
|
}
|
|
103
|
-
return
|
|
141
|
+
return [...bases.values()].sort((a, b) => compareTerms(a.term, b.term));
|
|
104
142
|
}
|
package/dist/planning/engine.js
CHANGED
|
@@ -8,6 +8,7 @@ import { bookPayrollTax } from "./payroll.js";
|
|
|
8
8
|
import { allocatePools } from "./pools.js";
|
|
9
9
|
import { bookRevenue, countDays } from "./revenue.js";
|
|
10
10
|
import { bookSalaries } from "./salaried.js";
|
|
11
|
+
import { compareTerms, offeringTerms } from "./terms.js";
|
|
11
12
|
import { enrollmentOf, round2, Staffing } from "./staffing.js";
|
|
12
13
|
/**
|
|
13
14
|
* Calculates the plan year's P&L from a plan's rows. The steps run in the order
|
|
@@ -18,6 +19,7 @@ export function computePlan(data) {
|
|
|
18
19
|
for (const o of data.plan_offerings)
|
|
19
20
|
ctx.enrollment.set(o.id, enrollmentOf(o));
|
|
20
21
|
checkAreas(ctx);
|
|
22
|
+
const terms = offeringTerms(ctx);
|
|
21
23
|
const { activities, coaching, doubleBooked } = new Staffing(ctx).run(scheduleActivities(ctx));
|
|
22
24
|
countDays(ctx, activities);
|
|
23
25
|
bookRevenue(ctx);
|
|
@@ -30,15 +32,22 @@ export function computePlan(data) {
|
|
|
30
32
|
bookPayrollTax(ctx);
|
|
31
33
|
const mtdcByOffering = new Map(ctx.data.plan_offerings.map((o) => [o.key, mtdc(ctx, o.key)]));
|
|
32
34
|
allocatePools(ctx);
|
|
33
|
-
const bonuses = bookBonuses(ctx);
|
|
35
|
+
const bonuses = bookBonuses(ctx, terms);
|
|
36
|
+
const bonusByPosition = new Map();
|
|
37
|
+
for (const b of bonuses) {
|
|
38
|
+
if (b.position_key == null)
|
|
39
|
+
continue;
|
|
40
|
+
bonusByPosition.set(b.position_key, (bonusByPosition.get(b.position_key) ?? 0) + b.bonus_cents);
|
|
41
|
+
}
|
|
34
42
|
const lines = sumLines(ctx);
|
|
35
43
|
const organization = organizationTotals(ctx, lines);
|
|
36
44
|
return {
|
|
37
45
|
lines,
|
|
38
|
-
pnl: summarize(ctx, lines, mtdcByOffering),
|
|
46
|
+
pnl: summarize(ctx, lines, mtdcByOffering, terms),
|
|
39
47
|
organization,
|
|
40
48
|
activities,
|
|
41
|
-
positions: summarizePositions(ctx, coaching, charged,
|
|
49
|
+
positions: summarizePositions(ctx, coaching, charged, bonusByPosition, doubleBooked),
|
|
50
|
+
bonuses,
|
|
42
51
|
capital: rankCapital(ctx, lines, organization),
|
|
43
52
|
warnings: ctx.warnings,
|
|
44
53
|
};
|
|
@@ -173,8 +182,8 @@ function sumLines(ctx) {
|
|
|
173
182
|
})
|
|
174
183
|
.filter((l) => l.amount_cents !== 0);
|
|
175
184
|
}
|
|
176
|
-
/** Each plan-year offering's P&L, then each product's, then each program area's. */
|
|
177
|
-
function summarize(ctx, lines, mtdcByOffering) {
|
|
185
|
+
/** Each plan-year offering's P&L, then each product's, then each program area's, then each area's terms. */
|
|
186
|
+
function summarize(ctx, lines, mtdcByOffering, terms) {
|
|
178
187
|
const pnl = (level, key, own, counts) => {
|
|
179
188
|
const byAccount = new Map();
|
|
180
189
|
let revenue = 0;
|
|
@@ -231,24 +240,38 @@ function summarize(ctx, lines, mtdcByOffering) {
|
|
|
231
240
|
for (const l of lines)
|
|
232
241
|
if (l.cost_center_type === "program_area" && l.cost_center_key !== null)
|
|
233
242
|
areaIds.add(l.cost_center_key);
|
|
234
|
-
const areaPnl = [
|
|
243
|
+
const areaPnl = [];
|
|
244
|
+
const termPnl = [];
|
|
245
|
+
for (const id of [...areaIds].sort()) {
|
|
235
246
|
const own = offeringPnl.filter((s) => {
|
|
236
247
|
const product = ctx.offeringsByKey.get(s.key)?.product_id;
|
|
237
248
|
return product !== undefined && ctx.products.get(product)?.program_area_id === id;
|
|
238
249
|
});
|
|
239
|
-
const keys = new Set(own.map((s) => s.key));
|
|
240
250
|
const products = new Set(own.map((s) => ctx.offeringsByKey.get(s.key)?.product_id));
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
251
|
+
// Costs left on the area or a product, because it had no offerings to divide them among, are in no term.
|
|
252
|
+
const rollup = (level, offerings, leftovers) => {
|
|
253
|
+
const keys = new Set(offerings.map((s) => s.key));
|
|
254
|
+
const add = (field) => offerings.reduce((t, s) => t + s[field], 0);
|
|
255
|
+
return pnl(level, id, lines.filter((l) => (l.cost_center_type === "offering" && l.cost_center_key !== null && keys.has(l.cost_center_key)) ||
|
|
256
|
+
(leftovers && l.cost_center_type === "product" && products.has(l.cost_center_key ?? undefined)) ||
|
|
257
|
+
(leftovers && l.cost_center_type === "program_area" && l.cost_center_key === id)), {
|
|
258
|
+
mtdc_cents: add("mtdc_cents"),
|
|
259
|
+
enrollment: add("enrollment"),
|
|
260
|
+
days: add("days"),
|
|
261
|
+
sailor_days: add("sailor_days"),
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
areaPnl.push(rollup("program_area", own, true));
|
|
265
|
+
const termOf = (s) => terms.get(s.key) ?? null;
|
|
266
|
+
const areaTerms = [...new Map(own.map((s) => [termOf(s)?.key, termOf(s)])).values()].filter((t) => t !== null);
|
|
267
|
+
for (const term of [...areaTerms.sort(compareTerms), null]) {
|
|
268
|
+
const inTerm = own.filter((s) => termOf(s)?.key === term?.key);
|
|
269
|
+
const row = { ...rollup("program_area_term", inTerm, term === null), term_key: term?.key ?? null };
|
|
270
|
+
if (inTerm.length > 0 || row.by_account.length > 0)
|
|
271
|
+
termPnl.push(row);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return [...offeringPnl, ...productPnl, ...areaPnl, ...termPnl];
|
|
252
275
|
}
|
|
253
276
|
function divide(totals, by) {
|
|
254
277
|
if (by <= 0)
|
package/dist/planning/lines.js
CHANGED
package/dist/planning/revenue.js
CHANGED
|
@@ -51,7 +51,7 @@ export function bookRevenue(ctx) {
|
|
|
51
51
|
for (const charge of ctx.data.plan_revenue_charges) {
|
|
52
52
|
if (!(charge.product_kinds ?? []).includes(product.kind))
|
|
53
53
|
continue;
|
|
54
|
-
book(charge.account_id, total * charge.rate, `charge:${charge.key}`, charge.name, `${charge.name}: ${charge.rate} × revenue`, charge.
|
|
54
|
+
book(charge.account_id, total * charge.rate, `charge:${charge.key}`, charge.name, `${charge.name}: ${charge.rate} × revenue`, charge.in_mtdc ? 1 : 0);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from "./context.ts";
|
|
2
|
+
import type { Id, PlanTerm } from "./types.ts";
|
|
3
|
+
/** Each offering's term, by offering key, in no particular order. Null for an offering in none. */
|
|
4
|
+
export type OfferingTerms = Map<Id, PlanTerm | null>;
|
|
5
|
+
/**
|
|
6
|
+
* Finds each offering's term in the plan's terms. An offering whose term isn't there, such as one deleted
|
|
7
|
+
* from the Operations Plan, is in none, with a warning.
|
|
8
|
+
*/
|
|
9
|
+
export declare function offeringTerms(ctx: Context): OfferingTerms;
|
|
10
|
+
/** Orders terms by their first days, then names, with no term (null) last. */
|
|
11
|
+
export declare function compareTerms(a: PlanTerm | null, b: PlanTerm | null): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds each offering's term in the plan's terms. An offering whose term isn't there, such as one deleted
|
|
3
|
+
* from the Operations Plan, is in none, with a warning.
|
|
4
|
+
*/
|
|
5
|
+
export function offeringTerms(ctx) {
|
|
6
|
+
const terms = new Map((ctx.data.plan_terms ?? []).map((t) => [t.key, t]));
|
|
7
|
+
const byOffering = new Map();
|
|
8
|
+
for (const o of ctx.data.plan_offerings) {
|
|
9
|
+
const term = o.term_key == null ? null : (terms.get(o.term_key) ?? null);
|
|
10
|
+
if (o.term_key != null && term === null) {
|
|
11
|
+
ctx.warn("unknown_term", `${o.name} is in a term the Operations Plan doesn't have, so it counts as in no term`, o.key);
|
|
12
|
+
}
|
|
13
|
+
byOffering.set(o.key, term);
|
|
14
|
+
}
|
|
15
|
+
return byOffering;
|
|
16
|
+
}
|
|
17
|
+
/** Orders terms by their first days, then names, with no term (null) last. */
|
|
18
|
+
export function compareTerms(a, b) {
|
|
19
|
+
if (a === null || b === null)
|
|
20
|
+
return a === b ? 0 : a === null ? 1 : -1;
|
|
21
|
+
return a.starts_on.localeCompare(b.starts_on) || a.name.localeCompare(b.name) || a.key.localeCompare(b.key);
|
|
22
|
+
}
|
package/dist/planning/types.d.ts
CHANGED
|
@@ -192,8 +192,8 @@ export interface PlanBudgetLine extends Versioned {
|
|
|
192
192
|
cost_center_key: Id | null;
|
|
193
193
|
amount_cents: number;
|
|
194
194
|
fiscal_year: number;
|
|
195
|
-
/**
|
|
196
|
-
|
|
195
|
+
/** Whether it counts toward MTDC; cleared for a cost 2 CFR 200.1 leaves out, such as equipment or participant support. */
|
|
196
|
+
in_mtdc: boolean;
|
|
197
197
|
note: string | null;
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
@@ -274,7 +274,8 @@ export interface PlanRevenueCharge extends Versioned {
|
|
|
274
274
|
product_kinds: ProductKind[] | null;
|
|
275
275
|
/** A revenue account adds revenue; an expense account adds a cost. */
|
|
276
276
|
account_id: Id;
|
|
277
|
-
|
|
277
|
+
/** Whether it counts toward MTDC; cleared for one 2 CFR 200.1 leaves out, such as scholarships. */
|
|
278
|
+
in_mtdc: boolean;
|
|
278
279
|
note: string | null;
|
|
279
280
|
}
|
|
280
281
|
/** A day the organization is closed, such as Labor Day. */
|
|
@@ -349,9 +350,12 @@ export interface Totals {
|
|
|
349
350
|
}
|
|
350
351
|
/** One offering's P&L for the plan year, or a product's or program area's, which adds up its offerings. */
|
|
351
352
|
export interface Pnl extends Totals {
|
|
352
|
-
|
|
353
|
+
/** `program_area_term` is a program area's offerings in one term, or in none. */
|
|
354
|
+
level: "offering" | "product" | "program_area" | "program_area_term";
|
|
353
355
|
/** An offering key, a product id, or a program area id. */
|
|
354
356
|
key: Id;
|
|
357
|
+
/** A `program_area_term`'s `plan_terms` key, null for its offerings in no term. Only that level has one. */
|
|
358
|
+
term_key?: Id | null;
|
|
355
359
|
/** Each account's total, revenue and expense alike. */
|
|
356
360
|
by_account: {
|
|
357
361
|
account_id: Id;
|
|
@@ -447,10 +451,26 @@ export interface PositionSummary {
|
|
|
447
451
|
/** Dates it's on two or more groups at once. */
|
|
448
452
|
double_booked_dates: number;
|
|
449
453
|
}
|
|
454
|
+
/** A bonus owner's bonus for one term, or on its offerings in no term for the year. */
|
|
455
|
+
export interface BonusSummary {
|
|
456
|
+
bonus_owner_key: Id;
|
|
457
|
+
role_name: string;
|
|
458
|
+
position_key: Id | null;
|
|
459
|
+
/** The program area of all the owner's products, or null when they're in more than one. */
|
|
460
|
+
program_area_id: Id | null;
|
|
461
|
+
/** A `plan_terms` key, or null for its offerings in no term. */
|
|
462
|
+
term_key: Id | null;
|
|
463
|
+
bonus_rate: number;
|
|
464
|
+
/** The net the bonus is figured on, which can be below zero. */
|
|
465
|
+
net_cents: number;
|
|
466
|
+
bonus_cents: number;
|
|
467
|
+
}
|
|
450
468
|
export interface EngineOutput {
|
|
451
469
|
lines: CostLine[];
|
|
452
|
-
/** Each offering's P&L, then each product's, then each program area's. */
|
|
470
|
+
/** Each offering's P&L, then each product's, then each program area's, then each area's terms. */
|
|
453
471
|
pnl: Pnl[];
|
|
472
|
+
/** Each bonus owner's bonus in each term it has offerings in, then in no term. */
|
|
473
|
+
bonuses: BonusSummary[];
|
|
454
474
|
organization: OrganizationTotals;
|
|
455
475
|
activities: ComputedActivity[];
|
|
456
476
|
/** Each salaried position's pay for the year. */
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Every collection, for `createDirectus<Schema>()`. */
|
|
2
2
|
export interface Schema {
|
|
3
3
|
accounts: Accounts[];
|
|
4
|
+
announcements: Announcements[];
|
|
4
5
|
assets: Assets[];
|
|
5
6
|
audit_findings: AuditFindings[];
|
|
6
7
|
boat_classes: BoatClasses[];
|
|
@@ -26,6 +27,7 @@ export interface Schema {
|
|
|
26
27
|
group_policies: GroupPolicies[];
|
|
27
28
|
groups: Groups[];
|
|
28
29
|
households: Households[];
|
|
30
|
+
links: Links[];
|
|
29
31
|
medical_profiles: MedicalProfiles[];
|
|
30
32
|
offerings: Offerings[];
|
|
31
33
|
organization_cost_accounts: OrganizationCostAccounts[];
|
|
@@ -46,6 +48,7 @@ export interface Schema {
|
|
|
46
48
|
plan_pools: PlanPools[];
|
|
47
49
|
plan_programs: PlanPrograms[];
|
|
48
50
|
plan_rates: PlanRates[];
|
|
51
|
+
plan_result_bonuses: PlanResultBonuses[];
|
|
49
52
|
plan_result_capital: PlanResultCapital[];
|
|
50
53
|
plan_result_lines: PlanResultLines[];
|
|
51
54
|
plan_result_pnl: PlanResultPnl[];
|
|
@@ -113,6 +116,14 @@ export interface Accounts {
|
|
|
113
116
|
/** Kept for history, and closed to new lines. */
|
|
114
117
|
retired: boolean;
|
|
115
118
|
}
|
|
119
|
+
/** What Sail Team's home page shows, newest first. */
|
|
120
|
+
export interface Announcements {
|
|
121
|
+
id: string;
|
|
122
|
+
title: string;
|
|
123
|
+
body: string;
|
|
124
|
+
/** The date the home page shows. Newest comes first. */
|
|
125
|
+
published_on: string;
|
|
126
|
+
}
|
|
116
127
|
/** Each boat, motor and piece of equipment CSC owns: sailboats, powerboats such as the coach boats, motors, and others such as trailers. Planning counts the boats of each class CSC has on each day. */
|
|
117
128
|
export interface Assets {
|
|
118
129
|
id: string;
|
|
@@ -445,6 +456,24 @@ export interface Households {
|
|
|
445
456
|
share_contact: boolean | null;
|
|
446
457
|
date_created: string | null;
|
|
447
458
|
}
|
|
459
|
+
/** The Links page in Sail Team. Drag the rows to change their order. */
|
|
460
|
+
export interface Links {
|
|
461
|
+
id: string;
|
|
462
|
+
title: string;
|
|
463
|
+
/** The page's address, starting with https://. */
|
|
464
|
+
url: string;
|
|
465
|
+
/** A line under the title, if it needs one. */
|
|
466
|
+
note: string | null;
|
|
467
|
+
sort: number | null;
|
|
468
|
+
/** The page's own title, read from the page when the link is saved. */
|
|
469
|
+
preview_title: string | null;
|
|
470
|
+
/** The page's own summary, read from the page when the link is saved. */
|
|
471
|
+
preview_description: string | null;
|
|
472
|
+
/** The address of the page's own picture, read from the page when the link is saved. */
|
|
473
|
+
preview_image: string | null;
|
|
474
|
+
/** When the page was last read. Empty if it couldn't be read: save the link again to retry. */
|
|
475
|
+
previewed_at: string | null;
|
|
476
|
+
}
|
|
448
477
|
/** A person's medical information, kept apart from People so only some staff see it. */
|
|
449
478
|
export interface MedicalProfiles {
|
|
450
479
|
id: string;
|
|
@@ -652,8 +681,8 @@ export interface PlanBudgetLines {
|
|
|
652
681
|
/** In cents: 12500 is $125.00. */
|
|
653
682
|
amount_cents: number;
|
|
654
683
|
fiscal_year: number;
|
|
655
|
-
/**
|
|
656
|
-
|
|
684
|
+
/** Counts toward modified total direct costs (MTDC). Clear it for a cost 2 CFR 200.1 leaves out, such as equipment. */
|
|
685
|
+
in_mtdc: boolean;
|
|
657
686
|
note: string | null;
|
|
658
687
|
}
|
|
659
688
|
/** Something lasting CSC means to buy, ranked with the most needed first. */
|
|
@@ -768,6 +797,29 @@ export interface PlanRates {
|
|
|
768
797
|
/** In cents: 12500 is $125.00. */
|
|
769
798
|
mileage_rate_cents: number | null;
|
|
770
799
|
}
|
|
800
|
+
/** Each bonus in a Financial Plan, for each term, and for the offerings in no term on the year. A draft's are worked out again a moment after each change to it, and a saved one's when it's saved, and then never change. */
|
|
801
|
+
export interface PlanResultBonuses {
|
|
802
|
+
id: string;
|
|
803
|
+
/** The bonus's role, such as Director. */
|
|
804
|
+
name: string;
|
|
805
|
+
term_key: string | null;
|
|
806
|
+
/** The term it's paid on. Empty for the offerings in no term, such as Adult Learn to Sail, whose bonus is worked out on the year. */
|
|
807
|
+
term_name: string | null;
|
|
808
|
+
/** The term's first day, which puts the terms in order. */
|
|
809
|
+
term_starts_on: string | null;
|
|
810
|
+
/** A share of the net. 0.05 means 5%. */
|
|
811
|
+
bonus_rate: string;
|
|
812
|
+
/** The net of its products in the term, which the bonus is figured on. In cents: 12500 is $125.00. */
|
|
813
|
+
net_cents: number;
|
|
814
|
+
/** Its rate × the net, or nothing when the net is a loss. In cents: 12500 is $125.00. */
|
|
815
|
+
bonus_cents: number;
|
|
816
|
+
bonus_owner_key: string;
|
|
817
|
+
program_area_key: string | null;
|
|
818
|
+
/** The Financial Plan these results are for. */
|
|
819
|
+
version_set_id: string | VersionSets;
|
|
820
|
+
/** The plan in the Financial Plan that pays the bonus. */
|
|
821
|
+
version_set_member_id: string | VersionSetMembers | null;
|
|
822
|
+
}
|
|
771
823
|
/** The capital list in rank order, in a Financial Plan, with what the year's money covers. A draft's are worked out again a moment after each change to it, and a saved one's when it's saved, and then never change. */
|
|
772
824
|
export interface PlanResultCapital {
|
|
773
825
|
id: string;
|
|
@@ -807,7 +859,7 @@ export interface PlanResultLines {
|
|
|
807
859
|
/** A Financial Plan's P&L for each offering, product, and program area. A draft's are worked out again a moment after each change to it, and a saved one's when it's saved, and then never change. */
|
|
808
860
|
export interface PlanResultPnl {
|
|
809
861
|
id: string;
|
|
810
|
-
level: "offering" | "product" | "program_area";
|
|
862
|
+
level: "offering" | "product" | "program_area" | "program_area_term";
|
|
811
863
|
subject_key: string;
|
|
812
864
|
name: string;
|
|
813
865
|
/** In cents: 12500 is $125.00. */
|
|
@@ -825,6 +877,11 @@ export interface PlanResultPnl {
|
|
|
825
877
|
version_set_id: string | VersionSets;
|
|
826
878
|
/** The plan in the Financial Plan that holds what it's charged to. */
|
|
827
879
|
version_set_member_id: string | VersionSetMembers | null;
|
|
880
|
+
term_key: string | null;
|
|
881
|
+
/** For a program area by term, the term. Empty for its offerings in no term, such as Adult Learn to Sail. */
|
|
882
|
+
term_name: string | null;
|
|
883
|
+
/** The term's first day, which puts the terms in order. */
|
|
884
|
+
term_starts_on: string | null;
|
|
828
885
|
}
|
|
829
886
|
/** Each salaried position's pay for the year, in a Financial Plan. A draft's are worked out again a moment after each change to it, and a saved one's when it's saved, and then never change. */
|
|
830
887
|
export interface PlanResultPositions {
|
|
@@ -908,8 +965,8 @@ export interface PlanRevenueCharges {
|
|
|
908
965
|
product_kinds: ("program" | "charter" | "storage_lease" | "merchandise" | "event_hosting" | "other")[] | null;
|
|
909
966
|
/** A revenue account adds revenue, such as card fees families pay. An expense account adds a cost. */
|
|
910
967
|
account_id: string | Accounts;
|
|
911
|
-
/**
|
|
912
|
-
|
|
968
|
+
/** Counts toward MTDC. Clear it for a charge 2 CFR 200.1 leaves out, such as scholarships. */
|
|
969
|
+
in_mtdc: boolean;
|
|
913
970
|
note: string | null;
|
|
914
971
|
}
|
|
915
972
|
/** Hours of a salaried position booked to a product. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyc-seattle/platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.0",
|
|
4
4
|
"description": "Pulumi components for CYC's shared Google Cloud platform: WebService, the platform outputs reader, and the stacks that build the platform",
|
|
5
5
|
"author": "CYC Community Sailing Center",
|
|
6
6
|
"license": "Apache-2.0",
|