@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.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,
|
|
@@ -2022,9 +2087,10 @@ function validateTimeSeriesInput(input) {
|
|
|
2022
2087
|
async function loadTimeSeriesBookContext(requestedBookId, workspaceRoot) {
|
|
2023
2088
|
const config = await loadOrInitConfig(workspaceRoot);
|
|
2024
2089
|
const bookId = resolveBookId(config, requestedBookId);
|
|
2090
|
+
const book = findBook(config, bookId);
|
|
2025
2091
|
return {
|
|
2026
2092
|
bookId,
|
|
2027
|
-
fiscalYearEnd: resolveFiscalYearEnd(
|
|
2093
|
+
fiscalYearEnd: resolveFiscalYearEnd(book?.fiscalYearEnd),
|
|
2028
2094
|
accounts: await readAccounts(bookId, workspaceRoot)
|
|
2029
2095
|
};
|
|
2030
2096
|
}
|
|
@@ -2058,7 +2124,7 @@ async function getTimeSeriesReport(input, workspaceRoot) {
|
|
|
2058
2124
|
async function rebuildSnapshots(input, workspaceRoot) {
|
|
2059
2125
|
const bookId = resolveBookId(await loadOrInitConfig(workspaceRoot), input.bookId);
|
|
2060
2126
|
const result = await rebuildAllSnapshots(bookId, workspaceRoot);
|
|
2061
|
-
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.snapshotsReady });
|
|
2127
|
+
publishBookChange(bookId, { kind: BOOK_EVENT_KINDS.snapshotsReady }, workspaceRoot);
|
|
2062
2128
|
return {
|
|
2063
2129
|
bookId,
|
|
2064
2130
|
rebuilt: result.rebuilt
|
|
@@ -2085,19 +2151,21 @@ function asyncHandler(namespace, fallbackMessage, handler) {
|
|
|
2085
2151
|
}
|
|
2086
2152
|
//#endregion
|
|
2087
2153
|
//#region src/server/router.ts
|
|
2088
|
-
async function handleOpenBook(rest) {
|
|
2154
|
+
async function handleOpenBook(rest, root) {
|
|
2089
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.");
|
|
2090
|
-
const list = await listBooks();
|
|
2156
|
+
const list = await listBooks(root);
|
|
2091
2157
|
if (!list.books.some((book) => book.id === rest.bookId)) throw new AccountingError(404, `openBook: book ${JSON.stringify(rest.bookId)} not found`);
|
|
2092
2158
|
const initialTab = typeof rest.initialTab === "string" ? rest.initialTab : void 0;
|
|
2159
|
+
const scopeField = isProjectScopedHost() ? { scope: channelScopeFor(root) } : {};
|
|
2093
2160
|
return {
|
|
2094
2161
|
kind: "accounting-app",
|
|
2095
2162
|
bookId: rest.bookId,
|
|
2096
2163
|
initialTab,
|
|
2097
|
-
books: list.books
|
|
2164
|
+
books: list.books,
|
|
2165
|
+
...scopeField
|
|
2098
2166
|
};
|
|
2099
2167
|
}
|
|
2100
|
-
async function handleGetReport(rest) {
|
|
2168
|
+
async function handleGetReport(rest, root) {
|
|
2101
2169
|
const kind = typeof rest.kind === "string" ? rest.kind : "";
|
|
2102
2170
|
const periodInput = optionalReportPeriod(rest.period);
|
|
2103
2171
|
const bookId = optionalString(rest.bookId);
|
|
@@ -2108,14 +2176,14 @@ async function handleGetReport(rest) {
|
|
|
2108
2176
|
return getBalanceSheetReport({
|
|
2109
2177
|
bookId,
|
|
2110
2178
|
period: periodInput
|
|
2111
|
-
});
|
|
2179
|
+
}, root);
|
|
2112
2180
|
}
|
|
2113
2181
|
if (kind === "pl") {
|
|
2114
2182
|
if (!periodInput) throw periodRequired("getReport pl");
|
|
2115
2183
|
return getProfitLossReport({
|
|
2116
2184
|
bookId,
|
|
2117
2185
|
period: periodInput
|
|
2118
|
-
});
|
|
2186
|
+
}, root);
|
|
2119
2187
|
}
|
|
2120
2188
|
if (kind === "ledger") {
|
|
2121
2189
|
if (typeof rest.accountCode !== "string" || rest.accountCode === "") throw new AccountingError(400, "getReport ledger: accountCode is required");
|
|
@@ -2123,79 +2191,79 @@ async function handleGetReport(rest) {
|
|
|
2123
2191
|
bookId,
|
|
2124
2192
|
accountCode: rest.accountCode,
|
|
2125
2193
|
period: periodInput
|
|
2126
|
-
});
|
|
2194
|
+
}, root);
|
|
2127
2195
|
}
|
|
2128
2196
|
throw new AccountingError(400, `getReport: unknown kind ${JSON.stringify(kind)}`);
|
|
2129
2197
|
}
|
|
2130
2198
|
var ACTION_HANDLERS = {
|
|
2131
2199
|
[ACCOUNTING_ACTIONS.openBook]: handleOpenBook,
|
|
2132
|
-
[ACCOUNTING_ACTIONS.getBooks]: () => listBooks(),
|
|
2133
|
-
[ACCOUNTING_ACTIONS.createBook]: async (rest) => {
|
|
2200
|
+
[ACCOUNTING_ACTIONS.getBooks]: (_rest, root) => listBooks(root),
|
|
2201
|
+
[ACCOUNTING_ACTIONS.createBook]: async (rest, root) => {
|
|
2134
2202
|
const result = await createBook({
|
|
2135
2203
|
name: typeof rest.name === "string" ? rest.name : "",
|
|
2136
2204
|
currency: typeof rest.currency === "string" ? rest.currency : void 0,
|
|
2137
2205
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2138
2206
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2139
|
-
});
|
|
2207
|
+
}, root);
|
|
2140
2208
|
return {
|
|
2141
2209
|
bookId: result.book.id,
|
|
2142
2210
|
...result
|
|
2143
2211
|
};
|
|
2144
2212
|
},
|
|
2145
|
-
[ACCOUNTING_ACTIONS.updateBook]: async (rest) => {
|
|
2213
|
+
[ACCOUNTING_ACTIONS.updateBook]: async (rest, root) => {
|
|
2146
2214
|
const result = await updateBook({
|
|
2147
2215
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2148
2216
|
name: typeof rest.name === "string" ? rest.name : void 0,
|
|
2149
2217
|
country: typeof rest.country === "string" ? rest.country : void 0,
|
|
2150
2218
|
fiscalYearEnd: rest.fiscalYearEnd
|
|
2151
|
-
});
|
|
2219
|
+
}, root);
|
|
2152
2220
|
return {
|
|
2153
2221
|
bookId: result.book.id,
|
|
2154
2222
|
...result
|
|
2155
2223
|
};
|
|
2156
2224
|
},
|
|
2157
|
-
[ACCOUNTING_ACTIONS.deleteBook]: (rest) => deleteBook({
|
|
2225
|
+
[ACCOUNTING_ACTIONS.deleteBook]: (rest, root) => deleteBook({
|
|
2158
2226
|
bookId: typeof rest.bookId === "string" ? rest.bookId : "",
|
|
2159
2227
|
confirm: rest.confirm === true
|
|
2160
|
-
}),
|
|
2161
|
-
[ACCOUNTING_ACTIONS.getAccounts]: (rest) => listAccounts({ bookId: optionalString(rest.bookId) }),
|
|
2162
|
-
[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({
|
|
2163
2231
|
bookId: optionalString(rest.bookId),
|
|
2164
2232
|
account: rest.account
|
|
2165
|
-
}),
|
|
2166
|
-
[ACCOUNTING_ACTIONS.addEntries]: (rest) => addEntries({
|
|
2233
|
+
}, root),
|
|
2234
|
+
[ACCOUNTING_ACTIONS.addEntries]: (rest, root) => addEntries({
|
|
2167
2235
|
bookId: optionalString(rest.bookId),
|
|
2168
2236
|
entries: rest.entries
|
|
2169
|
-
}),
|
|
2170
|
-
[ACCOUNTING_ACTIONS.voidEntry]: (rest) => voidEntry({
|
|
2237
|
+
}, root),
|
|
2238
|
+
[ACCOUNTING_ACTIONS.voidEntry]: (rest, root) => voidEntry({
|
|
2171
2239
|
bookId: optionalString(rest.bookId),
|
|
2172
2240
|
entryId: typeof rest.entryId === "string" ? rest.entryId : "",
|
|
2173
2241
|
reason: optionalString(rest.reason),
|
|
2174
2242
|
voidDate: optionalString(rest.voidDate)
|
|
2175
|
-
}),
|
|
2176
|
-
[ACCOUNTING_ACTIONS.getJournalEntries]: (rest) => listEntries({
|
|
2243
|
+
}, root),
|
|
2244
|
+
[ACCOUNTING_ACTIONS.getJournalEntries]: (rest, root) => listEntries({
|
|
2177
2245
|
bookId: optionalString(rest.bookId),
|
|
2178
2246
|
from: optionalString(rest.from),
|
|
2179
2247
|
to: optionalString(rest.to),
|
|
2180
2248
|
accountCode: optionalString(rest.accountCode)
|
|
2181
|
-
}),
|
|
2182
|
-
[ACCOUNTING_ACTIONS.getOpeningBalances]: (rest) => getOpeningBalances({ bookId: optionalString(rest.bookId) }),
|
|
2183
|
-
[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({
|
|
2184
2252
|
bookId: optionalString(rest.bookId),
|
|
2185
2253
|
asOfDate: typeof rest.asOfDate === "string" ? rest.asOfDate : "",
|
|
2186
2254
|
lines: rest.lines ?? [],
|
|
2187
2255
|
memo: optionalString(rest.memo)
|
|
2188
|
-
}),
|
|
2256
|
+
}, root),
|
|
2189
2257
|
[ACCOUNTING_ACTIONS.getReport]: handleGetReport,
|
|
2190
|
-
[ACCOUNTING_ACTIONS.getTimeSeries]: (rest) => getTimeSeriesReport({
|
|
2258
|
+
[ACCOUNTING_ACTIONS.getTimeSeries]: (rest, root) => getTimeSeriesReport({
|
|
2191
2259
|
bookId: optionalString(rest.bookId),
|
|
2192
2260
|
metric: rest.metric,
|
|
2193
2261
|
granularity: rest.granularity,
|
|
2194
2262
|
from: rest.from,
|
|
2195
2263
|
to: rest.to,
|
|
2196
2264
|
accountCode: rest.accountCode
|
|
2197
|
-
}),
|
|
2198
|
-
[ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest) => rebuildSnapshots({ bookId: optionalString(rest.bookId) })
|
|
2265
|
+
}, root),
|
|
2266
|
+
[ACCOUNTING_ACTIONS.rebuildSnapshots]: (rest, root) => rebuildSnapshots({ bookId: optionalString(rest.bookId) }, root)
|
|
2199
2267
|
};
|
|
2200
2268
|
var PREVIEW_ACTIONS = /* @__PURE__ */ new Set([
|
|
2201
2269
|
ACCOUNTING_ACTIONS.openBook,
|
|
@@ -2272,11 +2340,11 @@ function previewMessage(action, fields) {
|
|
|
2272
2340
|
const head = ownEntry(MESSAGE_BUILDERS, action)?.(fields);
|
|
2273
2341
|
return head ? `${head} ${VIEW_VISIBLE_TRAILER}` : VIEW_VISIBLE_TRAILER;
|
|
2274
2342
|
}
|
|
2275
|
-
async function dispatch(body) {
|
|
2343
|
+
async function dispatch(body, root) {
|
|
2276
2344
|
const { action, ...rest } = body;
|
|
2277
2345
|
const handler = ownEntry(ACTION_HANDLERS, action);
|
|
2278
2346
|
if (!handler) throw new AccountingError(400, `unknown action ${JSON.stringify(action)}`);
|
|
2279
|
-
const result = await handler(rest);
|
|
2347
|
+
const result = await handler(rest, root);
|
|
2280
2348
|
const handlerFields = isRecord(result) ? result : { value: result };
|
|
2281
2349
|
const dataField = PREVIEW_ACTIONS.has(action) ? { data: {
|
|
2282
2350
|
action,
|
|
@@ -2293,8 +2361,9 @@ async function dispatch(body) {
|
|
|
2293
2361
|
/** Build the accounting Express router. The host injects its workspace
|
|
2294
2362
|
* root + logger via `configureAccountingServer(...)` and pub/sub via
|
|
2295
2363
|
* `initAccountingEventPublisher(...)`, then mounts the returned router
|
|
2296
|
-
* with `app.use(...)`.
|
|
2297
|
-
|
|
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 = {}) {
|
|
2298
2367
|
const router = Router();
|
|
2299
2368
|
router.post(ACCOUNTING_API.dispatch.path, asyncHandler("accounting", "accounting dispatch failed", async (req, res) => {
|
|
2300
2369
|
const { body } = req;
|
|
@@ -2306,7 +2375,8 @@ function createAccountingRouter() {
|
|
|
2306
2375
|
const { action } = body;
|
|
2307
2376
|
log.info("accounting", "POST dispatch: start", { action });
|
|
2308
2377
|
try {
|
|
2309
|
-
const
|
|
2378
|
+
const root = options.resolveWorkspaceRoot?.(req);
|
|
2379
|
+
const result = await dispatch(body, root);
|
|
2310
2380
|
log.info("accounting", "POST dispatch: ok", { action });
|
|
2311
2381
|
res.json(result);
|
|
2312
2382
|
} catch (err) {
|