@cyc-seattle/platform 1.43.1 → 1.45.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/dist/planning/capital.d.ts +11 -0
- package/dist/planning/capital.js +60 -0
- package/dist/planning/engine.js +5 -1
- package/dist/planning/pools.d.ts +4 -2
- package/dist/planning/pools.js +62 -10
- package/dist/planning/types.d.ts +31 -0
- package/dist/schema/index.d.ts +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from "./context.ts";
|
|
2
|
+
import type { CostCenterBooker } from "./lines.ts";
|
|
3
|
+
import type { CapitalRank, CostLine, OrganizationTotals } from "./types.ts";
|
|
4
|
+
/** Records each planned purchase as capital on the organization, which pays for all capital. */
|
|
5
|
+
export declare function bookCapitalItems(ctx: Context, booker: CostCenterBooker): void;
|
|
6
|
+
/**
|
|
7
|
+
* The capital purchases in rank order, each with the running total and whether the year's operating
|
|
8
|
+
* and fundraising nets cover it. Capital entered as a budget line comes after the ranked items, with
|
|
9
|
+
* a warning, since it has no rank.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rankCapital(ctx: Context, lines: CostLine[], organization: OrganizationTotals): CapitalRank[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const byRank = (a, b) => (a.sort ?? Number.POSITIVE_INFINITY) - (b.sort ?? Number.POSITIVE_INFINITY) || a.name.localeCompare(b.name);
|
|
2
|
+
/** The capital items on an asset account, most needed first. The rest are left out, with a warning. */
|
|
3
|
+
function rankedItems(ctx) {
|
|
4
|
+
return (ctx.data.plan_capital_items ?? [])
|
|
5
|
+
.filter((item) => {
|
|
6
|
+
if (ctx.accountType(item.account_id) === "asset")
|
|
7
|
+
return true;
|
|
8
|
+
ctx.warn("capital_not_asset", `${item.name} isn't on an asset account, so it's left out of capital`, item.key);
|
|
9
|
+
return false;
|
|
10
|
+
})
|
|
11
|
+
.sort(byRank);
|
|
12
|
+
}
|
|
13
|
+
/** Records each planned purchase as capital on the organization, which pays for all capital. */
|
|
14
|
+
export function bookCapitalItems(ctx, booker) {
|
|
15
|
+
for (const item of rankedItems(ctx)) {
|
|
16
|
+
booker.add({
|
|
17
|
+
cost_center_type: "org",
|
|
18
|
+
cost_center_key: null,
|
|
19
|
+
account_id: item.account_id,
|
|
20
|
+
function: ctx.functionFor(item.account_id, null, "org"),
|
|
21
|
+
amount: item.amount_cents,
|
|
22
|
+
line_key: `capital:${item.key}`,
|
|
23
|
+
line_name: item.name,
|
|
24
|
+
explanation: item.name,
|
|
25
|
+
mtdc_share: 0,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The capital purchases in rank order, each with the running total and whether the year's operating
|
|
31
|
+
* and fundraising nets cover it. Capital entered as a budget line comes after the ranked items, with
|
|
32
|
+
* a warning, since it has no rank.
|
|
33
|
+
*/
|
|
34
|
+
export function rankCapital(ctx, lines, organization) {
|
|
35
|
+
const budget = lines
|
|
36
|
+
.filter((l) => l.line_key.startsWith("budget:") && ctx.accountType(l.account_id) === "asset")
|
|
37
|
+
.sort((a, b) => a.line_name.localeCompare(b.line_name));
|
|
38
|
+
for (const l of budget) {
|
|
39
|
+
ctx.warn("capital_unranked", `${l.line_name} is capital entered as a budget line, so it comes after the ranked capital list`, l.line_key.slice("budget:".length));
|
|
40
|
+
}
|
|
41
|
+
const available = organization.operating.net_cents + organization.fundraising.net_cents;
|
|
42
|
+
let running = 0;
|
|
43
|
+
return [
|
|
44
|
+
...rankedItems(ctx).map((i) => ({
|
|
45
|
+
key: i.key,
|
|
46
|
+
name: i.name,
|
|
47
|
+
account_id: i.account_id,
|
|
48
|
+
amount_cents: i.amount_cents,
|
|
49
|
+
})),
|
|
50
|
+
...budget.map((l) => ({
|
|
51
|
+
key: l.line_key.slice("budget:".length),
|
|
52
|
+
name: l.line_name,
|
|
53
|
+
account_id: l.account_id,
|
|
54
|
+
amount_cents: l.amount_cents,
|
|
55
|
+
})),
|
|
56
|
+
].map((item, i) => {
|
|
57
|
+
running += item.amount_cents;
|
|
58
|
+
return { rank: i + 1, ...item, running_cents: running, funded: running <= available };
|
|
59
|
+
});
|
|
60
|
+
}
|
package/dist/planning/engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scheduleActivities } from "./activities.js";
|
|
2
2
|
import { bookBonuses } from "./bonus.js";
|
|
3
|
+
import { bookCapitalItems, rankCapital } from "./capital.js";
|
|
3
4
|
import { Context } from "./context.js";
|
|
4
5
|
import { bookBudgetLines, bookLaborLines, CostCenterBooker } from "./lines.js";
|
|
5
6
|
import { mtdc } from "./measures.js";
|
|
@@ -22,6 +23,7 @@ export function computePlan(data) {
|
|
|
22
23
|
bookRevenue(ctx);
|
|
23
24
|
const booker = new CostCenterBooker(ctx);
|
|
24
25
|
bookBudgetLines(ctx, booker);
|
|
26
|
+
bookCapitalItems(ctx, booker);
|
|
25
27
|
bookLaborLines(ctx, booker);
|
|
26
28
|
const charged = bookSalaries(ctx, coaching, booker);
|
|
27
29
|
booker.flush();
|
|
@@ -30,12 +32,14 @@ export function computePlan(data) {
|
|
|
30
32
|
allocatePools(ctx);
|
|
31
33
|
const bonuses = bookBonuses(ctx);
|
|
32
34
|
const lines = sumLines(ctx);
|
|
35
|
+
const organization = organizationTotals(ctx, lines);
|
|
33
36
|
return {
|
|
34
37
|
lines,
|
|
35
38
|
pnl: summarize(ctx, lines, mtdcByOffering),
|
|
36
|
-
organization
|
|
39
|
+
organization,
|
|
37
40
|
activities,
|
|
38
41
|
positions: summarizePositions(ctx, coaching, charged, bonuses, doubleBooked),
|
|
42
|
+
capital: rankCapital(ctx, lines, organization),
|
|
39
43
|
warnings: ctx.warnings,
|
|
40
44
|
};
|
|
41
45
|
}
|
package/dist/planning/pools.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Context } from "./context.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Divides everything in each pool among
|
|
4
|
-
* amount's account and function.
|
|
3
|
+
* Divides everything in each pool among the plan year's programs' offerings by MTDC, keeping each
|
|
4
|
+
* amount's account and function. Non-program's net offsets the pools first (cyc-seattle/platform#242):
|
|
5
|
+
* that part of each pooled amount stays with the organization, so a gain shrinks every program's
|
|
6
|
+
* share and a loss grows it. Shared costs never count toward MTDC, so one pool's split never shifts
|
|
5
7
|
* another's.
|
|
6
8
|
*/
|
|
7
9
|
export declare function allocatePools(ctx: Context): void;
|
package/dist/planning/pools.js
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import { mtdc } from "./measures.js";
|
|
2
|
+
/** Whether an offering is a program's. Every other offering is non-program, such as a charter. */
|
|
3
|
+
function isProgram(ctx, productId) {
|
|
4
|
+
return ctx.products.get(productId)?.kind === "program";
|
|
5
|
+
}
|
|
6
|
+
/** Non-program's net before the pools: its offerings' revenue less their own costs. */
|
|
7
|
+
function nonProgramNet(ctx) {
|
|
8
|
+
const keys = new Set(ctx
|
|
9
|
+
.fyOfferings()
|
|
10
|
+
.filter((o) => !isProgram(ctx, o.product_id))
|
|
11
|
+
.map((o) => o.key));
|
|
12
|
+
let net = 0;
|
|
13
|
+
for (const e of ctx.entries) {
|
|
14
|
+
if (e.cost_center_type !== "offering" || !keys.has(e.cost_center_key ?? ""))
|
|
15
|
+
continue;
|
|
16
|
+
const type = ctx.accountType(e.account_id);
|
|
17
|
+
if (type === "revenue")
|
|
18
|
+
net += e.amount;
|
|
19
|
+
else if (type === "expense")
|
|
20
|
+
net -= e.amount;
|
|
21
|
+
}
|
|
22
|
+
return net;
|
|
23
|
+
}
|
|
2
24
|
/**
|
|
3
|
-
* Divides everything in each pool among
|
|
4
|
-
* amount's account and function.
|
|
25
|
+
* Divides everything in each pool among the plan year's programs' offerings by MTDC, keeping each
|
|
26
|
+
* amount's account and function. Non-program's net offsets the pools first (cyc-seattle/platform#242):
|
|
27
|
+
* that part of each pooled amount stays with the organization, so a gain shrinks every program's
|
|
28
|
+
* share and a loss grows it. Shared costs never count toward MTDC, so one pool's split never shifts
|
|
5
29
|
* another's.
|
|
6
30
|
*/
|
|
7
31
|
export function allocatePools(ctx) {
|
|
8
|
-
const weights = new Map(ctx
|
|
32
|
+
const weights = new Map(ctx
|
|
33
|
+
.fyOfferings()
|
|
34
|
+
.filter((o) => isProgram(ctx, o.product_id))
|
|
35
|
+
.map((o) => [o.key, Math.max(0, mtdc(ctx, o.key))]));
|
|
9
36
|
const total = [...weights.values()].reduce((t, w) => t + w, 0);
|
|
10
37
|
const pools = new Map(ctx.data.plan_pools.map((p) => [p.key, p]));
|
|
11
38
|
const pooled = [];
|
|
@@ -14,27 +41,52 @@ export function allocatePools(ctx) {
|
|
|
14
41
|
(e.cost_center_type === "pool" ? pooled : kept).push(e);
|
|
15
42
|
if (pooled.length === 0)
|
|
16
43
|
return;
|
|
44
|
+
const net = nonProgramNet(ctx);
|
|
17
45
|
ctx.entries.splice(0, ctx.entries.length, ...kept);
|
|
46
|
+
const overhead = pooled.reduce((t, e) => t + e.amount, 0);
|
|
47
|
+
// Each pooled amount gives up the same fraction, so the offset falls on each pool in proportion to its size.
|
|
48
|
+
let offset = overhead > 0 ? net / overhead : 0;
|
|
49
|
+
if (offset > 1) {
|
|
50
|
+
ctx.warn("non_program_exceeds_pools", `Non-program makes more than all the shared costs, so ${ctx.data.organization.name} keeps all of them`);
|
|
51
|
+
offset = 1;
|
|
52
|
+
}
|
|
18
53
|
if (total <= 0) {
|
|
19
|
-
ctx.warn("no_mtdc", `No offering this year has costs that count toward MTDC, so ${ctx.data.organization.name} keeps all the shared costs`);
|
|
54
|
+
ctx.warn("no_mtdc", `No program's offering this year has costs that count toward MTDC, so ${ctx.data.organization.name} keeps all the shared costs`);
|
|
20
55
|
}
|
|
21
56
|
for (const e of pooled) {
|
|
22
|
-
const
|
|
57
|
+
const name = pools.get(e.cost_center_key ?? "")?.name ?? "Pool";
|
|
23
58
|
const base = {
|
|
24
59
|
account_id: e.account_id,
|
|
25
60
|
function: e.function,
|
|
26
|
-
line_key: `pool:${e.cost_center_key}:${e.line_key}`,
|
|
27
|
-
line_name: `${pool?.name ?? "Pool"} share: ${e.line_name}`,
|
|
28
|
-
explanation: `${pool?.name ?? "Pool"}: ${e.explanation}`,
|
|
29
61
|
stage: "pool",
|
|
30
62
|
mtdc_share: 0,
|
|
31
63
|
};
|
|
64
|
+
const share = {
|
|
65
|
+
...base,
|
|
66
|
+
line_key: `pool:${e.cost_center_key}:${e.line_key}`,
|
|
67
|
+
line_name: `${name} share: ${e.line_name}`,
|
|
68
|
+
explanation: `${name}: ${e.explanation}`,
|
|
69
|
+
};
|
|
32
70
|
if (total <= 0) {
|
|
33
|
-
ctx.book({ ...
|
|
71
|
+
ctx.book({ ...share, cost_center_type: "org", cost_center_key: null, amount: e.amount });
|
|
34
72
|
continue;
|
|
35
73
|
}
|
|
74
|
+
ctx.book({
|
|
75
|
+
...base,
|
|
76
|
+
cost_center_type: "org",
|
|
77
|
+
cost_center_key: null,
|
|
78
|
+
amount: e.amount * offset,
|
|
79
|
+
line_key: `offset:${e.cost_center_key}:${e.line_key}`,
|
|
80
|
+
line_name: `${name} covered by non-program: ${e.line_name}`,
|
|
81
|
+
explanation: `${name}: ${e.explanation}, the part non-program's net covers`,
|
|
82
|
+
});
|
|
36
83
|
for (const [key, w] of weights) {
|
|
37
|
-
ctx.book({
|
|
84
|
+
ctx.book({
|
|
85
|
+
...share,
|
|
86
|
+
cost_center_type: "offering",
|
|
87
|
+
cost_center_key: key,
|
|
88
|
+
amount: (e.amount * (1 - offset) * w) / total,
|
|
89
|
+
});
|
|
38
90
|
}
|
|
39
91
|
}
|
|
40
92
|
}
|
package/dist/planning/types.d.ts
CHANGED
|
@@ -188,6 +188,18 @@ export interface PlanBudgetLine extends Versioned {
|
|
|
188
188
|
excluded_from_mtdc: boolean;
|
|
189
189
|
note: string | null;
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Something lasting the Operations Plan means to buy, ranked by `sort` with the most needed first.
|
|
193
|
+
* Its account is an asset account.
|
|
194
|
+
*/
|
|
195
|
+
export interface PlanCapitalItem extends Versioned {
|
|
196
|
+
name: string;
|
|
197
|
+
amount_cents: number;
|
|
198
|
+
account_id: Id;
|
|
199
|
+
sort: number | null;
|
|
200
|
+
note?: string | null;
|
|
201
|
+
link?: string | null;
|
|
202
|
+
}
|
|
191
203
|
/** The year's rates. An Operations plan version has at most one row. */
|
|
192
204
|
export interface PlanRates extends Versioned {
|
|
193
205
|
/** Employer payroll costs, as a share of wages. */
|
|
@@ -308,6 +320,7 @@ export interface PlanData {
|
|
|
308
320
|
plan_bonus_owners: PlanBonusOwner[];
|
|
309
321
|
plan_bonus_owner_products: PlanBonusOwnerProduct[];
|
|
310
322
|
plan_closed_dates?: PlanClosedDate[];
|
|
323
|
+
plan_capital_items?: PlanCapitalItem[];
|
|
311
324
|
}
|
|
312
325
|
/** One line of cost or revenue for the plan year, on one offering, product, pool or the organization. */
|
|
313
326
|
export interface CostLine {
|
|
@@ -388,6 +401,22 @@ export interface OrganizationTotals {
|
|
|
388
401
|
/** Operating net, plus fundraising net, less capital. */
|
|
389
402
|
total_cents: number;
|
|
390
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* One planned purchase in the ranked capital list: what's left of the year's money after operations
|
|
406
|
+
* and fundraising pays for each in rank order, as far as it goes.
|
|
407
|
+
*/
|
|
408
|
+
export interface CapitalRank {
|
|
409
|
+
rank: number;
|
|
410
|
+
/** The `plan_capital_items` key, or for capital entered as a budget line, that line's key. */
|
|
411
|
+
key: Id;
|
|
412
|
+
name: string;
|
|
413
|
+
account_id: Id;
|
|
414
|
+
amount_cents: number;
|
|
415
|
+
/** This item's amount plus every item ranked above it. */
|
|
416
|
+
running_cents: number;
|
|
417
|
+
/** Whether the operating net plus the fundraising net covers `running_cents`. */
|
|
418
|
+
funded: boolean;
|
|
419
|
+
}
|
|
391
420
|
/** One salaried position's year: what it's paid, and where its hours went. */
|
|
392
421
|
export interface PositionSummary {
|
|
393
422
|
position_key: Id;
|
|
@@ -421,5 +450,7 @@ export interface EngineOutput {
|
|
|
421
450
|
activities: ComputedActivity[];
|
|
422
451
|
/** Each salaried position's pay for the year. */
|
|
423
452
|
positions: PositionSummary[];
|
|
453
|
+
/** The planned capital purchases, most needed first. */
|
|
454
|
+
capital: CapitalRank[];
|
|
424
455
|
warnings: EngineWarning[];
|
|
425
456
|
}
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface Schema {
|
|
|
39
39
|
plan_bonus_owner_products: PlanBonusOwnerProducts[];
|
|
40
40
|
plan_bonus_owners: PlanBonusOwners[];
|
|
41
41
|
plan_budget_lines: PlanBudgetLines[];
|
|
42
|
+
plan_capital_items: PlanCapitalItems[];
|
|
42
43
|
plan_closed_dates: PlanClosedDates[];
|
|
43
44
|
plan_labor_lines: PlanLaborLines[];
|
|
44
45
|
plan_offerings: PlanOfferings[];
|
|
@@ -46,6 +47,7 @@ export interface Schema {
|
|
|
46
47
|
plan_programs: PlanPrograms[];
|
|
47
48
|
plan_rates: PlanRates[];
|
|
48
49
|
plan_resources: PlanResources[];
|
|
50
|
+
plan_result_capital: PlanResultCapital[];
|
|
49
51
|
plan_result_lines: PlanResultLines[];
|
|
50
52
|
plan_result_pnl: PlanResultPnl[];
|
|
51
53
|
plan_result_positions: PlanResultPositions[];
|
|
@@ -646,6 +648,26 @@ export interface PlanBudgetLines {
|
|
|
646
648
|
excluded_from_mtdc: boolean;
|
|
647
649
|
note: string | null;
|
|
648
650
|
}
|
|
651
|
+
/** Something lasting CSC means to buy, ranked with the most needed first. */
|
|
652
|
+
export interface PlanCapitalItems {
|
|
653
|
+
id: string;
|
|
654
|
+
/** The version this row belongs to. Capital goes in the Operations Plan's. */
|
|
655
|
+
version_id: string | PlanVersions;
|
|
656
|
+
/** Stays the same across versions. Copy it where another row asks for this one's Key. */
|
|
657
|
+
key: string;
|
|
658
|
+
/** What to buy, such as a coach boat. */
|
|
659
|
+
name: string;
|
|
660
|
+
/** In cents: 12500 is $125.00. */
|
|
661
|
+
amount_cents: number;
|
|
662
|
+
/** The asset account it goes on, such as Equipment. */
|
|
663
|
+
account_id: string | Accounts;
|
|
664
|
+
/** 1 is the most needed. Drag rows in the table to reorder them. */
|
|
665
|
+
sort: number | null;
|
|
666
|
+
/** Why CSC needs it. */
|
|
667
|
+
note: string | null;
|
|
668
|
+
/** A quote or listing for it, as a web address. */
|
|
669
|
+
link: string | null;
|
|
670
|
+
}
|
|
649
671
|
/** A day the organization is closed, such as Labor Day. The P&L warns about an activity on one. */
|
|
650
672
|
export interface PlanClosedDates {
|
|
651
673
|
id: string;
|
|
@@ -760,6 +782,22 @@ export interface PlanResources {
|
|
|
760
782
|
/** Set for a rental, whose cost is left out of modified total direct costs (MTDC). */
|
|
761
783
|
excluded_from_mtdc: boolean;
|
|
762
784
|
}
|
|
785
|
+
/** 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. */
|
|
786
|
+
export interface PlanResultCapital {
|
|
787
|
+
id: string;
|
|
788
|
+
/** The Financial Plan these results are for. */
|
|
789
|
+
version_set_id: string | VersionSets;
|
|
790
|
+
/** 1 is the most needed. */
|
|
791
|
+
rank: number | null;
|
|
792
|
+
item_key: string;
|
|
793
|
+
name: string | null;
|
|
794
|
+
/** In cents: 12500 is $125.00. */
|
|
795
|
+
amount_cents: number | null;
|
|
796
|
+
/** This and everything ranked above it. In cents: 12500 is $125.00. */
|
|
797
|
+
running_cents: number | null;
|
|
798
|
+
/** Whether what's left after operations and fundraising covers it, with everything ranked above it. */
|
|
799
|
+
funded: boolean | null;
|
|
800
|
+
}
|
|
763
801
|
/** Each cost and revenue line in a Financial Plan's P&L. 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. */
|
|
764
802
|
export interface PlanResultLines {
|
|
765
803
|
id: string;
|
|
@@ -1160,6 +1198,8 @@ export interface VersionSets {
|
|
|
1160
1198
|
saved_at: string | null;
|
|
1161
1199
|
/** The version of the planning engine that calculated its numbers. */
|
|
1162
1200
|
platform_version: string | null;
|
|
1201
|
+
/** Set when the Financial Plan is archived, which hides it from Planning's list. Clear it to bring the plan back. An active Financial Plan can't be archived. */
|
|
1202
|
+
archived_at: string | null;
|
|
1163
1203
|
/** Each plan in the Financial Plan. Add or remove one only while it's a draft. */
|
|
1164
1204
|
members: (string | VersionSetMembers)[];
|
|
1165
1205
|
/** The organization's P&L. A draft's is worked out again a moment after each change to it. A saved one's is what it showed when it was saved, worked out a moment after. One with none means working it out failed, and the logs say why. An admin can fill in a saved one's. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyc-seattle/platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.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",
|