@cyc-seattle/platform 1.22.1 → 1.24.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.
@@ -44,6 +44,7 @@ export function bookBonuses(ctx) {
44
44
  function: ctx.functionFor(owner.account_id, null, "offering"),
45
45
  amount,
46
46
  line_key: `bonus:${owner.key}`,
47
+ line_name: `Bonus: ${owner.role_name}`,
47
48
  explanation: `${owner.role_name}: ${owner.bonus_rate} × net of its products`,
48
49
  stage: "bonus",
49
50
  mtdc_share: 0,
@@ -57,6 +58,7 @@ export function bookBonuses(ctx) {
57
58
  function: ctx.functionFor(owner.account_id, null, "offering"),
58
59
  amount: amount * taxRate,
59
60
  line_key: `bonus_payroll_tax:${owner.key}`,
61
+ line_name: `Payroll tax on bonus: ${owner.role_name}`,
60
62
  explanation: `${taxRate} × ${owner.role_name}'s bonus`,
61
63
  stage: "bonus",
62
64
  mtdc_share: 0,
@@ -9,6 +9,8 @@ export interface Entry {
9
9
  function: AccountFunction | null;
10
10
  amount: number;
11
11
  line_key: string;
12
+ /** What the line is, in plain words. */
13
+ line_name: string;
12
14
  explanation: string;
13
15
  stage: Stage;
14
16
  /** How much of the amount MTDC counts: 0 when excluded, a fraction for a mix of rented and owned RIBs. */
@@ -7,7 +7,7 @@ import { bookPayrollTax } from "./payroll.js";
7
7
  import { allocatePools } from "./pools.js";
8
8
  import { bookRevenue, countDays } from "./revenue.js";
9
9
  import { bookSalaries } from "./salaried.js";
10
- import { enrollmentOf, Staffing } from "./staffing.js";
10
+ import { enrollmentOf, round2, Staffing } from "./staffing.js";
11
11
  /**
12
12
  * Calculates the plan year's P&L from a plan's rows. The steps run in the order
13
13
  * docs/planning-engine.md gives, each reading only what earlier steps recorded.
@@ -17,7 +17,7 @@ export function computePlan(data) {
17
17
  for (const o of data.plan_offerings)
18
18
  ctx.enrollment.set(o.id, enrollmentOf(o));
19
19
  checkAreas(ctx);
20
- const { activities, coaching } = new Staffing(ctx).run(scheduleActivities(ctx));
20
+ const { activities, coaching, doubleBooked } = new Staffing(ctx).run(scheduleActivities(ctx));
21
21
  countDays(ctx, activities);
22
22
  bookRevenue(ctx);
23
23
  const booker = new CostCenterBooker(ctx);
@@ -35,7 +35,7 @@ export function computePlan(data) {
35
35
  pnl: summarize(ctx, lines, mtdcByOffering),
36
36
  organization: organizationTotals(ctx, lines),
37
37
  activities,
38
- positions: summarizePositions(ctx, coaching, charged, bonuses),
38
+ positions: summarizePositions(ctx, coaching, charged, bonuses, doubleBooked),
39
39
  warnings: ctx.warnings,
40
40
  };
41
41
  }
@@ -66,9 +66,17 @@ function checkAreas(ctx) {
66
66
  ctx.warn("offering_in_wrong_plan", `Offering ${offering.name} of ${product.name} is in ${where}`, offering.key);
67
67
  }
68
68
  }
69
+ const operations = new Set(ctx.data.plan_versions.filter((v) => ctx.planOfVersion.get(v.id)?.kind === "operations").map((v) => v.id));
70
+ for (const version of ctx.data.plan_versions) {
71
+ const pinned = version.operations_version_id;
72
+ if (pinned == null || operations.size === 0 || operations.has(pinned))
73
+ continue;
74
+ const area = ctx.areas.get(ctx.planOfVersion.get(version.id)?.program_area_id ?? "");
75
+ ctx.warn("other_operations_version", `${area?.name ?? "A program area"}'s plan was saved with a different Operations Plan version from the one it's calculated with`, version.id);
76
+ }
69
77
  }
70
78
  /** Each salaried position's year: its salary, where its hours went, and its hourly wages and bonus. */
71
- function summarizePositions(ctx, coaching, charged, bonuses) {
79
+ function summarizePositions(ctx, coaching, charged, bonuses, doubleBooked) {
72
80
  const taxRate = ctx.rates?.payroll_tax_rate ?? 0;
73
81
  const known = new Set(ctx.data.plan_salaried_positions.map((p) => p.key));
74
82
  const named = [
@@ -99,6 +107,8 @@ function summarizePositions(ctx, coaching, charged, bonuses) {
99
107
  bonus_cents: Math.round(bonus),
100
108
  total_pay_cents: Math.round(total),
101
109
  payroll_tax_cents: Math.round(total * taxRate),
110
+ double_booked_hours: round2(doubleBooked.get(position.key)?.hours ?? 0),
111
+ double_booked_dates: doubleBooked.get(position.key)?.dates ?? 0,
102
112
  };
103
113
  });
104
114
  }
@@ -143,6 +153,7 @@ function sumLines(ctx) {
143
153
  function: e.function,
144
154
  amount_cents: 0,
145
155
  line_key: e.line_key,
156
+ line_name: e.line_name,
146
157
  explanation: e.explanation,
147
158
  exact: 0,
148
159
  };
@@ -74,6 +74,7 @@ export function bookBudgetLines(ctx, booker) {
74
74
  function: ctx.functionFor(line.account_id, line.function, onOrg ? "org" : line.cost_center_type),
75
75
  amount: line.amount_cents,
76
76
  line_key: `budget:${line.key}`,
77
+ line_name: line.name,
77
78
  explanation: line.name,
78
79
  mtdc_share: line.excluded_from_mtdc ? 0 : 1,
79
80
  });
@@ -97,6 +98,7 @@ export function bookLaborLines(ctx, booker) {
97
98
  function: ctx.functionFor(line.account_id, line.function, line.cost_center_type),
98
99
  amount: level.pay_type === "volunteer" ? 0 : line.hours * (level.hourly_rate_cents ?? 0),
99
100
  line_key: `labor:${line.key}`,
101
+ line_name: line.name,
100
102
  explanation: `${line.name}: hours × ${level.name}'s rate`,
101
103
  wages: true,
102
104
  });
@@ -25,6 +25,7 @@ export function bookPayrollTax(ctx) {
25
25
  function: w.function,
26
26
  amount: w.amount * rate,
27
27
  line_key: "payroll_tax",
28
+ line_name: "Payroll tax",
28
29
  explanation: `${rate} × wages`,
29
30
  });
30
31
  }
@@ -24,6 +24,7 @@ export function allocatePools(ctx) {
24
24
  account_id: e.account_id,
25
25
  function: e.function,
26
26
  line_key: `pool:${e.cost_center_key}:${e.line_key}`,
27
+ line_name: `${pool?.name ?? "Pool"} share: ${e.line_name}`,
27
28
  explanation: `${pool?.name ?? "Pool"}: ${e.explanation}`,
28
29
  stage: "pool",
29
30
  mtdc_share: 0,
@@ -31,26 +31,27 @@ export function bookRevenue(ctx) {
31
31
  ctx.warn("no_revenue_account", `Product ${product.name} has no revenue account`, product.id);
32
32
  continue;
33
33
  }
34
- const book = (account, amount, line, explanation, mtdc_share = 1) => ctx.book({
34
+ const book = (account, amount, line, name, explanation, mtdc_share = 1) => ctx.book({
35
35
  cost_center_type: "offering",
36
36
  cost_center_key: offering.key,
37
37
  account_id: account,
38
38
  function: ctx.functionFor(account, null, "offering"),
39
39
  amount,
40
40
  line_key: `${line}:${offering.key}`,
41
+ line_name: name,
41
42
  explanation,
42
43
  mtdc_share,
43
44
  });
44
- book(revenueAccount, total, "revenue", `${price}¢ × ${enrollment} enrolled`);
45
+ book(revenueAccount, total, "revenue", "Revenue", `${price}¢ × ${enrollment} enrolled`);
45
46
  if (offering.cogs_rate != null && offering.cogs_rate > 0) {
46
47
  const cogs = ctx.costAccount("cogs");
47
48
  if (cogs !== undefined)
48
- book(cogs, total * offering.cogs_rate, "cogs", `${offering.cogs_rate} × revenue`);
49
+ book(cogs, total * offering.cogs_rate, "cogs", "Cost of goods sold", `${offering.cogs_rate} × revenue`);
49
50
  }
50
51
  for (const charge of ctx.data.plan_revenue_charges) {
51
52
  if (!(charge.product_kinds ?? []).includes(product.kind))
52
53
  continue;
53
- book(charge.account_id, total * charge.rate, `charge:${charge.key}`, `${charge.name}: ${charge.rate} × revenue`, charge.excluded_from_mtdc ? 0 : 1);
54
+ book(charge.account_id, total * charge.rate, `charge:${charge.key}`, charge.name, `${charge.name}: ${charge.rate} × revenue`, charge.excluded_from_mtdc ? 0 : 1);
54
55
  }
55
56
  }
56
57
  }
@@ -74,6 +74,7 @@ export function bookSalaries(ctx, coaching, booker) {
74
74
  function: ctx.functionFor(account, null, "offering"),
75
75
  amount: c.hours * (rate ?? 0) * scale,
76
76
  line_key: `salary:${position.key}:${c.line}`,
77
+ line_name: `${position.name}: ${c.line}`,
77
78
  explanation: `${position.name}: ${c.line} hours at the salary's hourly equivalent`,
78
79
  wages: true,
79
80
  });
@@ -86,6 +87,7 @@ export function bookSalaries(ctx, coaching, booker) {
86
87
  function: ctx.functionFor(account, null, "pool"),
87
88
  amount: salary - demand * scale,
88
89
  line_key: `salary:${position.key}:remainder`,
90
+ line_name: `${position.name}: rest of salary`,
89
91
  explanation: `${position.name}: salary not charged to coaching or bookings`,
90
92
  wages: true,
91
93
  });
@@ -11,6 +11,13 @@ export interface SalariedCoaching {
11
11
  /** The wages for `hourly` hours. */
12
12
  wages: number;
13
13
  }
14
+ /** A salaried position's year of coaching two groups at once. */
15
+ export interface DoubleBooked {
16
+ /** Hours another coach would need: n − 1 for each hour it's on n groups at once. */
17
+ hours: number;
18
+ /** Dates it's on two groups at once. */
19
+ dates: number;
20
+ }
14
21
  /** Whole kids: `target_enrollment` if set, otherwise `capacity × fill_rate` rounded. */
15
22
  export declare function enrollmentOf(offering: PlanOffering): number;
16
23
  export declare class Staffing {
@@ -42,7 +49,7 @@ export declare class Staffing {
42
49
  private overlapping;
43
50
  /**
44
51
  * Warns when a salaried position coaches two activities at once that aren't staffed as one block
45
- * group. It's still charged to both.
52
+ * group. It's still charged to both. Returns each position's double-booked hours and dates.
46
53
  */
47
54
  private checkDoubleBooked;
48
55
  /**
@@ -54,6 +61,7 @@ export declare class Staffing {
54
61
  run(scheduled: ScheduledActivity[]): {
55
62
  activities: ComputedActivity[];
56
63
  coaching: SalariedCoaching[];
64
+ doubleBooked: Map<Id, DoubleBooked>;
57
65
  };
58
66
  /**
59
67
  * A trip's coaches each fly and stay; the miles are driven once. Its coaches are those of its
@@ -231,7 +231,7 @@ export class Staffing {
231
231
  }
232
232
  /**
233
233
  * Warns when a salaried position coaches two activities at once that aren't staffed as one block
234
- * group. It's still charged to both.
234
+ * group. It's still charged to both. Returns each position's double-booked hours and dates.
235
235
  */
236
236
  checkDoubleBooked(staffed) {
237
237
  const byDay = new Map();
@@ -244,6 +244,7 @@ export class Staffing {
244
244
  }
245
245
  }
246
246
  const names = new Map(this.ctx.data.plan_salaried_positions.map((p) => [p.key, p.name]));
247
+ const result = new Map();
247
248
  for (const [id, list] of byDay) {
248
249
  const [key, date] = id.split("|");
249
250
  const clash = list.flatMap((x, i) => list
@@ -251,8 +252,13 @@ export class Staffing {
251
252
  .flatMap((y) => ((x.group !== null && x.group === y.group) || !overlaps(x, y) ? [] : [[x, y]])))[0];
252
253
  if (clash === undefined)
253
254
  continue;
254
- this.ctx.warn("position_double_booked", `${names.get(key) ?? key} coaches ${clash[0].a.offering.name} and ${clash[1].a.offering.name} at the same time on ${date}`, key);
255
+ const hours = extraCoachMinutes(list) / 60;
256
+ const sum = result.get(key) ?? { hours: 0, dates: 0 };
257
+ result.set(key, { hours: sum.hours + hours, dates: sum.dates + 1 });
258
+ this.ctx.warn("position_double_booked", `${names.get(key) ?? key} coaches ${clash[0].a.offering.name} and ${clash[1].a.offering.name} at the same time on ${date}` +
259
+ (hours > 0 ? `, so another coach is needed for ${round2(hours)} hours` : ""), key);
255
260
  }
261
+ return result;
256
262
  }
257
263
  /**
258
264
  * One RIB's hourly and daily cost, averaged over the RIB resources by `quantity`, and the part of
@@ -299,13 +305,15 @@ export class Staffing {
299
305
  this.staffBlock(block, group);
300
306
  });
301
307
  }
302
- this.checkDoubleBooked(staffed);
308
+ const doubleBooked = this.checkDoubleBooked(staffed);
303
309
  const coaching = [];
304
310
  const resourcesByActivity = new Map();
305
311
  for (const r of ctx.data.plan_activity_resources) {
306
312
  resourcesByActivity.set(r.activity_id, [...(resourcesByActivity.get(r.activity_id) ?? []), r]);
307
313
  }
308
314
  const resources = new Map(ctx.data.plan_resources.map((r) => [r.key, r]));
315
+ const boatClasses = new Map(ctx.data.boat_classes.map((c) => [c.id, c]));
316
+ const salaried = new Map(ctx.data.plan_salaried_positions.map((p) => [p.key, p]));
309
317
  const ribRate = this.ribRate();
310
318
  for (const s of staffed) {
311
319
  const { a } = s;
@@ -313,7 +321,7 @@ export class Staffing {
313
321
  const key = offering.key;
314
322
  const paidHours = a.paid_minutes / 60;
315
323
  const waterHours = a.on_water_minutes / 60;
316
- const book = (type, amount, line, explanation, mtdcShare = 1) => {
324
+ const book = (type, amount, line, name, explanation, mtdcShare = 1) => {
317
325
  if (amount === 0)
318
326
  return;
319
327
  const account = ctx.costAccount(type);
@@ -326,6 +334,7 @@ export class Staffing {
326
334
  function: ctx.functionFor(account, null, "offering"),
327
335
  amount,
328
336
  line_key: `${line}:${key}`,
337
+ line_name: name,
329
338
  explanation,
330
339
  mtdc_share: mtdcShare,
331
340
  wages: type === "coach_wages",
@@ -343,8 +352,11 @@ export class Staffing {
343
352
  }
344
353
  const volunteer = s.level?.pay_type === "volunteer";
345
354
  const rate = volunteer ? 0 : (s.level?.hourly_rate_cents ?? 0);
346
- const wages = hourly * paidHours * rate;
347
- book("coach_wages", wages, "coach_wages", "Hourly coaches × paid hours × the coach level's rate");
355
+ // A salaried coach paid by the hour may have their own rate, even on a volunteer level.
356
+ const rateOf = (p) => salaried.get(p.key)?.hourly_coaching_rate_cents ?? rate;
357
+ const own = s.hourlyPositions.reduce((t, p) => t + p.coaches * (rateOf(p) - rate), 0);
358
+ const wages = (hourly * rate + own) * paidHours;
359
+ book("coach_wages", wages, "coach_wages", "Coach wages", "Hourly coaches × paid hours × each one's rate");
348
360
  for (const p of s.positions) {
349
361
  coaching.push({
350
362
  position_key: p.key,
@@ -356,7 +368,7 @@ export class Staffing {
356
368
  }
357
369
  for (const p of s.hourlyPositions) {
358
370
  const hours = p.coaches * paidHours;
359
- coaching.push({ position_key: p.key, offering_key: key, hours, pay: "hourly", wages: hours * rate });
371
+ coaching.push({ position_key: p.key, offering_key: key, hours, pay: "hourly", wages: hours * rateOf(p) });
360
372
  }
361
373
  if (s.ribs > 0 && waterHours > 0) {
362
374
  if (ribRate === undefined) {
@@ -365,7 +377,7 @@ export class Staffing {
365
377
  else {
366
378
  const cost = s.ribs * (waterHours * ribRate.hourly + ribRate.daily);
367
379
  const rental = s.ribs * (waterHours * ribRate.hourlyRental + ribRate.rental);
368
- book("ribs", cost, "ribs", "RIBs × on-water hours × the RIB rate, plus a day rate", cost > 0 ? 1 - rental / cost : 1);
380
+ book("ribs", cost, "ribs", "RIBs", "RIBs × on-water hours × the RIB rate, plus a day rate", cost > 0 ? 1 - rental / cost : 1);
369
381
  }
370
382
  }
371
383
  for (const [classId, boats] of s.boats) {
@@ -374,7 +386,7 @@ export class Staffing {
374
386
  const boatRate = ctx.data.plan_boat_rates.find((r) => r.boat_class_id === classId);
375
387
  if (boatRate === undefined)
376
388
  continue;
377
- book("boat_maintenance", boats * waterHours * boatRate.maintenance_per_boat_hour_cents, `boats:${classId}`, "Boats × on-water hours × the class's maintenance rate");
389
+ book("boat_maintenance", boats * waterHours * boatRate.maintenance_per_boat_hour_cents, `boats:${classId}`, `Boat maintenance: ${boatClasses.get(classId)?.name ?? "boats"}`, "Boats × on-water hours × the class's maintenance rate");
378
390
  }
379
391
  for (const use of resourcesByActivity.get(a.stored.id) ?? []) {
380
392
  const resource = resources.get(use.resource_key);
@@ -383,12 +395,13 @@ export class Staffing {
383
395
  continue;
384
396
  }
385
397
  const hours = use.hours ?? paidHours;
386
- book("resources", use.quantity * (hours * hourlyCost(resource) + (resource.cost_per_day_cents ?? 0)), `resource:${use.resource_key}`, `${resource.name}: quantity × hours × hourly cost, plus a day rate`, resource.excluded_from_mtdc ? 0 : 1);
398
+ book("resources", use.quantity * (hours * hourlyCost(resource) + (resource.cost_per_day_cents ?? 0)), `resource:${use.resource_key}`, resource.name, `${resource.name}: quantity × hours × hourly cost, plus a day rate`, resource.excluded_from_mtdc ? 0 : 1);
387
399
  }
388
400
  }
389
401
  this.bookTrips(staffed);
390
402
  return {
391
403
  coaching,
404
+ doubleBooked,
392
405
  activities: staffed.map((s) => ({
393
406
  offering_id: s.a.offering.id,
394
407
  offering_key: s.a.offering.key,
@@ -441,6 +454,7 @@ export class Staffing {
441
454
  function: ctx.functionFor(account, null, "offering"),
442
455
  amount,
443
456
  line_key: `trip:${trip.key}`,
457
+ line_name: trip.name,
444
458
  explanation: `${trip.name}: ${round2(coaches)} coaches, ${trip.nights ?? 0} nights, ${trip.miles ?? 0} miles`,
445
459
  });
446
460
  }
@@ -456,6 +470,39 @@ function overlaps(x, y) {
456
470
  return true;
457
471
  return xs < ye && ys < xe;
458
472
  }
473
+ /**
474
+ * Minutes another coach would cover among one position's activities on one date: at each moment
475
+ * it's on n groups, n − 1. A block group staffed together is one. Untimed activities add nothing.
476
+ */
477
+ function extraCoachMinutes(list) {
478
+ const spans = new Map();
479
+ list.forEach((s, i) => {
480
+ if (s.a.start_minutes === null || s.a.end_minutes === null)
481
+ return;
482
+ const id = s.group ?? `own|${i}`;
483
+ const span = spans.get(id);
484
+ spans.set(id, {
485
+ start: Math.min(span?.start ?? Infinity, s.a.start_minutes),
486
+ end: Math.max(span?.end ?? -Infinity, s.a.end_minutes),
487
+ });
488
+ });
489
+ const edges = [...spans.values()]
490
+ .flatMap(({ start, end }) => [
491
+ { at: start, step: 1 },
492
+ { at: end, step: -1 },
493
+ ])
494
+ .sort((x, y) => x.at - y.at || x.step - y.step);
495
+ let on = 0;
496
+ let last = 0;
497
+ let minutes = 0;
498
+ for (const { at, step } of edges) {
499
+ if (on > 1)
500
+ minutes += (on - 1) * (at - last);
501
+ on += step;
502
+ last = at;
503
+ }
504
+ return minutes;
505
+ }
459
506
  function hourlyCost(r) {
460
507
  return (r.cost_per_hour_cents ?? 0) + (r.fuel_per_hour_cents ?? 0) + (r.maintenance_per_hour_cents ?? 0);
461
508
  }
@@ -83,6 +83,8 @@ export interface Plan {
83
83
  export interface PlanVersion {
84
84
  id: Id;
85
85
  plan_id: Id;
86
+ /** A saved area plan's: the Operations Plan version it was saved with. */
87
+ operations_version_id?: Id | null;
86
88
  }
87
89
  export type ActivityKind = "practice" | "regatta" | "clinic" | "travel";
88
90
  export interface PlanOffering extends Versioned {
@@ -196,6 +198,8 @@ export interface PlanSalariedPosition extends Versioned {
196
198
  /** The pool that takes the salary its hours don't charge to programs. */
197
199
  remainder_pool_id: Id | null;
198
200
  form_990_line: string | null;
201
+ /** What it's paid an hour to coach an offering that pays it by the hour, instead of the offering's level. */
202
+ hourly_coaching_rate_cents?: number | null;
199
203
  }
200
204
  /** Hours charged to a product, or to a program area and shared among its offerings: one of the two. */
201
205
  export interface PlanSalariedBooking extends Versioned {
@@ -313,6 +317,8 @@ export interface CostLine {
313
317
  /** Always positive: the account's type says whether it's revenue or expense. */
314
318
  amount_cents: number;
315
319
  line_key: string;
320
+ /** What the line is, in plain words, such as "G&A share: Payroll tax". */
321
+ line_name: string;
316
322
  explanation: string;
317
323
  }
318
324
  /** Revenue, expenses and net, in cents. */
@@ -393,6 +399,10 @@ export interface PositionSummary {
393
399
  total_pay_cents: number;
394
400
  /** The year's payroll tax on its total pay. */
395
401
  payroll_tax_cents: number;
402
+ /** Hours another coach would need while it's on two or more groups at once: n − 1 for n groups. */
403
+ double_booked_hours: number;
404
+ /** Dates it's on two or more groups at once. */
405
+ double_booked_dates: number;
396
406
  }
397
407
  export interface EngineOutput {
398
408
  lines: CostLine[];
@@ -67,6 +67,8 @@ export interface Schema {
67
67
  resource_kinds: ResourceKinds[];
68
68
  sync_runs: SyncRuns[];
69
69
  sync_tasks: SyncTasks[];
70
+ version_set_members: VersionSetMembers[];
71
+ version_sets: VersionSets[];
70
72
  }
71
73
  /** The chart of accounts that plans, results, and actuals book to. */
72
74
  export interface Accounts {
@@ -685,8 +687,8 @@ export interface PlanResources {
685
687
  /** Each cost and revenue line in a saved version's P&L. Worked out when the version was saved, and never changed. */
686
688
  export interface PlanResultLines {
687
689
  id: string;
688
- /** The saved version these results are for. */
689
- version_id: string | PlanVersions;
690
+ /** The saved version these results are for, if they aren't a club plan's. */
691
+ version_id: string | PlanVersions | null;
690
692
  cost_center_type: "offering" | "product" | "program_area" | "pool" | "org";
691
693
  cost_center_key: string | null;
692
694
  cost_center_name: string;
@@ -694,15 +696,21 @@ export interface PlanResultLines {
694
696
  function: "program" | "management" | "fundraising" | null;
695
697
  /** Always positive: the account says whether it's revenue or a cost. In cents: 12500 is $125.00. */
696
698
  amount_cents: number;
697
- /** What the line is, as the P&L names it. */
699
+ /** The engine's key for the line. */
698
700
  line: string;
699
701
  explanation: string | null;
702
+ /** The club plan these results are for, if they aren't a version's. */
703
+ version_set_id: string | VersionSets | null;
704
+ /** In a club plan's results, the plan that holds what it's charged to. */
705
+ version_set_member_id: string | VersionSetMembers | null;
706
+ /** What the line is, in plain words, such as G&A share: Payroll tax. */
707
+ line_name: string | null;
700
708
  }
701
709
  /** A saved version's P&L for each offering, product, and program area. Worked out when the version was saved, and never changed. */
702
710
  export interface PlanResultPnl {
703
711
  id: string;
704
- /** The saved version these results are for. */
705
- version_id: string | PlanVersions;
712
+ /** The saved version these results are for, if they aren't a club plan's. */
713
+ version_id: string | PlanVersions | null;
706
714
  level: "offering" | "product" | "program_area";
707
715
  subject_key: string;
708
716
  name: string;
@@ -717,12 +725,16 @@ export interface PlanResultPnl {
717
725
  enrollment: number;
718
726
  days: number;
719
727
  sailor_days: number;
728
+ /** The club plan these results are for, if they aren't a version's. */
729
+ version_set_id: string | VersionSets | null;
730
+ /** In a club plan's results, the plan that holds what it's charged to. */
731
+ version_set_member_id: string | VersionSetMembers | null;
720
732
  }
721
733
  /** Each salaried position's pay for the year, in a saved version. Worked out when the version was saved, and never changed. */
722
734
  export interface PlanResultPositions {
723
735
  id: string;
724
- /** The saved version these results are for. */
725
- version_id: string | PlanVersions;
736
+ /** The saved version these results are for, if they aren't a club plan's. */
737
+ version_id: string | PlanVersions | null;
726
738
  position_key: string;
727
739
  name: string;
728
740
  /** In cents: 12500 is $125.00. */
@@ -742,6 +754,12 @@ export interface PlanResultPositions {
742
754
  total_pay_cents: number;
743
755
  /** The year's payroll tax on the total pay. In cents: 12500 is $125.00. */
744
756
  payroll_tax_cents: number;
757
+ /** The club plan these results are for, if they aren't a version's. */
758
+ version_set_id: string | VersionSets | null;
759
+ /** Hours another coach would cover while this position coaches two or more groups at once: for three groups at once, two coaches. */
760
+ double_booked_hours: string | null;
761
+ /** Dates this position coaches two or more groups at once. */
762
+ double_booked_dates: number | null;
745
763
  }
746
764
  /** The other saved versions a version's results were worked out with. Worked out when the version was saved, and never changed. */
747
765
  export interface PlanResultVersions {
@@ -754,18 +772,20 @@ export interface PlanResultVersions {
754
772
  /** What the P&L warned about when a version was saved. Worked out when the version was saved, and never changed. */
755
773
  export interface PlanResultWarnings {
756
774
  id: string;
757
- /** The saved version these results are for. */
758
- version_id: string | PlanVersions;
775
+ /** The saved version these results are for, if they aren't a club plan's. */
776
+ version_id: string | PlanVersions | null;
759
777
  /** The kind of warning. */
760
778
  code: string;
761
779
  message: string;
762
780
  subject_key: string | null;
781
+ /** The club plan these results are for, if they aren't a version's. */
782
+ version_set_id: string | VersionSets | null;
763
783
  }
764
784
  /** A saved version's club-wide totals. Worked out when the version was saved, and never changed. */
765
785
  export interface PlanResults {
766
786
  id: string;
767
- /** The saved version these results are for. */
768
- version_id: string | PlanVersions;
787
+ /** The saved version these results are for, if they aren't a club plan's. */
788
+ version_id: string | PlanVersions | null;
769
789
  calculated_at: string;
770
790
  /** In cents: 12500 is $125.00. */
771
791
  operating_revenue_cents: number;
@@ -783,6 +803,8 @@ export interface PlanResults {
783
803
  capital_cents: number;
784
804
  /** Operating net, plus fundraising net, less capital. In cents: 12500 is $125.00. */
785
805
  total_cents: number;
806
+ /** The club plan these results are for, if they aren't a version's. */
807
+ version_set_id: string | VersionSets | null;
786
808
  }
787
809
  /** A charge set as a share of each offering's revenue, such as scholarships or card fees. Operations Plan only. */
788
810
  export interface PlanRevenueCharges {
@@ -835,6 +857,8 @@ export interface PlanSalariedPositions {
835
857
  /** The pool that takes the salary its hours don't charge to programs. */
836
858
  remainder_pool_id: string | PlanPools | null;
837
859
  form_990_line: string | null;
860
+ /** What this position is paid an hour to coach an offering that pays its salaried coach by the hour, instead of the offering's coach level. Leave empty to use the level. In cents: 12500 is $125.00. */
861
+ hourly_coaching_rate_cents: number | null;
838
862
  }
839
863
  /** A level hourly staff are paid at, such as head coach. */
840
864
  export interface PlanStaffLevels {
@@ -960,6 +984,8 @@ export interface Programs {
960
984
  name: string;
961
985
  /** The product this program belongs to. Its kind must be Program. */
962
986
  product_id: string | Products;
987
+ /** A team, such as a race team. Families on a team see everyone on every team. */
988
+ team: boolean;
963
989
  /** Who has a role in this program. */
964
990
  role_assignments: (string | ProgramRoleAssignments)[];
965
991
  /** The program's Google Group, if it has one. Most programs don't. */
@@ -1024,3 +1050,33 @@ export interface SyncTasks {
1024
1050
  /** Set once the task has failed too many times. It keeps retrying; this flags it for staff. */
1025
1051
  needs_attention: boolean;
1026
1052
  }
1053
+ /** One plan's version in a club plan. Set when the club plan is saved, and never changed. */
1054
+ export interface VersionSetMembers {
1055
+ id: string;
1056
+ version_set_id: string | VersionSets;
1057
+ plan_id: string | Plans;
1058
+ /** The plan's active version when the club plan was saved. */
1059
+ version_id: string | PlanVersions;
1060
+ }
1061
+ /** The club's plan for a year, saved: the active Operations Plan version and each program area's, with the P&L they make together. Saved when it's added, and never changed. */
1062
+ export interface VersionSets {
1063
+ id: string;
1064
+ /** The organization the club plan is for. */
1065
+ organization_id: string | Organizations;
1066
+ /** The year the club plan is for. */
1067
+ fiscal_year: number;
1068
+ /** Such as 2027 budget as adopted. */
1069
+ name: string;
1070
+ /** Why it was saved. */
1071
+ message: string | null;
1072
+ /** Set when the club plan is saved. */
1073
+ saved_by: string | null;
1074
+ /** Set when the club plan is saved. */
1075
+ saved_at: string | null;
1076
+ /** The version of the planning engine that calculated its numbers. */
1077
+ platform_version: string | null;
1078
+ /** The active version of each plan when the club plan was saved. */
1079
+ members: (string | VersionSetMembers)[];
1080
+ /** What the club's P&L showed when it was saved, worked out a moment after. A club plan with none means working them out failed, and an admin can fill them in. */
1081
+ results: (string | PlanResults)[];
1082
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyc-seattle/platform",
3
- "version": "1.22.1",
3
+ "version": "1.24.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",