@classytic/ledger 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +33 -0
- package/dist/index.mjs +44 -17
- package/package.json +5 -2
package/dist/index.d.mts
CHANGED
|
@@ -280,6 +280,39 @@ interface AccountingEngineConfig {
|
|
|
280
280
|
currency: string;
|
|
281
281
|
/** Multi-tenant configuration. Omit for single-tenant apps. */
|
|
282
282
|
multiTenant?: MultiTenantConfig | undefined;
|
|
283
|
+
/**
|
|
284
|
+
* Field name used to stamp every journal entry with the originating
|
|
285
|
+
* organization / branch — *without* scoping the chart of accounts or any
|
|
286
|
+
* other collection. Use this for single-company-multi-branch deployments
|
|
287
|
+
* where Account / FiscalPeriod stay company-wide but each posting needs a
|
|
288
|
+
* branch attribution for partition-style reports (per-branch P&L, AR aging,
|
|
289
|
+
* partner ledger).
|
|
290
|
+
*
|
|
291
|
+
* When `multiTenant` is set, that takes precedence — `multiTenant.tenantField`
|
|
292
|
+
* already provides the same stamp and additionally scopes every repository.
|
|
293
|
+
*
|
|
294
|
+
* The host MUST declare the field on the JournalEntry schema via
|
|
295
|
+
* `schemaOptions.journalEntry.extraFields.<field>` (the engine doesn't add
|
|
296
|
+
* the schema path for you — keeps schema mutation explicit).
|
|
297
|
+
*
|
|
298
|
+
* Example:
|
|
299
|
+
*
|
|
300
|
+
* createAccountingEngine({
|
|
301
|
+
* // ...no multiTenant — accounts are company-wide
|
|
302
|
+
* journalEntryOrgField: 'organizationId',
|
|
303
|
+
* schemaOptions: {
|
|
304
|
+
* journalEntry: {
|
|
305
|
+
* extraFields: {
|
|
306
|
+
* organizationId: { type: ObjectId, ref: 'organization', default: null, index: true },
|
|
307
|
+
* },
|
|
308
|
+
* },
|
|
309
|
+
* },
|
|
310
|
+
* });
|
|
311
|
+
*
|
|
312
|
+
* await engine.record.sale(branchId, { ... });
|
|
313
|
+
* // → JE doc: { organizationId: branchId, ... }
|
|
314
|
+
*/
|
|
315
|
+
journalEntryOrgField?: string | undefined;
|
|
283
316
|
/** Multi-currency support. Omit for single-currency apps. */
|
|
284
317
|
multiCurrency?: MultiCurrencyConfig | undefined;
|
|
285
318
|
/** Fiscal year start month (1-12, default: 1 = January) */
|
package/dist/index.mjs
CHANGED
|
@@ -909,6 +909,14 @@ async function safePublish$3(events, outboxStore, type, payload, ctx) {
|
|
|
909
909
|
await events.publish(event);
|
|
910
910
|
} catch {}
|
|
911
911
|
}
|
|
912
|
+
function isDuplicateKeyBulkError(err) {
|
|
913
|
+
if (!err || typeof err !== "object") return false;
|
|
914
|
+
const e = err;
|
|
915
|
+
if (e.code === 11e3) return true;
|
|
916
|
+
if (Array.isArray(e.writeErrors) && e.writeErrors.length > 0) return true;
|
|
917
|
+
if (e.status === 409 && e.duplicate !== void 0) return true;
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
912
920
|
/**
|
|
913
921
|
* Wire seedAccounts, bulkCreate and posting-account validation
|
|
914
922
|
* onto an existing mongokit Repository.
|
|
@@ -1075,24 +1083,42 @@ function wireAccountMethods(repository, country, orgField, integrations = {}) {
|
|
|
1075
1083
|
_id: inserted[idx]._id
|
|
1076
1084
|
}));
|
|
1077
1085
|
} catch (err) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
_id:
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1086
|
+
if (!isDuplicateKeyBulkError(err)) throw err;
|
|
1087
|
+
const insertedDocs = err.insertedDocs ?? [];
|
|
1088
|
+
const insertedNumbers = new Set(insertedDocs.map((d) => d.accountNumber));
|
|
1089
|
+
const stillUnknown = toCreate.filter((t) => !insertedNumbers.has(t.accountNumber));
|
|
1090
|
+
const concurrentlyPersistedById = /* @__PURE__ */ new Map();
|
|
1091
|
+
if (stillUnknown.length > 0) {
|
|
1092
|
+
const concurrentFilter = { accountNumber: { $in: stillUnknown.map((t) => t.accountNumber) } };
|
|
1093
|
+
if (orgField && orgId != null) concurrentFilter[orgField] = orgId;
|
|
1094
|
+
const persisted = await repository.findAll(concurrentFilter, {
|
|
1095
|
+
select: {
|
|
1096
|
+
_id: 1,
|
|
1097
|
+
accountNumber: 1
|
|
1098
|
+
},
|
|
1099
|
+
lean: true
|
|
1100
|
+
});
|
|
1101
|
+
for (const p of persisted) concurrentlyPersistedById.set(p.accountNumber, p._id);
|
|
1102
|
+
}
|
|
1103
|
+
for (const item of toCreate) if (insertedNumbers.has(item.accountNumber)) {
|
|
1104
|
+
const iDoc = insertedDocs.find((d) => d.accountNumber === item.accountNumber);
|
|
1105
|
+
results.created.push({
|
|
1092
1106
|
accountTypeCode: item.accountTypeCode,
|
|
1093
|
-
|
|
1107
|
+
active: item.active,
|
|
1108
|
+
isCashAccount: item.isCashAccount,
|
|
1109
|
+
_id: iDoc?._id
|
|
1094
1110
|
});
|
|
1095
|
-
} else
|
|
1111
|
+
} else if (concurrentlyPersistedById.has(item.accountNumber)) results.skipped.push({
|
|
1112
|
+
index: item.index,
|
|
1113
|
+
accountTypeCode: item.accountTypeCode,
|
|
1114
|
+
reason: "Already exists (concurrent insert)",
|
|
1115
|
+
_id: concurrentlyPersistedById.get(item.accountNumber)
|
|
1116
|
+
});
|
|
1117
|
+
else results.skipped.push({
|
|
1118
|
+
index: item.index,
|
|
1119
|
+
accountTypeCode: item.accountTypeCode,
|
|
1120
|
+
reason: "Already exists (concurrent insert)"
|
|
1121
|
+
});
|
|
1096
1122
|
}
|
|
1097
1123
|
}
|
|
1098
1124
|
const summary = {
|
|
@@ -2346,6 +2372,7 @@ function buildIntrospectAPI({ models, country, config }) {
|
|
|
2346
2372
|
function buildRecordAPI({ models, repositories, config }) {
|
|
2347
2373
|
const AccountModel = models.Account;
|
|
2348
2374
|
const orgField = config.multiTenant?.tenantField;
|
|
2375
|
+
const journalTagField = orgField ?? config.journalEntryOrgField;
|
|
2349
2376
|
const resolveAccounts = async (organizationId, codes, path, session) => {
|
|
2350
2377
|
const unique = Array.from(new Set(codes));
|
|
2351
2378
|
const filter = { accountTypeCode: { $in: unique } };
|
|
@@ -2374,7 +2401,7 @@ function buildRecordAPI({ models, repositories, config }) {
|
|
|
2374
2401
|
}]);
|
|
2375
2402
|
};
|
|
2376
2403
|
const postEntry = async (organizationId, payload, options) => {
|
|
2377
|
-
if (
|
|
2404
|
+
if (journalTagField && organizationId != null) payload[journalTagField] = organizationId;
|
|
2378
2405
|
const actorId = options?.actorId ?? (options?.user ? options.user._id?.toString() ?? options.user.id?.toString() : void 0);
|
|
2379
2406
|
if (actorId) {
|
|
2380
2407
|
payload.createdBy = actorId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/ledger",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Production-grade double-entry accounting engine for MongoDB — schemas, reports, tax, multi-tenant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@biomejs/biome": "^2.4.12",
|
|
110
|
+
"@classytic/dev-tools": "^0.2.0",
|
|
110
111
|
"@classytic/fin-io": ">=0.1.0",
|
|
111
112
|
"@classytic/mongokit": ">=3.11.0",
|
|
112
113
|
"@classytic/primitives": ">=0.1.0",
|
|
@@ -139,8 +140,10 @@
|
|
|
139
140
|
"format": "biome format src/ --write",
|
|
140
141
|
"check": "biome ci src/ --diagnostic-level=error",
|
|
141
142
|
"knip": "knip",
|
|
143
|
+
"push": "classytic-push",
|
|
142
144
|
"smoke": "node scripts/smoke.mjs",
|
|
143
145
|
"prepublishOnly": "npm run check && npm run build && npm run typecheck && npm test && npm run smoke",
|
|
144
|
-
"release": "
|
|
146
|
+
"release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
|
|
147
|
+
"release": "npm run push -- main && npm run release:tag && npm publish"
|
|
145
148
|
}
|
|
146
149
|
}
|