@mulmoclaude/accounting-plugin 2.0.0 → 2.2.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 +37 -0
- package/dist/server/context.d.ts +45 -3
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/eventPublisher.d.ts +9 -4
- package/dist/server/eventPublisher.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/router.d.ts +27 -3
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/snapshotCache.d.ts +3 -3
- package/dist/server/snapshotCache.d.ts.map +1 -1
- package/dist/server.cjs +152 -82
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +151 -81
- package/dist/server.js.map +1 -1
- package/dist/shared/api.d.ts +12 -0
- package/dist/shared/api.d.ts.map +1 -1
- package/dist/shared/channels.d.ts +22 -2
- package/dist/shared/channels.d.ts.map +1 -1
- package/dist/{shared-BZAAF-I4.js → shared-BccLo0Ux.js} +39 -5
- package/dist/shared-BccLo0Ux.js.map +1 -0
- package/dist/{shared-BpFVwa6Y.cjs → shared-Dgt-i23T.cjs} +50 -4
- package/dist/shared-Dgt-i23T.cjs.map +1 -0
- package/dist/shared.cjs +3 -1
- package/dist/shared.js +2 -2
- package/dist/style.css +19 -10
- package/dist/vue/View.vue.d.ts +5 -0
- package/dist/vue/View.vue.d.ts.map +1 -1
- package/dist/vue/api.d.ts +132 -100
- package/dist/vue/api.d.ts.map +1 -1
- package/dist/vue/components/AccountsModal.vue.d.ts.map +1 -1
- package/dist/vue/components/BalanceSheet.vue.d.ts.map +1 -1
- package/dist/vue/components/BookSettings.vue.d.ts.map +1 -1
- package/dist/vue/components/JournalEntryForm.vue.d.ts.map +1 -1
- package/dist/vue/components/JournalList.vue.d.ts.map +1 -1
- package/dist/vue/components/Ledger.vue.d.ts.map +1 -1
- package/dist/vue/components/NewBookForm.vue.d.ts.map +1 -1
- package/dist/vue/components/OpeningBalancesForm.vue.d.ts.map +1 -1
- package/dist/vue/components/ProfitLoss.vue.d.ts.map +1 -1
- package/dist/vue/hostContext.d.ts +15 -0
- package/dist/vue/hostContext.d.ts.map +1 -1
- package/dist/vue/useAccountingChannel.d.ts +13 -3
- package/dist/vue/useAccountingChannel.d.ts.map +1 -1
- package/dist/vue.cjs +229 -99
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +230 -100
- package/dist/vue.js.map +1 -1
- package/package.json +5 -5
- package/dist/shared-BZAAF-I4.js.map +0 -1
- package/dist/shared-BpFVwa6Y.cjs.map +0 -1
package/dist/server.cjs
CHANGED
|
@@ -16,12 +16,12 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
20
|
value: mod,
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
const require_shared = require("./shared-
|
|
24
|
+
const require_shared = require("./shared-Dgt-i23T.cjs");
|
|
25
25
|
let express = require("express");
|
|
26
26
|
let node_crypto = require("node:crypto");
|
|
27
27
|
let node_fs = require("node:fs");
|
|
@@ -84,11 +84,41 @@ function configureAccountingServer(context) {
|
|
|
84
84
|
}
|
|
85
85
|
/** Default workspace root for io calls that don't pass one explicitly.
|
|
86
86
|
* Throws if the host never configured the server — a real wiring bug
|
|
87
|
-
* (unit tests always pass an explicit root, so they never hit this)
|
|
87
|
+
* (unit tests always pass an explicit root, so they never hit this) —
|
|
88
|
+
* and also under STRICT mode (`workspaceRoot: null`), where there is no
|
|
89
|
+
* ambient root to fall back to and a missing option is a bug rather
|
|
90
|
+
* than a default. */
|
|
88
91
|
function defaultWorkspaceRoot() {
|
|
89
92
|
if (!deps) throw new Error("@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests");
|
|
93
|
+
if (deps.workspaceRoot === null) throw new Error("@mulmoclaude/accounting-plugin: this host is configured with workspaceRoot: null (explicit-root mode), so every call must pass a workspaceRoot. A call reached the engine without one.");
|
|
90
94
|
return deps.workspaceRoot;
|
|
91
95
|
}
|
|
96
|
+
/** The opaque channel scope for a root, or `null` when the host declared
|
|
97
|
+
* none (single-root hosts, and a multi-root host's default root). Used
|
|
98
|
+
* by the event publisher; never contains a path.
|
|
99
|
+
*
|
|
100
|
+
* A missing root throws under explicit-root mode, exactly as
|
|
101
|
+
* `defaultWorkspaceRoot()` does. Returning `null` there would be worse
|
|
102
|
+
* than the io-path bug it mirrors: the event would go out on the
|
|
103
|
+
* UNSCOPED channel name, which every project's default-scope
|
|
104
|
+
* subscriber receives. The publisher catches it and logs, so a dropped
|
|
105
|
+
* root costs one event and a warning rather than a silent cross-project
|
|
106
|
+
* notification. */
|
|
107
|
+
function channelScopeFor(workspaceRoot) {
|
|
108
|
+
if (!deps?.channelScopeForRoot) return null;
|
|
109
|
+
return deps.channelScopeForRoot(workspaceRoot ?? defaultWorkspaceRoot());
|
|
110
|
+
}
|
|
111
|
+
/** Whether the host declared project scoping at all.
|
|
112
|
+
*
|
|
113
|
+
* The difference matters where a card records the project it belongs
|
|
114
|
+
* to: on a scoped host, "no scope" is a REAL answer (the default root)
|
|
115
|
+
* and must be recorded as `null`, not left out — an omitted field means
|
|
116
|
+
* "ask the host what is active now", which is precisely the drift a
|
|
117
|
+
* pinned card exists to prevent. On an unscoped host the field is left
|
|
118
|
+
* out entirely and every payload stays byte-identical to today's. */
|
|
119
|
+
function isProjectScopedHost() {
|
|
120
|
+
return Boolean(deps?.channelScopeForRoot);
|
|
121
|
+
}
|
|
92
122
|
var consoleLogger = {
|
|
93
123
|
error: (namespace, msg, data) => console.error(`[${namespace}] ${msg}`, data ?? ""),
|
|
94
124
|
warn: (namespace, msg, data) => console.warn(`[${namespace}] ${msg}`, data ?? ""),
|
|
@@ -1165,9 +1195,16 @@ var pubsub = null;
|
|
|
1165
1195
|
function initAccountingEventPublisher(instance) {
|
|
1166
1196
|
pubsub = instance;
|
|
1167
1197
|
}
|
|
1168
|
-
|
|
1198
|
+
/** `channelFor` is called INSIDE the guard, not before it: resolving the
|
|
1199
|
+
* scope can throw (a write that reached the publisher with no root
|
|
1200
|
+
* under explicit-root mode), and a lost event with a warning beats
|
|
1201
|
+
* either crashing the write or publishing to the unscoped channel that
|
|
1202
|
+
* every project can hear. */
|
|
1203
|
+
function safePublish(channelFor, payload) {
|
|
1169
1204
|
if (!pubsub) return;
|
|
1205
|
+
let channel = "(unresolved)";
|
|
1170
1206
|
try {
|
|
1207
|
+
channel = channelFor();
|
|
1171
1208
|
pubsub.publish(channel, payload);
|
|
1172
1209
|
} catch (err) {
|
|
1173
1210
|
log.warn("accounting", "publish failed; subscribers will miss this event", {
|
|
@@ -1178,15 +1215,20 @@ function safePublish(channel, payload) {
|
|
|
1178
1215
|
}
|
|
1179
1216
|
/** Per-book change notification. `period` should be the entry's
|
|
1180
1217
|
* YYYY-MM bucket (or the earliest invalidated month for snapshot
|
|
1181
|
-
* events).
|
|
1182
|
-
|
|
1183
|
-
|
|
1218
|
+
* events).
|
|
1219
|
+
*
|
|
1220
|
+
* `workspaceRoot` is the root the write happened under; the channel
|
|
1221
|
+
* name is namespaced by the host's opaque scope for it (see
|
|
1222
|
+
* `channelScopeFor`). Omit it — as a single-root host's service calls
|
|
1223
|
+
* do — and the name is what it has always been. */
|
|
1224
|
+
function publishBookChange(bookId, payload, workspaceRoot) {
|
|
1225
|
+
safePublish(() => require_shared.bookChannel(bookId, channelScopeFor(workspaceRoot)), payload);
|
|
1184
1226
|
}
|
|
1185
1227
|
/** Fired when the *list* of books changes (createBook, deleteBook).
|
|
1186
1228
|
* Payload is intentionally empty — subscribers refetch from
|
|
1187
|
-
* /api/accounting. */
|
|
1188
|
-
function publishBooksChanged() {
|
|
1189
|
-
safePublish(require_shared.
|
|
1229
|
+
* /api/accounting. Scoped by root like `publishBookChange`. */
|
|
1230
|
+
function publishBooksChanged(workspaceRoot) {
|
|
1231
|
+
safePublish(() => require_shared.booksChannel(channelScopeFor(workspaceRoot)), {});
|
|
1190
1232
|
}
|
|
1191
1233
|
//#endregion
|
|
1192
1234
|
//#region src/server/snapshotCache.ts
|
|
@@ -1255,18 +1297,39 @@ async function rebuildAllSnapshots(bookId, workspaceRoot) {
|
|
|
1255
1297
|
return { rebuilt: periods };
|
|
1256
1298
|
}
|
|
1257
1299
|
var rebuildQueues = /* @__PURE__ */ new Map();
|
|
1300
|
+
/** Canonical key for the root a rebuild belongs to. An absent root
|
|
1301
|
+
* resolves to the host's configured one, so a call that passes it
|
|
1302
|
+
* explicitly and one that relies on the default share a queue. Under
|
|
1303
|
+
* explicit-root mode there is no default and `defaultWorkspaceRoot()`
|
|
1304
|
+
* throws — deliberately loud: a `cancelRebuild` / `awaitRebuildIdle`
|
|
1305
|
+
* that quietly keyed a queue nobody scheduled would return while the
|
|
1306
|
+
* real rebuild is still writing, which is how `deleteBook` races a
|
|
1307
|
+
* `writeSnapshot` back into existence.
|
|
1308
|
+
*
|
|
1309
|
+
* Lexical resolution only — symlinks are deliberately NOT resolved,
|
|
1310
|
+
* matching `canonicalRoot` in `@mulmoclaude/core`'s collection engine,
|
|
1311
|
+
* so one project has one identity across both packages. A host that
|
|
1312
|
+
* hands the engine a real path in one call and a symlink alias in the
|
|
1313
|
+
* next would split the queue; the fix belongs in the host's own root
|
|
1314
|
+
* resolution, which is where the two packages agree it lives. */
|
|
1315
|
+
function rootKey(workspaceRoot) {
|
|
1316
|
+
return node_path.default.resolve(workspaceRoot ?? defaultWorkspaceRoot());
|
|
1317
|
+
}
|
|
1318
|
+
function queueKey(bookId, workspaceRoot) {
|
|
1319
|
+
return `${rootKey(workspaceRoot)}\u0000${bookId}`;
|
|
1320
|
+
}
|
|
1258
1321
|
function minPeriod(lhs, rhs) {
|
|
1259
1322
|
if (lhs === null) return rhs;
|
|
1260
1323
|
return lhs < rhs ? lhs : rhs;
|
|
1261
1324
|
}
|
|
1262
|
-
function isInvalidatedDuringRebuild(
|
|
1263
|
-
const queue = rebuildQueues.get(
|
|
1325
|
+
function isInvalidatedDuringRebuild(key, period) {
|
|
1326
|
+
const queue = rebuildQueues.get(key);
|
|
1264
1327
|
return queue !== void 0 && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;
|
|
1265
1328
|
}
|
|
1266
|
-
function isCancelled(
|
|
1267
|
-
return rebuildQueues.get(
|
|
1329
|
+
function isCancelled(key) {
|
|
1330
|
+
return rebuildQueues.get(key)?.cancelled === true;
|
|
1268
1331
|
}
|
|
1269
|
-
async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1332
|
+
async function runRebuild(key, bookId, fromPeriod, workspaceRoot) {
|
|
1270
1333
|
const startedAt = Date.now();
|
|
1271
1334
|
log.info("accounting", "snapshot rebuild started", {
|
|
1272
1335
|
bookId,
|
|
@@ -1275,25 +1338,25 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1275
1338
|
publishBookChange(bookId, {
|
|
1276
1339
|
kind: require_shared.BOOK_EVENT_KINDS.snapshotsRebuilding,
|
|
1277
1340
|
period: fromPeriod
|
|
1278
|
-
});
|
|
1341
|
+
}, workspaceRoot);
|
|
1279
1342
|
const targets = (await listJournalPeriods(bookId, workspaceRoot)).filter((monthKey) => monthKey >= fromPeriod);
|
|
1280
1343
|
let written = 0;
|
|
1281
1344
|
for (const monthKey of targets) {
|
|
1282
|
-
if (isCancelled(
|
|
1283
|
-
if (isInvalidatedDuringRebuild(
|
|
1345
|
+
if (isCancelled(key)) break;
|
|
1346
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) break;
|
|
1284
1347
|
const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);
|
|
1285
|
-
if (isCancelled(
|
|
1286
|
-
if (isInvalidatedDuringRebuild(
|
|
1348
|
+
if (isCancelled(key)) break;
|
|
1349
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) break;
|
|
1287
1350
|
await writeSnapshot(bookId, {
|
|
1288
1351
|
period: monthKey,
|
|
1289
1352
|
balances,
|
|
1290
1353
|
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1291
1354
|
}, workspaceRoot);
|
|
1292
|
-
if (isCancelled(
|
|
1355
|
+
if (isCancelled(key)) {
|
|
1293
1356
|
await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
|
|
1294
1357
|
break;
|
|
1295
1358
|
}
|
|
1296
|
-
if (isInvalidatedDuringRebuild(
|
|
1359
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) {
|
|
1297
1360
|
await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
|
|
1298
1361
|
break;
|
|
1299
1362
|
}
|
|
@@ -1301,7 +1364,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1301
1364
|
publishBookChange(bookId, {
|
|
1302
1365
|
kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady,
|
|
1303
1366
|
period: monthKey
|
|
1304
|
-
});
|
|
1367
|
+
}, workspaceRoot);
|
|
1305
1368
|
}
|
|
1306
1369
|
log.info("accounting", "snapshot rebuild done", {
|
|
1307
1370
|
bookId,
|
|
@@ -1309,7 +1372,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1309
1372
|
durationMs: Date.now() - startedAt
|
|
1310
1373
|
});
|
|
1311
1374
|
}
|
|
1312
|
-
function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1375
|
+
function startRebuild(key, bookId, fromPeriod, workspaceRoot) {
|
|
1313
1376
|
const entry = {
|
|
1314
1377
|
running: Promise.resolve(),
|
|
1315
1378
|
pendingFromPeriod: null,
|
|
@@ -1318,27 +1381,27 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1318
1381
|
runningFromPeriod: fromPeriod,
|
|
1319
1382
|
cancelled: false
|
|
1320
1383
|
};
|
|
1321
|
-
entry.running = runRebuild(bookId, fromPeriod, workspaceRoot).catch((err) => {
|
|
1384
|
+
entry.running = runRebuild(key, bookId, fromPeriod, workspaceRoot).catch((err) => {
|
|
1322
1385
|
log.error("accounting", "snapshot rebuild failed", {
|
|
1323
1386
|
bookId,
|
|
1324
1387
|
fromPeriod,
|
|
1325
1388
|
error: require_shared.errorMessage(err)
|
|
1326
1389
|
});
|
|
1327
1390
|
}).then(() => {
|
|
1328
|
-
const current = rebuildQueues.get(
|
|
1391
|
+
const current = rebuildQueues.get(key);
|
|
1329
1392
|
if (!current) return;
|
|
1330
1393
|
if (current.cancelled) {
|
|
1331
|
-
rebuildQueues.delete(
|
|
1394
|
+
rebuildQueues.delete(key);
|
|
1332
1395
|
return;
|
|
1333
1396
|
}
|
|
1334
1397
|
if (current.pendingFromPeriod !== null) {
|
|
1335
1398
|
const nextFrom = current.pendingFromPeriod;
|
|
1336
1399
|
const nextRoot = current.pendingWorkspaceRoot;
|
|
1337
1400
|
const carriedCount = current.coalescedWriteCount;
|
|
1338
|
-
const successor = startRebuild(bookId, nextFrom, nextRoot);
|
|
1401
|
+
const successor = startRebuild(key, bookId, nextFrom, nextRoot);
|
|
1339
1402
|
successor.coalescedWriteCount += carriedCount;
|
|
1340
|
-
rebuildQueues.set(
|
|
1341
|
-
} else rebuildQueues.delete(
|
|
1403
|
+
rebuildQueues.set(key, successor);
|
|
1404
|
+
} else rebuildQueues.delete(key);
|
|
1342
1405
|
});
|
|
1343
1406
|
return entry;
|
|
1344
1407
|
}
|
|
@@ -1347,9 +1410,10 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1347
1410
|
* follow-up rebuild that covers the minimum `fromPeriod` seen.
|
|
1348
1411
|
* Returns immediately — the rebuild runs on its own promise chain. */
|
|
1349
1412
|
function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1350
|
-
const
|
|
1413
|
+
const key = queueKey(bookId, workspaceRoot);
|
|
1414
|
+
const existing = rebuildQueues.get(key);
|
|
1351
1415
|
if (!existing) {
|
|
1352
|
-
rebuildQueues.set(
|
|
1416
|
+
rebuildQueues.set(key, startRebuild(key, bookId, fromPeriod, workspaceRoot));
|
|
1353
1417
|
return;
|
|
1354
1418
|
}
|
|
1355
1419
|
existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);
|
|
@@ -1360,9 +1424,10 @@ function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1360
1424
|
* `bookId`. Also called by `deleteBook` after `cancelRebuild` to
|
|
1361
1425
|
* ensure a previously running rebuild has fully stopped before the
|
|
1362
1426
|
* caller removes the book's directory on disk. */
|
|
1363
|
-
async function awaitRebuildIdle(bookId) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1427
|
+
async function awaitRebuildIdle(bookId, workspaceRoot) {
|
|
1428
|
+
const key = queueKey(bookId, workspaceRoot);
|
|
1429
|
+
while (rebuildQueues.has(key)) {
|
|
1430
|
+
const entry = rebuildQueues.get(key);
|
|
1366
1431
|
if (!entry) return;
|
|
1367
1432
|
await entry.running;
|
|
1368
1433
|
}
|
|
@@ -1372,8 +1437,8 @@ async function awaitRebuildIdle(bookId) {
|
|
|
1372
1437
|
* `removeBookDir` cannot race with a `writeSnapshot` that would
|
|
1373
1438
|
* re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`
|
|
1374
1439
|
* to wait for the in-flight rebuild to finish bailing. */
|
|
1375
|
-
function cancelRebuild(bookId) {
|
|
1376
|
-
const entry = rebuildQueues.get(bookId);
|
|
1440
|
+
function cancelRebuild(bookId, workspaceRoot) {
|
|
1441
|
+
const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));
|
|
1377
1442
|
if (!entry) return;
|
|
1378
1443
|
entry.cancelled = true;
|
|
1379
1444
|
entry.pendingFromPeriod = null;
|
|
@@ -1727,7 +1792,7 @@ async function createBook(input, workspaceRoot) {
|
|
|
1727
1792
|
await ensureBookDir(bookId, workspaceRoot);
|
|
1728
1793
|
await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);
|
|
1729
1794
|
await writeConfig({ books: [...config.books, book] }, workspaceRoot);
|
|
1730
|
-
publishBooksChanged();
|
|
1795
|
+
publishBooksChanged(workspaceRoot);
|
|
1731
1796
|
return { book };
|
|
1732
1797
|
}
|
|
1733
1798
|
async function updateBook(input, workspaceRoot) {
|
|
@@ -1744,7 +1809,7 @@ async function updateBook(input, workspaceRoot) {
|
|
|
1744
1809
|
};
|
|
1745
1810
|
if (country === "") delete next.country;
|
|
1746
1811
|
await writeConfig({ books: config.books.map((book) => book.id === input.bookId ? next : book) }, workspaceRoot);
|
|
1747
|
-
publishBooksChanged();
|
|
1812
|
+
publishBooksChanged(workspaceRoot);
|
|
1748
1813
|
return { book: next };
|
|
1749
1814
|
}
|
|
1750
1815
|
async function deleteBook(input, workspaceRoot) {
|
|
@@ -1752,11 +1817,11 @@ async function deleteBook(input, workspaceRoot) {
|
|
|
1752
1817
|
const config = await loadOrInitConfig(workspaceRoot);
|
|
1753
1818
|
const target = findBook(config, input.bookId);
|
|
1754
1819
|
if (!target) throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
|
|
1755
|
-
cancelRebuild(input.bookId);
|
|
1756
|
-
await awaitRebuildIdle(input.bookId);
|
|
1820
|
+
cancelRebuild(input.bookId, workspaceRoot);
|
|
1821
|
+
await awaitRebuildIdle(input.bookId, workspaceRoot);
|
|
1757
1822
|
await removeBookDir(input.bookId, workspaceRoot);
|
|
1758
1823
|
await writeConfig({ books: config.books.filter((book) => book.id !== input.bookId) }, workspaceRoot);
|
|
1759
|
-
publishBooksChanged();
|
|
1824
|
+
publishBooksChanged(workspaceRoot);
|
|
1760
1825
|
return {
|
|
1761
1826
|
deletedBookId: input.bookId,
|
|
1762
1827
|
deletedBookName: target.name
|
|
@@ -1806,7 +1871,7 @@ async function upsertAccount(input, workspaceRoot) {
|
|
|
1806
1871
|
scheduleRebuild(bookId, "0000-00", workspaceRoot);
|
|
1807
1872
|
await invalidateAllSnapshots(bookId, workspaceRoot);
|
|
1808
1873
|
}
|
|
1809
|
-
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts });
|
|
1874
|
+
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.accounts }, workspaceRoot);
|
|
1810
1875
|
return {
|
|
1811
1876
|
bookId,
|
|
1812
1877
|
account,
|
|
@@ -1855,7 +1920,7 @@ async function addEntries(input, workspaceRoot) {
|
|
|
1855
1920
|
publishBookChange(bookId, {
|
|
1856
1921
|
kind: require_shared.BOOK_EVENT_KINDS.journal,
|
|
1857
1922
|
period: earliestPeriod
|
|
1858
|
-
});
|
|
1923
|
+
}, workspaceRoot);
|
|
1859
1924
|
return {
|
|
1860
1925
|
bookId,
|
|
1861
1926
|
entries: built
|
|
@@ -1884,7 +1949,7 @@ async function voidEntry(input, workspaceRoot) {
|
|
|
1884
1949
|
publishBookChange(bookId, {
|
|
1885
1950
|
kind: require_shared.BOOK_EVENT_KINDS.journal,
|
|
1886
1951
|
period: fromPeriod
|
|
1887
|
-
});
|
|
1952
|
+
}, workspaceRoot);
|
|
1888
1953
|
return {
|
|
1889
1954
|
bookId,
|
|
1890
1955
|
reverseEntry: reverse,
|
|
@@ -1948,7 +2013,7 @@ async function setOpeningBalances(input, workspaceRoot) {
|
|
|
1948
2013
|
await appendJournal(bookId, opening, workspaceRoot);
|
|
1949
2014
|
scheduleRebuild(bookId, "0000-00", workspaceRoot);
|
|
1950
2015
|
await invalidateAllSnapshots(bookId, workspaceRoot);
|
|
1951
|
-
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening });
|
|
2016
|
+
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.opening }, workspaceRoot);
|
|
1952
2017
|
return {
|
|
1953
2018
|
bookId,
|
|
1954
2019
|
openingEntry: opening,
|
|
@@ -2046,9 +2111,10 @@ function validateTimeSeriesInput(input) {
|
|
|
2046
2111
|
async function loadTimeSeriesBookContext(requestedBookId, workspaceRoot) {
|
|
2047
2112
|
const config = await loadOrInitConfig(workspaceRoot);
|
|
2048
2113
|
const bookId = resolveBookId(config, requestedBookId);
|
|
2114
|
+
const book = findBook(config, bookId);
|
|
2049
2115
|
return {
|
|
2050
2116
|
bookId,
|
|
2051
|
-
fiscalYearEnd: require_shared.resolveFiscalYearEnd(
|
|
2117
|
+
fiscalYearEnd: require_shared.resolveFiscalYearEnd(book?.fiscalYearEnd),
|
|
2052
2118
|
accounts: await readAccounts(bookId, workspaceRoot)
|
|
2053
2119
|
};
|
|
2054
2120
|
}
|
|
@@ -2082,7 +2148,7 @@ async function getTimeSeriesReport(input, workspaceRoot) {
|
|
|
2082
2148
|
async function rebuildSnapshots(input, workspaceRoot) {
|
|
2083
2149
|
const bookId = resolveBookId(await loadOrInitConfig(workspaceRoot), input.bookId);
|
|
2084
2150
|
const result = await rebuildAllSnapshots(bookId, workspaceRoot);
|
|
2085
|
-
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady });
|
|
2151
|
+
publishBookChange(bookId, { kind: require_shared.BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);
|
|
2086
2152
|
return {
|
|
2087
2153
|
bookId,
|
|
2088
2154
|
rebuilt: result.rebuilt
|
|
@@ -2109,19 +2175,21 @@ function asyncHandler(namespace, fallbackMessage, handler) {
|
|
|
2109
2175
|
}
|
|
2110
2176
|
//#endregion
|
|
2111
2177
|
//#region src/server/router.ts
|
|
2112
|
-
async function handleOpenBook(rest) {
|
|
2178
|
+
async function handleOpenBook(rest, root) {
|
|
2113
2179
|
if (typeof rest.bookId !== "string" || rest.bookId === "") throw new AccountingError(400, "openBook: bookId is required. Call 'getBooks' to enumerate, or 'createBook' first on a fresh workspace.");
|
|
2114
|
-
const list = await listBooks();
|
|
2180
|
+
const list = await listBooks(root);
|
|
2115
2181
|
if (!list.books.some((book) => book.id === rest.bookId)) throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);
|
|
2116
2182
|
const initialTab = typeof rest.initialTab === "string" ? rest.initialTab : void 0;
|
|
2183
|
+
const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};
|
|
2117
2184
|
return {
|
|
2118
2185
|
kind: "accounting-app",
|
|
2119
2186
|
bookId: rest.bookId,
|
|
2120
2187
|
initialTab,
|
|
2121
|
-
books: list.books
|
|
2188
|
+
books: list.books,
|
|
2189
|
+
...scopeField
|
|
2122
2190
|
};
|
|
2123
2191
|
}
|
|
2124
|
-
async function handleGetReport(rest) {
|
|
2192
|
+
async function handleGetReport(rest, root) {
|
|
2125
2193
|
const kind = typeof rest.kind === "string" ? rest.kind : "";
|
|
2126
2194
|
const periodInput = optionalReportPeriod(rest.period);
|
|
2127
2195
|
const bookId = optionalString(rest.bookId);
|
|
@@ -2132,14 +2200,14 @@ async function handleGetReport(rest) {
|
|
|
2132
2200
|
return getBalanceSheetReport({
|
|
2133
2201
|
bookId,
|
|
2134
2202
|
period: periodInput
|
|
2135
|
-
});
|
|
2203
|
+
}, root);
|
|
2136
2204
|
}
|
|
2137
2205
|
if (kind === "pl") {
|
|
2138
2206
|
if (!periodInput) throw periodRequired("getReport pl");
|
|
2139
2207
|
return getProfitLossReport({
|
|
2140
2208
|
bookId,
|
|
2141
2209
|
period: periodInput
|
|
2142
|
-
});
|
|
2210
|
+
}, root);
|
|
2143
2211
|
}
|
|
2144
2212
|
if (kind === "ledger") {
|
|
2145
2213
|
if (typeof rest.accountCode !== "string" || rest.accountCode === "") throw new AccountingError(400, "getReport ledger: accountCode is required");
|
|
@@ -2147,79 +2215,79 @@ async function handleGetReport(rest) {
|
|
|
2147
2215
|
bookId,
|
|
2148
2216
|
accountCode: rest.accountCode,
|
|
2149
2217
|
period: periodInput
|
|
2150
|
-
});
|
|
2218
|
+
}, root);
|
|
2151
2219
|
}
|
|
2152
2220
|
throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);
|
|
2153
2221
|
}
|
|
2154
2222
|
var ACTION_HANDLERS = {
|
|
2155
2223
|
[require_shared.ACCOUNTING_ACTIONS.openBook]: handleOpenBook,
|
|
2156
|
-
[require_shared.ACCOUNTING_ACTIONS.getBooks]: () => listBooks(),
|
|
2157
|
-
[require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest) => {
|
|
2224
|
+
[require_shared.ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),
|
|
2225
|
+
[require_shared.ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {
|
|
2158
2226
|
const result = await createBook({
|
|
2159
2227
|
name: typeof rest.name === "string" ? rest.name : "",
|
|
2160
2228
|
currency: typeof rest.currency === "string" ? rest.currency : void 0,
|
|
2161
2229
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2162
2230
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2163
|
-
});
|
|
2231
|
+
}, root);
|
|
2164
2232
|
return {
|
|
2165
2233
|
bookId: result.book.id,
|
|
2166
2234
|
...result
|
|
2167
2235
|
};
|
|
2168
2236
|
},
|
|
2169
|
-
[require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest) => {
|
|
2237
|
+
[require_shared.ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {
|
|
2170
2238
|
const result = await updateBook({
|
|
2171
2239
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2172
2240
|
name: typeof rest.name === "string" ? rest.name : void 0,
|
|
2173
2241
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2174
2242
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2175
|
-
});
|
|
2243
|
+
}, root);
|
|
2176
2244
|
return {
|
|
2177
2245
|
bookId: result.book.id,
|
|
2178
2246
|
...result
|
|
2179
2247
|
};
|
|
2180
2248
|
},
|
|
2181
|
-
[require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({
|
|
2249
|
+
[require_shared.ACCOUNTING_ACTIONS.deleteBook]: (rest, root) => deleteBook({
|
|
2182
2250
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2183
2251
|
confirm: rest.confirm === true
|
|
2184
|
-
}),
|
|
2185
|
-
[require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: optionalString(rest.bookId) }),
|
|
2186
|
-
[require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest) => upsertAccount({
|
|
2252
|
+
}, root),
|
|
2253
|
+
[require_shared.ACCOUNTING_ACTIONS.getAccounts]: (rest, root) => listAccounts({ bookId: optionalString(rest.bookId) }, root),
|
|
2254
|
+
[require_shared.ACCOUNTING_ACTIONS.upsertAccount]: (rest, root) => upsertAccount({
|
|
2187
2255
|
bookId: optionalString(rest.bookId),
|
|
2188
2256
|
account: rest.account
|
|
2189
|
-
}),
|
|
2190
|
-
[require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest) => addEntries({
|
|
2257
|
+
}, root),
|
|
2258
|
+
[require_shared.ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({
|
|
2191
2259
|
bookId: optionalString(rest.bookId),
|
|
2192
2260
|
entries: rest.entries
|
|
2193
|
-
}),
|
|
2194
|
-
[require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest) => voidEntry({
|
|
2261
|
+
}, root),
|
|
2262
|
+
[require_shared.ACCOUNTING_ACTIONS.voidEntry]: (rest, root) => voidEntry({
|
|
2195
2263
|
bookId: optionalString(rest.bookId),
|
|
2196
2264
|
entryId: typeof rest.entryId === "string" ? rest.entryId : "",
|
|
2197
2265
|
reason: optionalString(rest.reason),
|
|
2198
2266
|
voidDate: optionalString(rest.voidDate)
|
|
2199
|
-
}),
|
|
2200
|
-
[require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest) => listEntries({
|
|
2267
|
+
}, root),
|
|
2268
|
+
[require_shared.ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) => listEntries({
|
|
2201
2269
|
bookId: optionalString(rest.bookId),
|
|
2202
2270
|
from: optionalString(rest.from),
|
|
2203
2271
|
to: optionalString(rest.to),
|
|
2204
2272
|
accountCode: optionalString(rest.accountCode)
|
|
2205
|
-
}),
|
|
2206
|
-
[require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: optionalString(rest.bookId) }),
|
|
2207
|
-
[require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest) => setOpeningBalances({
|
|
2273
|
+
}, root),
|
|
2274
|
+
[require_shared.ACCOUNTING_ACTIONS.getOpeningBalances]: (rest, root) => getOpeningBalances({ bookId: optionalString(rest.bookId) }, root),
|
|
2275
|
+
[require_shared.ACCOUNTING_ACTIONS.setOpeningBalances]: (rest, root) => setOpeningBalances({
|
|
2208
2276
|
bookId: optionalString(rest.bookId),
|
|
2209
2277
|
asOfDate: typeof rest.asOfDate === "string" ? rest.asOfDate : "",
|
|
2210
2278
|
lines: rest.lines ?? [],
|
|
2211
2279
|
memo: optionalString(rest.memo)
|
|
2212
|
-
}),
|
|
2280
|
+
}, root),
|
|
2213
2281
|
[require_shared.ACCOUNTING_ACTIONS.getReport]: handleGetReport,
|
|
2214
|
-
[require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest) => getTimeSeriesReport({
|
|
2282
|
+
[require_shared.ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) => getTimeSeriesReport({
|
|
2215
2283
|
bookId: optionalString(rest.bookId),
|
|
2216
2284
|
metric: rest.metric,
|
|
2217
2285
|
granularity: rest.granularity,
|
|
2218
2286
|
from: rest.from,
|
|
2219
2287
|
to: rest.to,
|
|
2220
2288
|
accountCode: rest.accountCode
|
|
2221
|
-
}),
|
|
2222
|
-
[require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: optionalString(rest.bookId) })
|
|
2289
|
+
}, root),
|
|
2290
|
+
[require_shared.ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root)
|
|
2223
2291
|
};
|
|
2224
2292
|
var PREVIEW_ACTIONS = /* @__PURE__ */ new Set([
|
|
2225
2293
|
require_shared.ACCOUNTING_ACTIONS.openBook,
|
|
@@ -2296,11 +2364,11 @@ function previewMessage(action, fields) {
|
|
|
2296
2364
|
const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);
|
|
2297
2365
|
return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;
|
|
2298
2366
|
}
|
|
2299
|
-
async function dispatch(body) {
|
|
2367
|
+
async function dispatch(body, root) {
|
|
2300
2368
|
const { action, ...rest } = body;
|
|
2301
2369
|
const handler = ownEntry(ACTION_HANDLERS, action);
|
|
2302
2370
|
if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
|
|
2303
|
-
const result = await handler(rest);
|
|
2371
|
+
const result = await handler(rest, root);
|
|
2304
2372
|
const handlerFields = require_shared.isRecord(result) ? result : { value: result };
|
|
2305
2373
|
const dataField = PREVIEW_ACTIONS.has(action) ? { data: {
|
|
2306
2374
|
action,
|
|
@@ -2317,8 +2385,9 @@ async function dispatch(body) {
|
|
|
2317
2385
|
/** Build the accounting Express router. The host injects its workspace
|
|
2318
2386
|
* root + logger via `configureAccountingServer(...)` and pub/sub via
|
|
2319
2387
|
* `initAccountingEventPublisher(...)`, then mounts the returned router
|
|
2320
|
-
* with `app.use(...)`.
|
|
2321
|
-
|
|
2388
|
+
* with `app.use(...)`. Pass `resolveWorkspaceRoot` to serve more than
|
|
2389
|
+
* one root; omit it and every request uses the configured one. */
|
|
2390
|
+
function createAccountingRouter(options = {}) {
|
|
2322
2391
|
const router = (0, express.Router)();
|
|
2323
2392
|
router.post(require_shared.ACCOUNTING_API.dispatch.path, asyncHandler("accounting", "accounting dispatch failed", async (req, res) => {
|
|
2324
2393
|
const { body } = req;
|
|
@@ -2330,7 +2399,8 @@ function createAccountingRouter() {
|
|
|
2330
2399
|
const { action } = body;
|
|
2331
2400
|
log.info("accounting", "POST dispatch: start", { action });
|
|
2332
2401
|
try {
|
|
2333
|
-
const
|
|
2402
|
+
const root = options.resolveWorkspaceRoot?.(req);
|
|
2403
|
+
const result = await dispatch(body, root);
|
|
2334
2404
|
log.info("accounting", "POST dispatch: ok", { action });
|
|
2335
2405
|
res.json(result);
|
|
2336
2406
|
} catch (err) {
|