@mulmoclaude/accounting-plugin 2.1.0 → 3.0.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 +149 -80
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +149 -80
- 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 +227 -98
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +228 -99
- package/dist/vue.js.map +1 -1
- package/package.json +4 -3
- package/dist/shared-BZAAF-I4.js.map +0 -1
- package/dist/shared-BpFVwa6Y.cjs.map +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as resolveFiscalYearEnd, B as
|
|
1
|
+
import { A as resolveFiscalYearEnd, B as ACCOUNT_TYPES, D as isFiscalYearEnd, F as isUnknownArray, G as ACCOUNTING_ACTIONS, H as JOURNAL_ENTRY_KINDS, L as BOOK_EVENT_KINDS, M as errorMessage, N as hasStringProp, P as isRecord, R as bookChannel, S as FISCAL_YEAR_END_MONTHS, T as fiscalYearEndMonth, U as ACCOUNTING_API, V as BALANCE_SHEET_ACCOUNT_TYPES, _ as SUPPORTED_COUNTRY_CODES, j as ACCOUNTING_DIRS, n as TIME_SERIES_METRICS, t as TIME_SERIES_GRANULARITIES, y as isSupportedCountryCode, z as booksChannel } from "./shared-BccLo0Ux.js";
|
|
2
2
|
import { Router } from "express";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -60,11 +60,41 @@ function configureAccountingServer(context) {
|
|
|
60
60
|
}
|
|
61
61
|
/** Default workspace root for io calls that don't pass one explicitly.
|
|
62
62
|
* Throws if the host never configured the server — a real wiring bug
|
|
63
|
-
* (unit tests always pass an explicit root, so they never hit this)
|
|
63
|
+
* (unit tests always pass an explicit root, so they never hit this) —
|
|
64
|
+
* and also under STRICT mode (`workspaceRoot: null`), where there is no
|
|
65
|
+
* ambient root to fall back to and a missing option is a bug rather
|
|
66
|
+
* than a default. */
|
|
64
67
|
function defaultWorkspaceRoot() {
|
|
65
68
|
if (!deps) throw new Error("@mulmoclaude/accounting-plugin: configureAccountingServer() must be called before serving accounting requests");
|
|
69
|
+
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.");
|
|
66
70
|
return deps.workspaceRoot;
|
|
67
71
|
}
|
|
72
|
+
/** The opaque channel scope for a root, or `null` when the host declared
|
|
73
|
+
* none (single-root hosts, and a multi-root host's default root). Used
|
|
74
|
+
* by the event publisher; never contains a path.
|
|
75
|
+
*
|
|
76
|
+
* A missing root throws under explicit-root mode, exactly as
|
|
77
|
+
* `defaultWorkspaceRoot()` does. Returning `null` there would be worse
|
|
78
|
+
* than the io-path bug it mirrors: the event would go out on the
|
|
79
|
+
* UNSCOPED channel name, which every project's default-scope
|
|
80
|
+
* subscriber receives. The publisher catches it and logs, so a dropped
|
|
81
|
+
* root costs one event and a warning rather than a silent cross-project
|
|
82
|
+
* notification. */
|
|
83
|
+
function channelScopeFor(workspaceRoot) {
|
|
84
|
+
if (!deps?.channelScopeForRoot) return null;
|
|
85
|
+
return deps.channelScopeForRoot(workspaceRoot ?? defaultWorkspaceRoot());
|
|
86
|
+
}
|
|
87
|
+
/** Whether the host declared project scoping at all.
|
|
88
|
+
*
|
|
89
|
+
* The difference matters where a card records the project it belongs
|
|
90
|
+
* to: on a scoped host, "no scope" is a REAL answer (the default root)
|
|
91
|
+
* and must be recorded as `null`, not left out — an omitted field means
|
|
92
|
+
* "ask the host what is active now", which is precisely the drift a
|
|
93
|
+
* pinned card exists to prevent. On an unscoped host the field is left
|
|
94
|
+
* out entirely and every payload stays byte-identical to today's. */
|
|
95
|
+
function isProjectScopedHost() {
|
|
96
|
+
return Boolean(deps?.channelScopeForRoot);
|
|
97
|
+
}
|
|
68
98
|
var consoleLogger = {
|
|
69
99
|
error: (namespace, msg, data) => console.error(`[${namespace}] ${msg}`, data ?? ""),
|
|
70
100
|
warn: (namespace, msg, data) => console.warn(`[${namespace}] ${msg}`, data ?? ""),
|
|
@@ -1141,9 +1171,16 @@ var pubsub = null;
|
|
|
1141
1171
|
function initAccountingEventPublisher(instance) {
|
|
1142
1172
|
pubsub = instance;
|
|
1143
1173
|
}
|
|
1144
|
-
|
|
1174
|
+
/** `channelFor` is called INSIDE the guard, not before it: resolving the
|
|
1175
|
+
* scope can throw (a write that reached the publisher with no root
|
|
1176
|
+
* under explicit-root mode), and a lost event with a warning beats
|
|
1177
|
+
* either crashing the write or publishing to the unscoped channel that
|
|
1178
|
+
* every project can hear. */
|
|
1179
|
+
function safePublish(channelFor, payload) {
|
|
1145
1180
|
if (!pubsub) return;
|
|
1181
|
+
let channel = "(unresolved)";
|
|
1146
1182
|
try {
|
|
1183
|
+
channel = channelFor();
|
|
1147
1184
|
pubsub.publish(channel, payload);
|
|
1148
1185
|
} catch (err) {
|
|
1149
1186
|
log.warn("accounting", "publish failed; subscribers will miss this event", {
|
|
@@ -1154,15 +1191,20 @@ function safePublish(channel, payload) {
|
|
|
1154
1191
|
}
|
|
1155
1192
|
/** Per-book change notification. `period` should be the entry's
|
|
1156
1193
|
* YYYY-MM bucket (or the earliest invalidated month for snapshot
|
|
1157
|
-
* events).
|
|
1158
|
-
|
|
1159
|
-
|
|
1194
|
+
* events).
|
|
1195
|
+
*
|
|
1196
|
+
* `workspaceRoot` is the root the write happened under; the channel
|
|
1197
|
+
* name is namespaced by the host's opaque scope for it (see
|
|
1198
|
+
* `channelScopeFor`). Omit it — as a single-root host's service calls
|
|
1199
|
+
* do — and the name is what it has always been. */
|
|
1200
|
+
function publishBookChange(bookId, payload, workspaceRoot) {
|
|
1201
|
+
safePublish(() => bookChannel(bookId, channelScopeFor(workspaceRoot)), payload);
|
|
1160
1202
|
}
|
|
1161
1203
|
/** Fired when the *list* of books changes (createBook, deleteBook).
|
|
1162
1204
|
* Payload is intentionally empty — subscribers refetch from
|
|
1163
|
-
* /api/accounting. */
|
|
1164
|
-
function publishBooksChanged() {
|
|
1165
|
-
safePublish(
|
|
1205
|
+
* /api/accounting. Scoped by root like `publishBookChange`. */
|
|
1206
|
+
function publishBooksChanged(workspaceRoot) {
|
|
1207
|
+
safePublish(() => booksChannel(channelScopeFor(workspaceRoot)), {});
|
|
1166
1208
|
}
|
|
1167
1209
|
//#endregion
|
|
1168
1210
|
//#region src/server/snapshotCache.ts
|
|
@@ -1231,18 +1273,39 @@ async function rebuildAllSnapshots(bookId, workspaceRoot) {
|
|
|
1231
1273
|
return { rebuilt: periods };
|
|
1232
1274
|
}
|
|
1233
1275
|
var rebuildQueues = /* @__PURE__ */ new Map();
|
|
1276
|
+
/** Canonical key for the root a rebuild belongs to. An absent root
|
|
1277
|
+
* resolves to the host's configured one, so a call that passes it
|
|
1278
|
+
* explicitly and one that relies on the default share a queue. Under
|
|
1279
|
+
* explicit-root mode there is no default and `defaultWorkspaceRoot()`
|
|
1280
|
+
* throws — deliberately loud: a `cancelRebuild` / `awaitRebuildIdle`
|
|
1281
|
+
* that quietly keyed a queue nobody scheduled would return while the
|
|
1282
|
+
* real rebuild is still writing, which is how `deleteBook` races a
|
|
1283
|
+
* `writeSnapshot` back into existence.
|
|
1284
|
+
*
|
|
1285
|
+
* Lexical resolution only — symlinks are deliberately NOT resolved,
|
|
1286
|
+
* matching `canonicalRoot` in `@mulmoclaude/core`'s collection engine,
|
|
1287
|
+
* so one project has one identity across both packages. A host that
|
|
1288
|
+
* hands the engine a real path in one call and a symlink alias in the
|
|
1289
|
+
* next would split the queue; the fix belongs in the host's own root
|
|
1290
|
+
* resolution, which is where the two packages agree it lives. */
|
|
1291
|
+
function rootKey(workspaceRoot) {
|
|
1292
|
+
return path.resolve(workspaceRoot ?? defaultWorkspaceRoot());
|
|
1293
|
+
}
|
|
1294
|
+
function queueKey(bookId, workspaceRoot) {
|
|
1295
|
+
return `${rootKey(workspaceRoot)}\u0000${bookId}`;
|
|
1296
|
+
}
|
|
1234
1297
|
function minPeriod(lhs, rhs) {
|
|
1235
1298
|
if (lhs === null) return rhs;
|
|
1236
1299
|
return lhs < rhs ? lhs : rhs;
|
|
1237
1300
|
}
|
|
1238
|
-
function isInvalidatedDuringRebuild(
|
|
1239
|
-
const queue = rebuildQueues.get(
|
|
1301
|
+
function isInvalidatedDuringRebuild(key, period) {
|
|
1302
|
+
const queue = rebuildQueues.get(key);
|
|
1240
1303
|
return queue !== void 0 && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;
|
|
1241
1304
|
}
|
|
1242
|
-
function isCancelled(
|
|
1243
|
-
return rebuildQueues.get(
|
|
1305
|
+
function isCancelled(key) {
|
|
1306
|
+
return rebuildQueues.get(key)?.cancelled === true;
|
|
1244
1307
|
}
|
|
1245
|
-
async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1308
|
+
async function runRebuild(key, bookId, fromPeriod, workspaceRoot) {
|
|
1246
1309
|
const startedAt = Date.now();
|
|
1247
1310
|
log.info("accounting", "snapshot rebuild started", {
|
|
1248
1311
|
bookId,
|
|
@@ -1251,25 +1314,25 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1251
1314
|
publishBookChange(bookId, {
|
|
1252
1315
|
kind: BOOK_EVENT_KINDS.snapshotsRebuilding,
|
|
1253
1316
|
period: fromPeriod
|
|
1254
|
-
});
|
|
1317
|
+
}, workspaceRoot);
|
|
1255
1318
|
const targets = (await listJournalPeriods(bookId, workspaceRoot)).filter((monthKey) => monthKey >= fromPeriod);
|
|
1256
1319
|
let written = 0;
|
|
1257
1320
|
for (const monthKey of targets) {
|
|
1258
|
-
if (isCancelled(
|
|
1259
|
-
if (isInvalidatedDuringRebuild(
|
|
1321
|
+
if (isCancelled(key)) break;
|
|
1322
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) break;
|
|
1260
1323
|
const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);
|
|
1261
|
-
if (isCancelled(
|
|
1262
|
-
if (isInvalidatedDuringRebuild(
|
|
1324
|
+
if (isCancelled(key)) break;
|
|
1325
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) break;
|
|
1263
1326
|
await writeSnapshot(bookId, {
|
|
1264
1327
|
period: monthKey,
|
|
1265
1328
|
balances,
|
|
1266
1329
|
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1267
1330
|
}, workspaceRoot);
|
|
1268
|
-
if (isCancelled(
|
|
1331
|
+
if (isCancelled(key)) {
|
|
1269
1332
|
await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
|
|
1270
1333
|
break;
|
|
1271
1334
|
}
|
|
1272
|
-
if (isInvalidatedDuringRebuild(
|
|
1335
|
+
if (isInvalidatedDuringRebuild(key, monthKey)) {
|
|
1273
1336
|
await invalidateSnapshotsFrom(bookId, monthKey, workspaceRoot);
|
|
1274
1337
|
break;
|
|
1275
1338
|
}
|
|
@@ -1277,7 +1340,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1277
1340
|
publishBookChange(bookId, {
|
|
1278
1341
|
kind: BOOK_EVENT_KINDS.snapshotsReady,
|
|
1279
1342
|
period: monthKey
|
|
1280
|
-
});
|
|
1343
|
+
}, workspaceRoot);
|
|
1281
1344
|
}
|
|
1282
1345
|
log.info("accounting", "snapshot rebuild done", {
|
|
1283
1346
|
bookId,
|
|
@@ -1285,7 +1348,7 @@ async function runRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1285
1348
|
durationMs: Date.now() - startedAt
|
|
1286
1349
|
});
|
|
1287
1350
|
}
|
|
1288
|
-
function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1351
|
+
function startRebuild(key, bookId, fromPeriod, workspaceRoot) {
|
|
1289
1352
|
const entry = {
|
|
1290
1353
|
running: Promise.resolve(),
|
|
1291
1354
|
pendingFromPeriod: null,
|
|
@@ -1294,27 +1357,27 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1294
1357
|
runningFromPeriod: fromPeriod,
|
|
1295
1358
|
cancelled: false
|
|
1296
1359
|
};
|
|
1297
|
-
entry.running = runRebuild(bookId, fromPeriod, workspaceRoot).catch((err) => {
|
|
1360
|
+
entry.running = runRebuild(key, bookId, fromPeriod, workspaceRoot).catch((err) => {
|
|
1298
1361
|
log.error("accounting", "snapshot rebuild failed", {
|
|
1299
1362
|
bookId,
|
|
1300
1363
|
fromPeriod,
|
|
1301
1364
|
error: errorMessage(err)
|
|
1302
1365
|
});
|
|
1303
1366
|
}).then(() => {
|
|
1304
|
-
const current = rebuildQueues.get(
|
|
1367
|
+
const current = rebuildQueues.get(key);
|
|
1305
1368
|
if (!current) return;
|
|
1306
1369
|
if (current.cancelled) {
|
|
1307
|
-
rebuildQueues.delete(
|
|
1370
|
+
rebuildQueues.delete(key);
|
|
1308
1371
|
return;
|
|
1309
1372
|
}
|
|
1310
1373
|
if (current.pendingFromPeriod !== null) {
|
|
1311
1374
|
const nextFrom = current.pendingFromPeriod;
|
|
1312
1375
|
const nextRoot = current.pendingWorkspaceRoot;
|
|
1313
1376
|
const carriedCount = current.coalescedWriteCount;
|
|
1314
|
-
const successor = startRebuild(bookId, nextFrom, nextRoot);
|
|
1377
|
+
const successor = startRebuild(key, bookId, nextFrom, nextRoot);
|
|
1315
1378
|
successor.coalescedWriteCount += carriedCount;
|
|
1316
|
-
rebuildQueues.set(
|
|
1317
|
-
} else rebuildQueues.delete(
|
|
1379
|
+
rebuildQueues.set(key, successor);
|
|
1380
|
+
} else rebuildQueues.delete(key);
|
|
1318
1381
|
});
|
|
1319
1382
|
return entry;
|
|
1320
1383
|
}
|
|
@@ -1323,9 +1386,10 @@ function startRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1323
1386
|
* follow-up rebuild that covers the minimum `fromPeriod` seen.
|
|
1324
1387
|
* Returns immediately — the rebuild runs on its own promise chain. */
|
|
1325
1388
|
function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
1326
|
-
const
|
|
1389
|
+
const key = queueKey(bookId, workspaceRoot);
|
|
1390
|
+
const existing = rebuildQueues.get(key);
|
|
1327
1391
|
if (!existing) {
|
|
1328
|
-
rebuildQueues.set(
|
|
1392
|
+
rebuildQueues.set(key, startRebuild(key, bookId, fromPeriod, workspaceRoot));
|
|
1329
1393
|
return;
|
|
1330
1394
|
}
|
|
1331
1395
|
existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);
|
|
@@ -1336,9 +1400,10 @@ function scheduleRebuild(bookId, fromPeriod, workspaceRoot) {
|
|
|
1336
1400
|
* `bookId`. Also called by `deleteBook` after `cancelRebuild` to
|
|
1337
1401
|
* ensure a previously running rebuild has fully stopped before the
|
|
1338
1402
|
* caller removes the book's directory on disk. */
|
|
1339
|
-
async function awaitRebuildIdle(bookId) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1403
|
+
async function awaitRebuildIdle(bookId, workspaceRoot) {
|
|
1404
|
+
const key = queueKey(bookId, workspaceRoot);
|
|
1405
|
+
while (rebuildQueues.has(key)) {
|
|
1406
|
+
const entry = rebuildQueues.get(key);
|
|
1342
1407
|
if (!entry) return;
|
|
1343
1408
|
await entry.running;
|
|
1344
1409
|
}
|
|
@@ -1348,8 +1413,8 @@ async function awaitRebuildIdle(bookId) {
|
|
|
1348
1413
|
* `removeBookDir` cannot race with a `writeSnapshot` that would
|
|
1349
1414
|
* re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`
|
|
1350
1415
|
* to wait for the in-flight rebuild to finish bailing. */
|
|
1351
|
-
function cancelRebuild(bookId) {
|
|
1352
|
-
const entry = rebuildQueues.get(bookId);
|
|
1416
|
+
function cancelRebuild(bookId, workspaceRoot) {
|
|
1417
|
+
const entry = rebuildQueues.get(queueKey(bookId, workspaceRoot));
|
|
1353
1418
|
if (!entry) return;
|
|
1354
1419
|
entry.cancelled = true;
|
|
1355
1420
|
entry.pendingFromPeriod = null;
|
|
@@ -1703,7 +1768,7 @@ async function createBook(input, workspaceRoot) {
|
|
|
1703
1768
|
await ensureBookDir(bookId, workspaceRoot);
|
|
1704
1769
|
await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);
|
|
1705
1770
|
await writeConfig({ books: [...config.books, book] }, workspaceRoot);
|
|
1706
|
-
publishBooksChanged();
|
|
1771
|
+
publishBooksChanged(workspaceRoot);
|
|
1707
1772
|
return { book };
|
|
1708
1773
|
}
|
|
1709
1774
|
async function updateBook(input, workspaceRoot) {
|
|
@@ -1720,7 +1785,7 @@ async function updateBook(input, workspaceRoot) {
|
|
|
1720
1785
|
};
|
|
1721
1786
|
if (country === "") delete next.country;
|
|
1722
1787
|
await writeConfig({ books: config.books.map((book) => book.id === input.bookId ? next : book) }, workspaceRoot);
|
|
1723
|
-
publishBooksChanged();
|
|
1788
|
+
publishBooksChanged(workspaceRoot);
|
|
1724
1789
|
return { book: next };
|
|
1725
1790
|
}
|
|
1726
1791
|
async function deleteBook(input, workspaceRoot) {
|
|
@@ -1728,11 +1793,11 @@ async function deleteBook(input, workspaceRoot) {
|
|
|
1728
1793
|
const config = await loadOrInitConfig(workspaceRoot);
|
|
1729
1794
|
const target = findBook(config, input.bookId);
|
|
1730
1795
|
if (!target) throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
|
|
1731
|
-
cancelRebuild(input.bookId);
|
|
1732
|
-
await awaitRebuildIdle(input.bookId);
|
|
1796
|
+
cancelRebuild(input.bookId, workspaceRoot);
|
|
1797
|
+
await awaitRebuildIdle(input.bookId, workspaceRoot);
|
|
1733
1798
|
await removeBookDir(input.bookId, workspaceRoot);
|
|
1734
1799
|
await writeConfig({ books: config.books.filter((book) => book.id !== input.bookId) }, workspaceRoot);
|
|
1735
|
-
publishBooksChanged();
|
|
1800
|
+
publishBooksChanged(workspaceRoot);
|
|
1736
1801
|
return {
|
|
1737
1802
|
deletedBookId: input.bookId,
|
|
1738
1803
|
deletedBookName: target.name
|
|
@@ -1782,7 +1847,7 @@ async function upsertAccount(input, workspaceRoot) {
|
|
|
1782
1847
|
scheduleRebuild(bookId, "0000-00", workspaceRoot);
|
|
1783
1848
|
await invalidateAllSnapshots(bookId, workspaceRoot);
|
|
1784
1849
|
}
|
|
1785
|
-
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.accounts });
|
|
1850
|
+
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.accounts }, workspaceRoot);
|
|
1786
1851
|
return {
|
|
1787
1852
|
bookId,
|
|
1788
1853
|
account,
|
|
@@ -1831,7 +1896,7 @@ async function addEntries(input, workspaceRoot) {
|
|
|
1831
1896
|
publishBookChange(bookId, {
|
|
1832
1897
|
kind: BOOK_EVENT_KINDS.journal,
|
|
1833
1898
|
period: earliestPeriod
|
|
1834
|
-
});
|
|
1899
|
+
}, workspaceRoot);
|
|
1835
1900
|
return {
|
|
1836
1901
|
bookId,
|
|
1837
1902
|
entries: built
|
|
@@ -1860,7 +1925,7 @@ async function voidEntry(input, workspaceRoot) {
|
|
|
1860
1925
|
publishBookChange(bookId, {
|
|
1861
1926
|
kind: BOOK_EVENT_KINDS.journal,
|
|
1862
1927
|
period: fromPeriod
|
|
1863
|
-
});
|
|
1928
|
+
}, workspaceRoot);
|
|
1864
1929
|
return {
|
|
1865
1930
|
bookId,
|
|
1866
1931
|
reverseEntry: reverse,
|
|
@@ -1924,7 +1989,7 @@ async function setOpeningBalances(input, workspaceRoot) {
|
|
|
1924
1989
|
await appendJournal(bookId, opening, workspaceRoot);
|
|
1925
1990
|
scheduleRebuild(bookId, "0000-00", workspaceRoot);
|
|
1926
1991
|
await invalidateAllSnapshots(bookId, workspaceRoot);
|
|
1927
|
-
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.opening });
|
|
1992
|
+
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.opening }, workspaceRoot);
|
|
1928
1993
|
return {
|
|
1929
1994
|
bookId,
|
|
1930
1995
|
openingEntry: opening,
|
|
@@ -2059,7 +2124,7 @@ async function getTimeSeriesReport(input, workspaceRoot) {
|
|
|
2059
2124
|
async function rebuildSnapshots(input, workspaceRoot) {
|
|
2060
2125
|
const bookId = resolveBookId(await loadOrInitConfig(workspaceRoot), input.bookId);
|
|
2061
2126
|
const result = await rebuildAllSnapshots(bookId, workspaceRoot);
|
|
2062
|
-
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.snapshotsReady });
|
|
2127
|
+
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);
|
|
2063
2128
|
return {
|
|
2064
2129
|
bookId,
|
|
2065
2130
|
rebuilt: result.rebuilt
|
|
@@ -2086,19 +2151,21 @@ function asyncHandler(namespace, fallbackMessage, handler) {
|
|
|
2086
2151
|
}
|
|
2087
2152
|
//#endregion
|
|
2088
2153
|
//#region src/server/router.ts
|
|
2089
|
-
async function handleOpenBook(rest) {
|
|
2154
|
+
async function handleOpenBook(rest, root) {
|
|
2090
2155
|
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.");
|
|
2091
|
-
const list = await listBooks();
|
|
2156
|
+
const list = await listBooks(root);
|
|
2092
2157
|
if (!list.books.some((book) => book.id === rest.bookId)) throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);
|
|
2093
2158
|
const initialTab = typeof rest.initialTab === "string" ? rest.initialTab : void 0;
|
|
2159
|
+
const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};
|
|
2094
2160
|
return {
|
|
2095
2161
|
kind: "accounting-app",
|
|
2096
2162
|
bookId: rest.bookId,
|
|
2097
2163
|
initialTab,
|
|
2098
|
-
books: list.books
|
|
2164
|
+
books: list.books,
|
|
2165
|
+
...scopeField
|
|
2099
2166
|
};
|
|
2100
2167
|
}
|
|
2101
|
-
async function handleGetReport(rest) {
|
|
2168
|
+
async function handleGetReport(rest, root) {
|
|
2102
2169
|
const kind = typeof rest.kind === "string" ? rest.kind : "";
|
|
2103
2170
|
const periodInput = optionalReportPeriod(rest.period);
|
|
2104
2171
|
const bookId = optionalString(rest.bookId);
|
|
@@ -2109,14 +2176,14 @@ async function handleGetReport(rest) {
|
|
|
2109
2176
|
return getBalanceSheetReport({
|
|
2110
2177
|
bookId,
|
|
2111
2178
|
period: periodInput
|
|
2112
|
-
});
|
|
2179
|
+
}, root);
|
|
2113
2180
|
}
|
|
2114
2181
|
if (kind === "pl") {
|
|
2115
2182
|
if (!periodInput) throw periodRequired("getReport pl");
|
|
2116
2183
|
return getProfitLossReport({
|
|
2117
2184
|
bookId,
|
|
2118
2185
|
period: periodInput
|
|
2119
|
-
});
|
|
2186
|
+
}, root);
|
|
2120
2187
|
}
|
|
2121
2188
|
if (kind === "ledger") {
|
|
2122
2189
|
if (typeof rest.accountCode !== "string" || rest.accountCode === "") throw new AccountingError(400, "getReport ledger: accountCode is required");
|
|
@@ -2124,79 +2191,79 @@ async function handleGetReport(rest) {
|
|
|
2124
2191
|
bookId,
|
|
2125
2192
|
accountCode: rest.accountCode,
|
|
2126
2193
|
period: periodInput
|
|
2127
|
-
});
|
|
2194
|
+
}, root);
|
|
2128
2195
|
}
|
|
2129
2196
|
throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);
|
|
2130
2197
|
}
|
|
2131
2198
|
var ACTION_HANDLERS = {
|
|
2132
2199
|
[ACCOUNTING_ACTIONS.openBook]: handleOpenBook,
|
|
2133
|
-
[ACCOUNTING_ACTIONS.getBooks]: () => listBooks(),
|
|
2134
|
-
[ACCOUNTING_ACTIONS.createBook]: async (rest) => {
|
|
2200
|
+
[ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),
|
|
2201
|
+
[ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {
|
|
2135
2202
|
const result = await createBook({
|
|
2136
2203
|
name: typeof rest.name === "string" ? rest.name : "",
|
|
2137
2204
|
currency: typeof rest.currency === "string" ? rest.currency : void 0,
|
|
2138
2205
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2139
2206
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2140
|
-
});
|
|
2207
|
+
}, root);
|
|
2141
2208
|
return {
|
|
2142
2209
|
bookId: result.book.id,
|
|
2143
2210
|
...result
|
|
2144
2211
|
};
|
|
2145
2212
|
},
|
|
2146
|
-
[ACCOUNTING_ACTIONS.updateBook]: async (rest) => {
|
|
2213
|
+
[ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {
|
|
2147
2214
|
const result = await updateBook({
|
|
2148
2215
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2149
2216
|
name: typeof rest.name === "string" ? rest.name : void 0,
|
|
2150
2217
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2151
2218
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2152
|
-
});
|
|
2219
|
+
}, root);
|
|
2153
2220
|
return {
|
|
2154
2221
|
bookId: result.book.id,
|
|
2155
2222
|
...result
|
|
2156
2223
|
};
|
|
2157
2224
|
},
|
|
2158
|
-
[ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({
|
|
2225
|
+
[ACCOUNTING_ACTIONS.deleteBook]: (rest, root) => deleteBook({
|
|
2159
2226
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2160
2227
|
confirm: rest.confirm === true
|
|
2161
|
-
}),
|
|
2162
|
-
[ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: optionalString(rest.bookId) }),
|
|
2163
|
-
[ACCOUNTING_ACTIONS.upsertAccount]: (rest) => upsertAccount({
|
|
2228
|
+
}, root),
|
|
2229
|
+
[ACCOUNTING_ACTIONS.getAccounts]: (rest, root) => listAccounts({ bookId: optionalString(rest.bookId) }, root),
|
|
2230
|
+
[ACCOUNTING_ACTIONS.upsertAccount]: (rest, root) => upsertAccount({
|
|
2164
2231
|
bookId: optionalString(rest.bookId),
|
|
2165
2232
|
account: rest.account
|
|
2166
|
-
}),
|
|
2167
|
-
[ACCOUNTING_ACTIONS.addEntries]: (rest) => addEntries({
|
|
2233
|
+
}, root),
|
|
2234
|
+
[ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({
|
|
2168
2235
|
bookId: optionalString(rest.bookId),
|
|
2169
2236
|
entries: rest.entries
|
|
2170
|
-
}),
|
|
2171
|
-
[ACCOUNTING_ACTIONS.voidEntry]: (rest) => voidEntry({
|
|
2237
|
+
}, root),
|
|
2238
|
+
[ACCOUNTING_ACTIONS.voidEntry]: (rest, root) => voidEntry({
|
|
2172
2239
|
bookId: optionalString(rest.bookId),
|
|
2173
2240
|
entryId: typeof rest.entryId === "string" ? rest.entryId : "",
|
|
2174
2241
|
reason: optionalString(rest.reason),
|
|
2175
2242
|
voidDate: optionalString(rest.voidDate)
|
|
2176
|
-
}),
|
|
2177
|
-
[ACCOUNTING_ACTIONS.getJournalEntries]: (rest) => listEntries({
|
|
2243
|
+
}, root),
|
|
2244
|
+
[ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) => listEntries({
|
|
2178
2245
|
bookId: optionalString(rest.bookId),
|
|
2179
2246
|
from: optionalString(rest.from),
|
|
2180
2247
|
to: optionalString(rest.to),
|
|
2181
2248
|
accountCode: optionalString(rest.accountCode)
|
|
2182
|
-
}),
|
|
2183
|
-
[ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: optionalString(rest.bookId) }),
|
|
2184
|
-
[ACCOUNTING_ACTIONS.setOpeningBalances]: (rest) => setOpeningBalances({
|
|
2249
|
+
}, root),
|
|
2250
|
+
[ACCOUNTING_ACTIONS.getOpeningBalances]: (rest, root) => getOpeningBalances({ bookId: optionalString(rest.bookId) }, root),
|
|
2251
|
+
[ACCOUNTING_ACTIONS.setOpeningBalances]: (rest, root) => setOpeningBalances({
|
|
2185
2252
|
bookId: optionalString(rest.bookId),
|
|
2186
2253
|
asOfDate: typeof rest.asOfDate === "string" ? rest.asOfDate : "",
|
|
2187
2254
|
lines: rest.lines ?? [],
|
|
2188
2255
|
memo: optionalString(rest.memo)
|
|
2189
|
-
}),
|
|
2256
|
+
}, root),
|
|
2190
2257
|
[ACCOUNTING_ACTIONS.getReport]: handleGetReport,
|
|
2191
|
-
[ACCOUNTING_ACTIONS.getTimeSeries]: (rest) => getTimeSeriesReport({
|
|
2258
|
+
[ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) => getTimeSeriesReport({
|
|
2192
2259
|
bookId: optionalString(rest.bookId),
|
|
2193
2260
|
metric: rest.metric,
|
|
2194
2261
|
granularity: rest.granularity,
|
|
2195
2262
|
from: rest.from,
|
|
2196
2263
|
to: rest.to,
|
|
2197
2264
|
accountCode: rest.accountCode
|
|
2198
|
-
}),
|
|
2199
|
-
[ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: optionalString(rest.bookId) })
|
|
2265
|
+
}, root),
|
|
2266
|
+
[ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root)
|
|
2200
2267
|
};
|
|
2201
2268
|
var PREVIEW_ACTIONS = /* @__PURE__ */ new Set([
|
|
2202
2269
|
ACCOUNTING_ACTIONS.openBook,
|
|
@@ -2273,11 +2340,11 @@ function previewMessage(action, fields) {
|
|
|
2273
2340
|
const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);
|
|
2274
2341
|
return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;
|
|
2275
2342
|
}
|
|
2276
|
-
async function dispatch(body) {
|
|
2343
|
+
async function dispatch(body, root) {
|
|
2277
2344
|
const { action, ...rest } = body;
|
|
2278
2345
|
const handler = ownEntry(ACTION_HANDLERS, action);
|
|
2279
2346
|
if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
|
|
2280
|
-
const result = await handler(rest);
|
|
2347
|
+
const result = await handler(rest, root);
|
|
2281
2348
|
const handlerFields = isRecord(result) ? result : { value: result };
|
|
2282
2349
|
const dataField = PREVIEW_ACTIONS.has(action) ? { data: {
|
|
2283
2350
|
action,
|
|
@@ -2294,8 +2361,9 @@ async function dispatch(body) {
|
|
|
2294
2361
|
/** Build the accounting Express router. The host injects its workspace
|
|
2295
2362
|
* root + logger via `configureAccountingServer(...)` and pub/sub via
|
|
2296
2363
|
* `initAccountingEventPublisher(...)`, then mounts the returned router
|
|
2297
|
-
* with `app.use(...)`.
|
|
2298
|
-
|
|
2364
|
+
* with `app.use(...)`. Pass `resolveWorkspaceRoot` to serve more than
|
|
2365
|
+
* one root; omit it and every request uses the configured one. */
|
|
2366
|
+
function createAccountingRouter(options = {}) {
|
|
2299
2367
|
const router = Router();
|
|
2300
2368
|
router.post(ACCOUNTING_API.dispatch.path, asyncHandler("accounting", "accounting dispatch failed", async (req, res) => {
|
|
2301
2369
|
const { body } = req;
|
|
@@ -2307,7 +2375,8 @@ function createAccountingRouter() {
|
|
|
2307
2375
|
const { action } = body;
|
|
2308
2376
|
log.info("accounting", "POST dispatch: start", { action });
|
|
2309
2377
|
try {
|
|
2310
|
-
const
|
|
2378
|
+
const root = options.resolveWorkspaceRoot?.(req);
|
|
2379
|
+
const result = await dispatch(body, root);
|
|
2311
2380
|
log.info("accounting", "POST dispatch: ok", { action });
|
|
2312
2381
|
res.json(result);
|
|
2313
2382
|
} catch (err) {
|