@cyc-seattle/platform 1.46.0 → 1.48.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.
@@ -15,7 +15,7 @@ export interface ScheduledActivity {
15
15
  uses_boats: boolean;
16
16
  }
17
17
  /**
18
- * The plan year's activities. Paid time is start to end plus setup and put-away; on-water time is
18
+ * The plan's activities. Paid time is start to end plus setup and put-away; on-water time is
19
19
  * `on_water_minutes` if set, otherwise the whole span, and zero when the activity uses no boats.
20
20
  */
21
21
  export declare function scheduleActivities(ctx: Context): ScheduledActivity[];
@@ -1,6 +1,6 @@
1
1
  import { minutesOf } from "./dates.js";
2
2
  /**
3
- * The plan year's activities. Paid time is start to end plus setup and put-away; on-water time is
3
+ * The plan's activities. Paid time is start to end plus setup and put-away; on-water time is
4
4
  * `on_water_minutes` if set, otherwise the whole span, and zero when the activity uses no boats.
5
5
  */
6
6
  export function scheduleActivities(ctx) {
@@ -21,8 +21,6 @@ export function scheduleActivities(ctx) {
21
21
  ctx.warn("unknown_offering", `Activity ${activity.key} is for an offering that isn't in the plan`, activity.key);
22
22
  continue;
23
23
  }
24
- if (!ctx.inYear(offering))
25
- continue;
26
24
  const timed = activity.start_time != null && activity.end_time != null;
27
25
  const start = timed ? minutesOf(activity.start_time) : null;
28
26
  let end = timed ? minutesOf(activity.end_time) : null;
@@ -84,10 +84,7 @@ function baseOf(ctx, owner) {
84
84
  }
85
85
  products.add(row.product_id);
86
86
  }
87
- const offeringKeys = new Set(ctx
88
- .fyOfferings()
89
- .filter((o) => products.has(o.product_id))
90
- .map((o) => o.key));
87
+ const offeringKeys = new Set(ctx.data.plan_offerings.filter((o) => products.has(o.product_id)).map((o) => o.key));
91
88
  const afterPools = (owner.basis ?? "after_shared_costs") === "after_shared_costs";
92
89
  let net = 0;
93
90
  for (const e of ctx.entries) {
@@ -26,11 +26,6 @@ export interface Entry {
26
26
  export type EntryInput = Omit<Entry, "stage" | "mtdc_share" | "wages"> & Partial<Pick<Entry, "stage" | "mtdc_share" | "wages">>;
27
27
  /** Lookups over the input, and the entries recorded so far, shared by every step. */
28
28
  export declare class Context {
29
- /** The plan year's first and last days. */
30
- readonly fy: {
31
- from: DateString;
32
- to: DateString;
33
- };
34
29
  readonly entries: Entry[];
35
30
  readonly warnings: EngineWarning[];
36
31
  readonly accounts: Map<Id, Account>;
@@ -62,8 +57,4 @@ export declare class Context {
62
57
  accountUnder(accountId: Id, ancestorId: Id): boolean;
63
58
  /** The account a product's sales are booked to: its program area's, or its own outside an area. */
64
59
  revenueAccount(product: Product): Id | null;
65
- /** Whether the offering counts in the plan year: it does, whole, in the year it starts. */
66
- inYear(offering: PlanOffering): boolean;
67
- /** The plan year's offerings. */
68
- fyOfferings(): PlanOffering[];
69
60
  }
@@ -1,4 +1,3 @@
1
- import { fiscalYearDates, within } from "./dates.js";
2
1
  /** The warnings about what the blocks are given, or when they run. The rest are about the data. */
3
2
  const ALLOCATION_WARNINGS = new Set([
4
3
  "over_inventory_boats",
@@ -21,8 +20,6 @@ const COST_TYPES = {
21
20
  };
22
21
  /** Lookups over the input, and the entries recorded so far, shared by every step. */
23
22
  export class Context {
24
- /** The plan year's first and last days. */
25
- fy;
26
23
  entries = [];
27
24
  warnings = [];
28
25
  accounts;
@@ -41,7 +38,6 @@ export class Context {
41
38
  data;
42
39
  constructor(data) {
43
40
  this.data = data;
44
- this.fy = fiscalYearDates(data.fiscal_year, data.organization.fiscal_year_start_month);
45
41
  this.accounts = new Map(data.accounts.map((a) => [a.id, a]));
46
42
  this.products = new Map(data.products.map((p) => [p.id, p]));
47
43
  this.areas = new Map(data.program_areas.map((a) => [a.id, a]));
@@ -124,12 +120,4 @@ export class Context {
124
120
  }
125
121
  return area.revenue_account_id;
126
122
  }
127
- /** Whether the offering counts in the plan year: it does, whole, in the year it starts. */
128
- inYear(offering) {
129
- return within(offering.starts_on, this.fy.from, this.fy.to);
130
- }
131
- /** The plan year's offerings. */
132
- fyOfferings() {
133
- return this.data.plan_offerings.filter((o) => this.inYear(o));
134
- }
135
123
  }
@@ -1,12 +1,4 @@
1
1
  import type { DateString, TimeString } from "./types.ts";
2
- export declare function addDays(date: DateString, days: number): DateString;
3
2
  /** ISO weekday, 1 = Monday. */
4
3
  export declare function isoWeekday(date: DateString): number;
5
- /** Fiscal year N ends in calendar year N, so a July start makes FY2027 2026-07-01 to 2027-06-30. */
6
- export declare function fiscalYearDates(fiscalYear: number, startMonth: number): {
7
- from: DateString;
8
- to: DateString;
9
- };
10
4
  export declare function minutesOf(time: TimeString): number;
11
- /** Whether `date` falls in a window whose open ends are unbounded. */
12
- export declare function within(date: DateString, from: DateString | null | undefined, to: DateString | null | undefined): boolean;
@@ -1,31 +1,12 @@
1
1
  function toUtc(date) {
2
2
  return new Date(`${date}T00:00:00Z`);
3
3
  }
4
- function fromUtc(d) {
5
- return d.toISOString().slice(0, 10);
6
- }
7
- export function addDays(date, days) {
8
- const d = toUtc(date);
9
- d.setUTCDate(d.getUTCDate() + days);
10
- return fromUtc(d);
11
- }
12
4
  /** ISO weekday, 1 = Monday. */
13
5
  export function isoWeekday(date) {
14
6
  const day = toUtc(date).getUTCDay();
15
7
  return day === 0 ? 7 : day;
16
8
  }
17
- /** Fiscal year N ends in calendar year N, so a July start makes FY2027 2026-07-01 to 2027-06-30. */
18
- export function fiscalYearDates(fiscalYear, startMonth) {
19
- const startYear = startMonth === 1 ? fiscalYear : fiscalYear - 1;
20
- const from = `${startYear}-${String(startMonth).padStart(2, "0")}-01`;
21
- const nextYear = `${startYear + 1}-${String(startMonth).padStart(2, "0")}-01`;
22
- return { from, to: addDays(nextYear, -1) };
23
- }
24
9
  export function minutesOf(time) {
25
10
  const [h, m] = time.split(":").map(Number);
26
11
  return h * 60 + m;
27
12
  }
28
- /** Whether `date` falls in a window whose open ends are unbounded. */
29
- export function within(date, from, to) {
30
- return (from == null || date >= from) && (to == null || date <= to);
31
- }
@@ -28,7 +28,7 @@ export function computePlan(data) {
28
28
  const charged = bookSalaries(ctx, coaching, booker);
29
29
  booker.flush();
30
30
  bookPayrollTax(ctx);
31
- const mtdcByOffering = new Map(ctx.fyOfferings().map((o) => [o.key, mtdc(ctx, o.key)]));
31
+ const mtdcByOffering = new Map(ctx.data.plan_offerings.map((o) => [o.key, mtdc(ctx, o.key)]));
32
32
  allocatePools(ctx);
33
33
  const bonuses = bookBonuses(ctx);
34
34
  const lines = sumLines(ctx);
@@ -49,7 +49,7 @@ export function computePlan(data) {
49
49
  * products in no area. The catalog stays live, so the database can't check these.
50
50
  */
51
51
  function checkAreas(ctx) {
52
- for (const offering of ctx.fyOfferings()) {
52
+ for (const offering of ctx.data.plan_offerings) {
53
53
  const product = ctx.products.get(offering.product_id);
54
54
  if (product === undefined)
55
55
  continue;
@@ -200,7 +200,7 @@ function summarize(ctx, lines, mtdcByOffering) {
200
200
  ...counts,
201
201
  };
202
202
  };
203
- const offerings = ctx.fyOfferings().sort((a, b) => a.key.localeCompare(b.key));
203
+ const offerings = [...ctx.data.plan_offerings].sort((a, b) => a.key.localeCompare(b.key));
204
204
  const offeringPnl = offerings.map((o) => {
205
205
  const enrollment = ctx.enrollment.get(o.id) ?? 0;
206
206
  const days = ctx.days.get(o.key) ?? 0;
@@ -4,7 +4,7 @@ import type { Id } from "./types.ts";
4
4
  export declare function mtdc(ctx: Context, offeringKey: Id): number;
5
5
  export declare function revenue(ctx: Context, offeringKey: Id): number;
6
6
  /**
7
- * How an amount on a product or program area divides among its plan-year offerings, by MTDC. Null
7
+ * How an amount on a product or program area divides among its offerings, by MTDC. Null
8
8
  * when it has none.
9
9
  */
10
10
  export declare function spreadWeights(ctx: Context, type: "product" | "program_area", key: Id): Map<Id, number> | null;
@@ -19,13 +19,11 @@ export function revenue(ctx, offeringKey) {
19
19
  return sumEntries(ctx, offeringKey, (e) => (ctx.accountType(e.account_id) === "revenue" ? 1 : 0));
20
20
  }
21
21
  /**
22
- * How an amount on a product or program area divides among its plan-year offerings, by MTDC. Null
22
+ * How an amount on a product or program area divides among its offerings, by MTDC. Null
23
23
  * when it has none.
24
24
  */
25
25
  export function spreadWeights(ctx, type, key) {
26
- const offerings = ctx
27
- .fyOfferings()
28
- .filter((o) => type === "product" ? o.product_id === key : ctx.products.get(o.product_id)?.program_area_id === key);
26
+ const offerings = ctx.data.plan_offerings.filter((o) => type === "product" ? o.product_id === key : ctx.products.get(o.product_id)?.program_area_id === key);
29
27
  if (offerings.length === 0)
30
28
  return null;
31
29
  const weights = new Map(offerings.map((o) => [o.key, Math.max(0, mtdc(ctx, o.key))]));
@@ -1,6 +1,6 @@
1
1
  import type { Context } from "./context.ts";
2
2
  /**
3
- * Divides everything in each pool among the plan year's programs' offerings by MTDC, keeping each
3
+ * Divides everything in each pool among the plan's programs' offerings by MTDC, keeping each
4
4
  * amount's account and function. Non-program's net offsets the pools first (cyc-seattle/platform#242):
5
5
  * that part of each pooled amount stays with the organization, so a gain shrinks every program's
6
6
  * share and a loss grows it. Shared costs never count toward MTDC, so one pool's split never shifts
@@ -5,10 +5,7 @@ function isProgram(ctx, productId) {
5
5
  }
6
6
  /** Non-program's net before the pools: its offerings' revenue less their own costs. */
7
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));
8
+ const keys = new Set(ctx.data.plan_offerings.filter((o) => !isProgram(ctx, o.product_id)).map((o) => o.key));
12
9
  let net = 0;
13
10
  for (const e of ctx.entries) {
14
11
  if (e.cost_center_type !== "offering" || !keys.has(e.cost_center_key ?? ""))
@@ -22,15 +19,14 @@ function nonProgramNet(ctx) {
22
19
  return net;
23
20
  }
24
21
  /**
25
- * Divides everything in each pool among the plan year's programs' offerings by MTDC, keeping each
22
+ * Divides everything in each pool among the plan's programs' offerings by MTDC, keeping each
26
23
  * amount's account and function. Non-program's net offsets the pools first (cyc-seattle/platform#242):
27
24
  * that part of each pooled amount stays with the organization, so a gain shrinks every program's
28
25
  * share and a loss grows it. Shared costs never count toward MTDC, so one pool's split never shifts
29
26
  * another's.
30
27
  */
31
28
  export function allocatePools(ctx) {
32
- const weights = new Map(ctx
33
- .fyOfferings()
29
+ const weights = new Map(ctx.data.plan_offerings
34
30
  .filter((o) => isProgram(ctx, o.product_id))
35
31
  .map((o) => [o.key, Math.max(0, mtdc(ctx, o.key))]));
36
32
  const total = [...weights.values()].reduce((t, w) => t + w, 0);
@@ -15,7 +15,7 @@ export function countDays(ctx, activities) {
15
15
  * account, a cost.
16
16
  */
17
17
  export function bookRevenue(ctx) {
18
- for (const offering of ctx.fyOfferings()) {
18
+ for (const offering of ctx.data.plan_offerings) {
19
19
  const product = ctx.products.get(offering.product_id);
20
20
  if (product === undefined) {
21
21
  ctx.warn("unknown_product", `Offering ${offering.key} is for a product that doesn't exist`, offering.key);
@@ -38,9 +38,9 @@ export declare class Staffing {
38
38
  private programs;
39
39
  /**
40
40
  * Warns when a block's sailors are more than its staff or its boats can safely take. Each program's
41
- * sailors count against its own sailors per coach, and need coaches at its own staff level. A
42
- * salaried position on the block counts as a coach for any of its programs, since the plan trusts
43
- * the planner's allocation (#208). Boats count the ones sailors bring.
41
+ * sailors count against its own sailors per coach, and every coach on the block counts, at any
42
+ * staff level or salaried: the ratio is required, a coach's level isn't (cyc-seattle/sail-team#82).
43
+ * Boats count the ones sailors bring.
44
44
  */
45
45
  private checkCapacity;
46
46
  /**
@@ -130,9 +130,9 @@ export class Staffing {
130
130
  }
131
131
  /**
132
132
  * Warns when a block's sailors are more than its staff or its boats can safely take. Each program's
133
- * sailors count against its own sailors per coach, and need coaches at its own staff level. A
134
- * salaried position on the block counts as a coach for any of its programs, since the plan trusts
135
- * the planner's allocation (#208). Boats count the ones sailors bring.
133
+ * sailors count against its own sailors per coach, and every coach on the block counts, at any
134
+ * staff level or salaried: the ratio is required, a coach's level isn't (cyc-seattle/sail-team#82).
135
+ * Boats count the ones sailors bring.
136
136
  */
137
137
  checkCapacity(block, programs) {
138
138
  const { ctx } = this;
@@ -145,8 +145,8 @@ export class Staffing {
145
145
  const scope = { version_id: block.row.version_id, date: block.row.date };
146
146
  // The block's offerings, with the verb that agrees with them: "Guppies Week 2 has".
147
147
  const has = `${offeringsOf(block)} ${new Set(block.members.map((a) => a.offering.id)).size > 1 ? "have" : "has"}`;
148
- // Coaches each level needs, from the sailors of the programs coached at it.
149
- const needed = new Map();
148
+ // Coaches the block needs: each program's sailors over its sailors per coach.
149
+ let load = 0;
150
150
  let counted = 0;
151
151
  for (const o of offerings) {
152
152
  const settings = programs.get(o.product_id);
@@ -155,21 +155,11 @@ export class Staffing {
155
155
  continue;
156
156
  }
157
157
  const n = ctx.enrollment.get(o.id) ?? 0;
158
- needed.set(settings.staff_level_key, (needed.get(settings.staff_level_key) ?? 0) + n / settings.sailors_per_coach);
158
+ load += n / settings.sailors_per_coach;
159
159
  counted += n;
160
160
  }
161
161
  if (counted > 0) {
162
- const load = sum(needed.values());
163
- // Each level's staff cover only its own programs; salaried positions cover what's left.
164
- let short = 0;
165
- let covered = 0;
166
- for (const [level, need] of needed) {
167
- const have = sum(block.staff.filter((s) => s.position_key == null && s.staff_level_key === level).map((s) => s.count));
168
- covered += Math.min(need, have);
169
- short += Math.max(0, need - have);
170
- }
171
- const salaried = sum(block.staff.filter((s) => s.position_key != null).map((s) => s.count));
172
- covered += Math.min(short, salaried);
162
+ const covered = Math.min(load, sum(block.staff.map((s) => s.count)));
173
163
  if (load > covered + 1e-9) {
174
164
  ctx.warn("over_capacity", covered === 0
175
165
  ? `${has} ${countOf(counted, "sailor")} and no coach`
@@ -415,7 +405,7 @@ export class Staffing {
415
405
  const { ctx } = this;
416
406
  for (const trip of ctx.data.plan_trips) {
417
407
  const offering = ctx.offeringsById.get(trip.offering_id);
418
- if (offering === undefined || !ctx.inYear(offering))
408
+ if (offering === undefined)
419
409
  continue;
420
410
  const travel = (a) => a.kind === "travel" && a.trip?.id === trip.id;
421
411
  const staffed = [...blocks.values()].filter((b) => b.members.some(travel));
@@ -247,7 +247,7 @@ export interface PlanBoatRate extends Versioned {
247
247
  /** How many boats of the class the organization has. */
248
248
  quantity: number | null;
249
249
  }
250
- /** A group of shared costs, such as G&A, divided among all of the plan year's offerings by MTDC. */
250
+ /** A group of shared costs, such as G&A, divided among all of the plan's offerings by MTDC. */
251
251
  export interface PlanPool extends Versioned {
252
252
  name: string;
253
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyc-seattle/platform",
3
- "version": "1.46.0",
3
+ "version": "1.48.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",