@voyant-travel/mice 0.4.0 → 0.5.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/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export type { MiceAdminRoutes } from "./routes.js";
26
26
  export { miceAdminRoutes } from "./routes.js";
27
27
  export * from "./schema.js";
28
28
  export * from "./service.js";
29
+ export * from "./service-commercials.js";
29
30
  export * from "./service-delegates.js";
30
31
  export * from "./service-rfp.js";
31
32
  export * from "./service-rooming.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAI5D;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,kBAK7B,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,kBAK7B,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,kBAK9B,CAAA;AAED,eAAO,MAAM,yBAAyB,EAAE,kBAKvC,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,kBAKzB,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,kBAKzB,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;CAOxB,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,MAMxB,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,UAG5B,CAAA;AAED,eAAO,MAAM,eAAe,uBAA4B,CAAA;AAExD,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA;AACpC,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,2BAA2B,CAAA;AACzC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAI5D;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,kBAK7B,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,kBAK7B,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,kBAK9B,CAAA;AAED,eAAO,MAAM,yBAAyB,EAAE,kBAKvC,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,kBAKzB,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,kBAKzB,CAAA;AAED,eAAO,MAAM,YAAY;;;;;;;CAOxB,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,MAMxB,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,UAG5B,CAAA;AAED,eAAO,MAAM,eAAe,uBAA4B,CAAA;AAExD,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,0BAA0B,CAAA;AACxC,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA;AACpC,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,2BAA2B,CAAA;AACzC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA"}
package/dist/index.js CHANGED
@@ -63,6 +63,7 @@ export const miceHonoModules = [miceHonoModule];
63
63
  export { miceAdminRoutes } from "./routes.js";
64
64
  export * from "./schema.js";
65
65
  export * from "./service.js";
66
+ export * from "./service-commercials.js";
66
67
  export * from "./service-delegates.js";
67
68
  export * from "./service-rfp.js";
68
69
  export * from "./service-rooming.js";
package/dist/routes.d.ts CHANGED
@@ -122,6 +122,53 @@ export declare const miceAdminRoutes: import("hono/hono-base").HonoBase<Env, {
122
122
  status: import("hono/utils/http-status").ContentfulStatusCode;
123
123
  };
124
124
  };
125
+ } & {
126
+ "/programs/:id/cost-sheet": {
127
+ $get: {
128
+ input: {
129
+ param: {
130
+ id: string;
131
+ };
132
+ };
133
+ output: {
134
+ error: string;
135
+ };
136
+ outputFormat: "json";
137
+ status: 404;
138
+ } | {
139
+ input: {
140
+ param: {
141
+ id: string;
142
+ };
143
+ };
144
+ output: {
145
+ data: {
146
+ programId: string;
147
+ mixedCurrency: boolean;
148
+ byCurrency: {
149
+ currency: string;
150
+ roomBlocks: {
151
+ contractedCostCents: number;
152
+ pickedCostCents: number;
153
+ pickedSellCents: number;
154
+ };
155
+ spaceBlocks: {
156
+ contractedCostCents: number;
157
+ pickedCostCents: number;
158
+ pickedSellCents: number;
159
+ };
160
+ sessionInclusionsCostCents: number;
161
+ costCents: number;
162
+ sellCents: number;
163
+ marginCents: number;
164
+ marginPct: number | null;
165
+ }[];
166
+ };
167
+ };
168
+ outputFormat: "json";
169
+ status: import("hono/utils/http-status").ContentfulStatusCode;
170
+ };
171
+ };
125
172
  } & {
126
173
  "/programs/:id": {
127
174
  $patch: {
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AA0DjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA0MxB,CAAA;AAEJ,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AA2DjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAiNxB,CAAA;AAEJ,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAA"}
package/dist/routes.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { parseJsonBody, parseQuery } from "@voyant-travel/hono";
6
6
  import { Hono } from "hono";
7
7
  import { createProgram, getProgram, listPrograms, updateProgram } from "./service.js";
8
+ import { commercialsService } from "./service-commercials.js";
8
9
  import { createDelegate, enrollDelegate, getDelegate, listDelegates, updateDelegate, } from "./service-delegates.js";
9
10
  import { rfpService } from "./service-rfp.js";
10
11
  import { createRoomingAssignment, getRoomingAssignment, listRoomingAssignments, setRoomingDelegates, updateRoomingAssignment, } from "./service-rooming.js";
@@ -28,6 +29,14 @@ export const miceAdminRoutes = new Hono()
28
29
  if (!program)
29
30
  return c.json({ error: "Program not found" }, 404);
30
31
  return c.json({ data: program });
32
+ })
33
+ // Consolidated commercials — program cost sheet / P&L (Phase 5).
34
+ .get("/programs/:id/cost-sheet", async (c) => {
35
+ const id = c.req.param("id");
36
+ const program = await getProgram(c.get("db"), id);
37
+ if (!program)
38
+ return c.json({ error: "Program not found" }, 404);
39
+ return c.json({ data: await commercialsService.getProgramCostSheet(c.get("db"), id) });
31
40
  })
32
41
  .patch("/programs/:id", async (c) => {
33
42
  const body = await parseJsonBody(c, updateProgramSchema);
@@ -0,0 +1,42 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ /**
3
+ * Consolidated commercials — a program P&L / cost sheet computed on the fly from
4
+ * the program's committed inventory (room blocks, space blocks, session
5
+ * inclusions). No new spine tables (RFC §7 Phase 5): a read model over what
6
+ * Phases 1–4 persist.
7
+ *
8
+ * Each source carries its own currency, so totals are GROUPED BY CURRENCY — no
9
+ * FX conversion is assumed. `mixedCurrency` flags programs spanning more than
10
+ * one. Amounts whose source row has no currency fall back to the program
11
+ * currency, or `UNSPECIFIED` when the program has none either.
12
+ *
13
+ * Per category: contracted exposure (held × net) and the actualized figures on
14
+ * picked-up inventory (cost + sell); margin is on the actualized numbers.
15
+ */
16
+ export interface CostSheetCategory {
17
+ contractedCostCents: number;
18
+ pickedCostCents: number;
19
+ pickedSellCents: number;
20
+ }
21
+ export interface CostSheetCurrencyTotals {
22
+ currency: string;
23
+ roomBlocks: CostSheetCategory;
24
+ spaceBlocks: CostSheetCategory;
25
+ sessionInclusionsCostCents: number;
26
+ costCents: number;
27
+ sellCents: number;
28
+ marginCents: number;
29
+ /** Margin as a % of sell, or null when there is no realized revenue yet. */
30
+ marginPct: number | null;
31
+ }
32
+ export interface ProgramCostSheet {
33
+ programId: string;
34
+ mixedCurrency: boolean;
35
+ /** One entry per currency present, sorted by currency code. */
36
+ byCurrency: CostSheetCurrencyTotals[];
37
+ }
38
+ export declare function getProgramCostSheet(db: PostgresJsDatabase, programId: string): Promise<ProgramCostSheet>;
39
+ export declare const commercialsService: {
40
+ getProgramCostSheet: typeof getProgramCostSheet;
41
+ };
42
+ //# sourceMappingURL=service-commercials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-commercials.d.ts","sourceRoot":"","sources":["../src/service-commercials.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAKjE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,iBAAiB;IAChC,mBAAmB,EAAE,MAAM,CAAA;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,iBAAiB,CAAA;IAC7B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,0BAA0B,EAAE,MAAM,CAAA;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;IACtB,+DAA+D;IAC/D,UAAU,EAAE,uBAAuB,EAAE,CAAA;CACtC;AAgBD,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,gBAAgB,CAAC,CAoI3B;AAED,eAAO,MAAM,kBAAkB;;CAA0B,CAAA"}
@@ -0,0 +1,125 @@
1
+ import { roomBlockNights, roomBlocks } from "@voyant-travel/accommodations/schema";
2
+ import { spaceBlockSlots, spaceBlocks } from "@voyant-travel/operations";
3
+ import { eq, inArray } from "drizzle-orm";
4
+ import { programs } from "./schema.js";
5
+ import { programSessions, sessionInclusions } from "./schema-sessions.js";
6
+ const UNSPECIFIED = "UNSPECIFIED";
7
+ const emptyCategory = () => ({
8
+ contractedCostCents: 0,
9
+ pickedCostCents: 0,
10
+ pickedSellCents: 0,
11
+ });
12
+ export async function getProgramCostSheet(db, programId) {
13
+ const [program] = await db
14
+ .select({ currency: programs.currency })
15
+ .from(programs)
16
+ .where(eq(programs.id, programId))
17
+ .limit(1);
18
+ const fallbackCurrency = program?.currency ?? UNSPECIFIED;
19
+ const buckets = new Map();
20
+ const bucketFor = (currency) => {
21
+ let b = buckets.get(currency);
22
+ if (!b) {
23
+ b = {
24
+ roomBlocks: emptyCategory(),
25
+ spaceBlocks: emptyCategory(),
26
+ sessionInclusionsCostCents: 0,
27
+ };
28
+ buckets.set(currency, b);
29
+ }
30
+ return b;
31
+ };
32
+ // ── Room blocks (accommodations) — currency is required on the block. ──
33
+ const rBlocks = await db
34
+ .select({
35
+ id: roomBlocks.id,
36
+ net: roomBlocks.netRateCents,
37
+ sell: roomBlocks.sellRateCents,
38
+ currency: roomBlocks.currency,
39
+ })
40
+ .from(roomBlocks)
41
+ .where(eq(roomBlocks.programId, programId));
42
+ if (rBlocks.length) {
43
+ const byId = new Map(rBlocks.map((b) => [b.id, b]));
44
+ const nights = await db
45
+ .select()
46
+ .from(roomBlockNights)
47
+ .where(inArray(roomBlockNights.blockId, rBlocks.map((b) => b.id)));
48
+ for (const n of nights) {
49
+ const block = byId.get(n.blockId);
50
+ if (!block)
51
+ continue;
52
+ const cat = bucketFor(block.currency ?? fallbackCurrency).roomBlocks;
53
+ const net = n.netRateCentsOverride ?? block.net ?? 0;
54
+ const sell = n.sellRateCentsOverride ?? block.sell ?? 0;
55
+ cat.contractedCostCents += n.roomsHeld * net;
56
+ cat.pickedCostCents += n.roomsPickedUp * net;
57
+ cat.pickedSellCents += n.roomsPickedUp * sell;
58
+ }
59
+ }
60
+ // ── Space blocks (operations) — currency nullable. ──
61
+ const sBlocks = await db
62
+ .select({
63
+ id: spaceBlocks.id,
64
+ net: spaceBlocks.netRateCents,
65
+ sell: spaceBlocks.sellRateCents,
66
+ currency: spaceBlocks.currency,
67
+ })
68
+ .from(spaceBlocks)
69
+ .where(eq(spaceBlocks.programId, programId));
70
+ if (sBlocks.length) {
71
+ const byId = new Map(sBlocks.map((b) => [b.id, b]));
72
+ const slots = await db
73
+ .select()
74
+ .from(spaceBlockSlots)
75
+ .where(inArray(spaceBlockSlots.blockId, sBlocks.map((b) => b.id)));
76
+ for (const s of slots) {
77
+ const block = byId.get(s.blockId);
78
+ if (!block)
79
+ continue;
80
+ const cat = bucketFor(block.currency ?? fallbackCurrency).spaceBlocks;
81
+ const net = s.netRateCentsOverride ?? block.net ?? 0;
82
+ const sell = s.sellRateCentsOverride ?? block.sell ?? 0;
83
+ cat.contractedCostCents += s.unitsHeld * net;
84
+ cat.pickedCostCents += s.unitsPickedUp * net;
85
+ cat.pickedSellCents += s.unitsPickedUp * sell;
86
+ }
87
+ }
88
+ // ── Session inclusions (mice) — currency nullable, cost only. ──
89
+ const sessions = await db
90
+ .select({ id: programSessions.id })
91
+ .from(programSessions)
92
+ .where(eq(programSessions.programId, programId));
93
+ if (sessions.length) {
94
+ const inclusions = await db
95
+ .select({
96
+ qty: sessionInclusions.quantity,
97
+ cost: sessionInclusions.costAmountCents,
98
+ currency: sessionInclusions.currency,
99
+ })
100
+ .from(sessionInclusions)
101
+ .where(inArray(sessionInclusions.sessionId, sessions.map((s) => s.id)));
102
+ for (const i of inclusions) {
103
+ bucketFor(i.currency ?? fallbackCurrency).sessionInclusionsCostCents += i.qty * (i.cost ?? 0);
104
+ }
105
+ }
106
+ const byCurrency = [...buckets.entries()]
107
+ .sort(([a], [b]) => a.localeCompare(b))
108
+ .map(([currency, b]) => {
109
+ const costCents = b.roomBlocks.pickedCostCents + b.spaceBlocks.pickedCostCents + b.sessionInclusionsCostCents;
110
+ const sellCents = b.roomBlocks.pickedSellCents + b.spaceBlocks.pickedSellCents;
111
+ const marginCents = sellCents - costCents;
112
+ return {
113
+ currency,
114
+ roomBlocks: b.roomBlocks,
115
+ spaceBlocks: b.spaceBlocks,
116
+ sessionInclusionsCostCents: b.sessionInclusionsCostCents,
117
+ costCents,
118
+ sellCents,
119
+ marginCents,
120
+ marginPct: sellCents > 0 ? Math.round((marginCents / sellCents) * 1000) / 10 : null,
121
+ };
122
+ });
123
+ return { programId, mixedCurrency: byCurrency.length > 1, byCurrency };
124
+ }
125
+ export const commercialsService = { getProgramCostSheet };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/mice",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,9 +34,11 @@
34
34
  "drizzle-orm": "^0.45.2",
35
35
  "hono": "^4.12.10",
36
36
  "zod": "^4.3.6",
37
+ "@voyant-travel/accommodations": "^0.108.0",
38
+ "@voyant-travel/operations": "^0.5.0",
39
+ "@voyant-travel/hono": "^0.116.1",
37
40
  "@voyant-travel/core": "^0.111.0",
38
- "@voyant-travel/db": "^0.109.4",
39
- "@voyant-travel/hono": "^0.116.1"
41
+ "@voyant-travel/db": "^0.109.4"
40
42
  },
41
43
  "devDependencies": {
42
44
  "drizzle-kit": "^0.31.10",
@@ -58,7 +60,11 @@
58
60
  "directory": "packages/mice"
59
61
  },
60
62
  "voyant": {
61
- "schema": "./schema"
63
+ "schema": "./schema",
64
+ "requiresSchemas": [
65
+ "@voyant-travel/accommodations",
66
+ "@voyant-travel/operations"
67
+ ]
62
68
  },
63
69
  "scripts": {
64
70
  "typecheck": "tsc --noEmit",