@classytic/ledger 0.9.0 → 0.10.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 +94 -348
- package/dist/bridges/index.d.mts +2 -2
- package/dist/{errors-BI5k4iak.mjs → errors-vXd932rB.mjs} +1 -1
- package/dist/events/index.d.mts +3 -2
- package/dist/events/index.mjs +2 -2
- package/dist/{fx-realization.plugin-Bxlb8cIx.mjs → fx-realization.plugin-Dzqzi3u0.mjs} +1 -1
- package/dist/{index-dqkjgpII.d.mts → index-Bl0gP9lD.d.mts} +69 -4
- package/dist/{index-Db0n_6Z8.d.mts → index-ClLwzNRF.d.mts} +3 -3
- package/dist/index.d.mts +100 -74
- package/dist/index.mjs +195 -198
- package/dist/outbox-store-BbKdQ2eT.mjs +253 -0
- package/dist/outbox-store-BcCiHMPw.d.mts +305 -0
- package/dist/{partner-ledger-BoebloHk.mjs → partner-ledger-CR0geilx.mjs} +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/reports/index.mjs +1 -1
- package/dist/sync/index.d.mts +13 -2
- package/dist/sync/index.mjs +6 -3
- package/package.json +23 -14
- package/dist/outbox-store-DQbL-KYT.mjs +0 -132
- package/dist/outbox-store-UYC4eZpI.d.mts +0 -249
package/README.md
CHANGED
|
@@ -1,412 +1,158 @@
|
|
|
1
1
|
# @classytic/ledger
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
> **0.7.0 (BREAKING)** — `@classytic/ledger` is now a **pure double-entry accounting engine**. Tax computation, return templates, repartition, and exigibility have been removed from the core and country-pack contracts and now live in dedicated tax packages (`@classytic/bd-tax` is the existing reference; `@classytic/ca-tax` will follow). Country packs `@classytic/ledger-bd@0.2.0` and `@classytic/ledger-ca@0.2.0` ship the chart of accounts + journal templates only — they re-export the raw tax data tables as named constants for tax engines to lift. The 0.6.x A/P + A/R primitives (item-level matching, partner ledger, credit limit, FX realization, journal resource, open-item queries) are unchanged. See [CHANGELOG.md](CHANGELOG.md).
|
|
3
|
+
Double-entry accounting engine for MongoDB. Integer-cents arithmetic, plugin-based, country-agnostic, multi-tenant. Framework-agnostic — works with Fastify, Express, Nest, or plain Mongoose.
|
|
6
4
|
|
|
7
5
|
## Install
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
8
|
npm install @classytic/ledger @classytic/mongokit mongoose
|
|
11
|
-
npm install @classytic/ledger-ca # Canada (GIFI chart of accounts)
|
|
12
9
|
npm install @classytic/ledger-bd # Bangladesh (BFRS chart of accounts)
|
|
13
10
|
```
|
|
14
11
|
|
|
15
12
|
## Quick Start
|
|
16
13
|
|
|
17
14
|
```ts
|
|
18
|
-
import mongoose from
|
|
19
|
-
import { createAccountingEngine } from
|
|
20
|
-
import {
|
|
15
|
+
import mongoose from 'mongoose';
|
|
16
|
+
import { createAccountingEngine } from '@classytic/ledger';
|
|
17
|
+
import { bangladeshPack } from '@classytic/ledger-bd';
|
|
21
18
|
|
|
22
19
|
const engine = createAccountingEngine({
|
|
23
20
|
mongoose: mongoose.connection,
|
|
24
|
-
country:
|
|
25
|
-
currency:
|
|
26
|
-
multiTenant: { orgField:
|
|
21
|
+
country: bangladeshPack,
|
|
22
|
+
currency: 'BDT',
|
|
23
|
+
multiTenant: { orgField: 'organizationId', orgRef: 'organization' },
|
|
27
24
|
});
|
|
28
25
|
|
|
26
|
+
// Seed chart of accounts for a branch
|
|
29
27
|
await engine.repositories.accounts.seedAccounts(orgId);
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const bs = await engine.reports.balanceSheet({
|
|
40
|
-
organizationId: orgId,
|
|
41
|
-
dateOption: "year",
|
|
42
|
-
dateValue: 2025,
|
|
43
|
-
});
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
The engine owns the models. After `createAccountingEngine` you have:
|
|
47
|
-
|
|
48
|
-
| Property | What it gives you |
|
|
49
|
-
| --- | --- |
|
|
50
|
-
| `engine.models.{Account,JournalEntry,FiscalPeriod,Budget,Reconciliation,Journal}` | Mongoose models |
|
|
51
|
-
| `engine.repositories.accounts` | `seedAccounts()`, `bulkCreate()` + plugins |
|
|
52
|
-
| `engine.repositories.journalEntries` | `post()`, `unpost()`, `reverse()`, `duplicate()` + double-entry, fiscal-lock, idempotency |
|
|
53
|
-
| `engine.repositories.journals` | First-class posting channels — `seedDefaults()`, `nextSequenceNumber()` |
|
|
54
|
-
| `engine.repositories.reconciliations` | Item-level matching — `match()`, `unmatch()`, `getOpenItems()` |
|
|
55
|
-
| `engine.repositories.{fiscalPeriods,budgets}` | Plain CRUD |
|
|
56
|
-
| `engine.record.*` | Domain verbs (`sale`, `expense`, `transfer`, `payment`, `adjustment`) |
|
|
57
|
-
| `engine.introspect.*` | Runtime catalog of accounts, journal types, reports, fiscal periods |
|
|
58
|
-
| `engine.reports.*` | All 12 reports, bound to owned models |
|
|
59
|
-
|
|
60
|
-
## Semantic Record API
|
|
61
|
-
|
|
62
|
-
Record business operations as domain verbs. The engine resolves account codes and produces a balanced journal entry — you never touch debits/credits.
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
await engine.record.sale(orgId, {
|
|
66
|
-
date, amount: 10000,
|
|
67
|
-
receivableAccount: "1001", revenueAccount: "4010",
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
await engine.record.expense(orgId, {
|
|
71
|
-
date, amount: 3000,
|
|
72
|
-
expenseAccount: "6010", paidFromAccount: "2001",
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await engine.record.transfer(orgId, { date, amount: 5000, fromAccount: "1001", toAccount: "1002" });
|
|
76
|
-
|
|
77
|
-
await engine.record.payment(orgId, {
|
|
78
|
-
date, amount: 11300,
|
|
79
|
-
fromReceivableAccount: "1200", toCashAccount: "1001",
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Multi-line adjustment (depreciation, accruals, corrections)
|
|
83
|
-
await engine.record.adjustment(orgId, {
|
|
84
|
-
date, label: "Monthly depreciation",
|
|
85
|
-
lines: [
|
|
86
|
-
{ account: "6030", debit: 1000 },
|
|
87
|
-
{ account: "1500", credit: 1000 },
|
|
29
|
+
// Post a journal entry
|
|
30
|
+
const entry = await engine.repositories.journalEntries.create({
|
|
31
|
+
journalType: 'GENERAL',
|
|
32
|
+
date: new Date(),
|
|
33
|
+
label: 'Office supplies',
|
|
34
|
+
journalItems: [
|
|
35
|
+
{ account: expenseAccountId, debit: 500_00, credit: 0 },
|
|
36
|
+
{ account: cashAccountId, debit: 0, credit: 500_00 },
|
|
88
37
|
],
|
|
89
38
|
});
|
|
39
|
+
await engine.repositories.journalEntries.post(entry._id, orgId);
|
|
90
40
|
```
|
|
91
41
|
|
|
92
|
-
|
|
42
|
+
## Multi-Currency (0.9.0+)
|
|
93
43
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
## Accounts Payable & Receivable
|
|
97
|
-
|
|
98
|
-
The 0.6.x A/P + A/R primitives are the foundation for any ERP workflow on top of the ledger.
|
|
44
|
+
GL stays in base currency (BDT). Foreign currency is audit metadata.
|
|
99
45
|
|
|
100
46
|
```ts
|
|
101
|
-
// Tag every journal item with a partnerId via extraItemFields (one-time setup)
|
|
102
47
|
const engine = createAccountingEngine({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
},
|
|
48
|
+
country: bangladeshPack,
|
|
49
|
+
currency: 'BDT',
|
|
50
|
+
multiCurrency: { enabled: true, currencies: ['USD', 'EUR', 'GBP'] },
|
|
51
|
+
bridges: {
|
|
52
|
+
exchangeRate: myRateBridge, // host-injected rate source
|
|
110
53
|
},
|
|
111
54
|
});
|
|
112
55
|
|
|
113
|
-
// Post
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
date: new Date(
|
|
56
|
+
// Post with foreign currency metadata
|
|
57
|
+
await engine.repositories.journalEntries.create({
|
|
58
|
+
journalType: 'PURCHASES',
|
|
59
|
+
date: new Date(),
|
|
60
|
+
label: 'Import from China',
|
|
117
61
|
journalItems: [
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
journalItems: [
|
|
128
|
-
{ account: cashId, debit: 40_000 },
|
|
129
|
-
{ account: arId, credit: 40_000, partnerId: "wholesale-1" },
|
|
130
|
-
],
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Match the AR sides — partial settlement
|
|
134
|
-
await engine.repositories.reconciliations.match({
|
|
135
|
-
account: arId,
|
|
136
|
-
items: [
|
|
137
|
-
{ entry: invoice._id, itemIndex: 0 },
|
|
138
|
-
{ entry: payment._id, itemIndex: 1 },
|
|
62
|
+
{
|
|
63
|
+
account: inventoryId,
|
|
64
|
+
debit: 120_500_00, // BDT (base currency, always)
|
|
65
|
+
credit: 0,
|
|
66
|
+
currency: 'USD',
|
|
67
|
+
originalDebit: 1_000_00, // USD 1,000.00
|
|
68
|
+
exchangeRate: 120.50,
|
|
69
|
+
},
|
|
70
|
+
{ account: apId, debit: 0, credit: 120_500_00 },
|
|
139
71
|
],
|
|
140
72
|
});
|
|
141
|
-
|
|
142
|
-
// Open items for this partner (subsidiary ledger)
|
|
143
|
-
await engine.repositories.reconciliations.getOpenItems({
|
|
144
|
-
accountId: arId,
|
|
145
|
-
filter: { partnerId: "wholesale-1" },
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Customer statement with running balance + aged buckets
|
|
149
|
-
import { generatePartnerLedger } from "@classytic/ledger";
|
|
150
|
-
await generatePartnerLedger(
|
|
151
|
-
{ AccountModel: engine.models.Account, JournalEntryModel: engine.models.JournalEntry },
|
|
152
|
-
{
|
|
153
|
-
controlAccountId: arId,
|
|
154
|
-
partnerId: "wholesale-1",
|
|
155
|
-
startDate: new Date("2026-01-01"),
|
|
156
|
-
endDate: new Date("2026-03-31"),
|
|
157
|
-
},
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// Cross-partner aged A/R buckets
|
|
161
|
-
import { generateAgedBalance } from "@classytic/ledger";
|
|
162
|
-
await generateAgedBalance(
|
|
163
|
-
{ AccountModel, JournalEntryModel, country: canadaPack },
|
|
164
|
-
{ type: "receivable", contactField: "journalItems.partnerId" },
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// Enforce per-customer credit limits
|
|
168
|
-
import { creditLimitPlugin } from "@classytic/ledger/plugins";
|
|
169
|
-
creditLimitPlugin({
|
|
170
|
-
arControlAccountId: arId,
|
|
171
|
-
JournalEntryModel: engine.models.JournalEntry,
|
|
172
|
-
getCreditLimit: async (partnerId) => Customer.findById(partnerId).then(c => c?.creditLimit ?? null),
|
|
173
|
-
}).apply(engine.repositories.journalEntries);
|
|
174
73
|
```
|
|
175
74
|
|
|
176
|
-
##
|
|
75
|
+
## Features
|
|
177
76
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
await engine.record.sale(orgId, { ... });
|
|
191
|
-
} catch (err) {
|
|
192
|
-
if (err instanceof AccountingError) {
|
|
193
|
-
err.status // 400 | 402 | 403 | 404 | 409
|
|
194
|
-
err.code // 'VALIDATION_ERROR' | 'NOT_FOUND' | 'CREDIT_LIMIT_EXCEEDED' | 'PERIOD_LOCKED_FISCAL' | ...
|
|
195
|
-
err.fields // [{ path, issue, value }, ...]
|
|
196
|
-
err.toJSON();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
## Audit, Observability & Framework Integration
|
|
202
|
-
|
|
203
|
-
Every operation flows through mongokit's `RepositoryContext`. Custom plugins can hook `before:create` / `after:create` / `before:update` / `after:update` / `after:match` to add audit trails, metrics, webhooks, or business rules — none of it is hardcoded into the core.
|
|
204
|
-
|
|
205
|
-
The `_ledgerInternal` context flag (`'post' | 'unpost' | 'archive' | 'reverseMark' | 'fxRealize'`) tells plugins which engine operation is in flight, so guards (locks, credit limit, immutability) can exempt legitimate engine writes without affecting consumer code.
|
|
77
|
+
| Feature | What it does |
|
|
78
|
+
|---------|-------------|
|
|
79
|
+
| **Double-entry** | `doubleEntryPlugin` validates debit = credit on every post |
|
|
80
|
+
| **Integer cents** | All amounts in minor units (paisa/cents). No float errors. |
|
|
81
|
+
| **Multi-tenant** | `organizationId` scoping via mongokit plugin |
|
|
82
|
+
| **Multi-currency** | Optional foreign currency fields + FX realization + revaluation |
|
|
83
|
+
| **Idempotency** | `idempotencyPlugin` prevents duplicate postings |
|
|
84
|
+
| **Period locking** | `createLockPlugin` blocks edits to closed periods |
|
|
85
|
+
| **Credit limits** | `creditLimitPlugin` enforces per-partner credit caps |
|
|
86
|
+
| **Immutable guard** | `immutableGuardPlugin` prevents posted entry edits |
|
|
87
|
+
| **Country packs** | Pluggable chart of accounts (BD, CA, custom) |
|
|
206
88
|
|
|
207
89
|
## Reports
|
|
208
90
|
|
|
209
|
-
12 typed reports, all multi-tenant scoped, all returning structured JSON ready for any UI:
|
|
210
|
-
|
|
211
|
-
| Report | Purpose |
|
|
212
|
-
| --- | --- |
|
|
213
|
-
| `trialBalance` | Debits/credits per account with running balances |
|
|
214
|
-
| `balanceSheet` | Assets, liabilities, equity at a date with computed retained earnings |
|
|
215
|
-
| `incomeStatement` | Revenue, COGS, expenses, net income for a period |
|
|
216
|
-
| `generalLedger` | Per-account transaction detail with running balance |
|
|
217
|
-
| `cashFlow` | Operating / investing / financing breakdown |
|
|
218
|
-
| `agedBalance` | A/R or A/P bucketed by age, optionally per partner |
|
|
219
|
-
| `partnerLedger` | Supplier/customer statement with opening + running balance + aged buckets |
|
|
220
|
-
| `dimensionBreakdown` | Group by department/project/cost center |
|
|
221
|
-
| `budgetVsActual` | Variance vs budget per account/period |
|
|
222
|
-
| `revaluation` | Foreign-currency unrealized FX gain/loss at a date |
|
|
223
|
-
| `closeFiscalPeriod` / `reopenFiscalPeriod` | Year-end close pipeline |
|
|
224
|
-
|
|
225
|
-
## Engine Configuration
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
const engine = createAccountingEngine({
|
|
229
|
-
mongoose: mongoose.connection,
|
|
230
|
-
country: canadaPack,
|
|
231
|
-
currency: "CAD",
|
|
232
|
-
multiTenant: { orgField: "organizationId", orgRef: "Organization" },
|
|
233
|
-
multiCurrency: { enabled: true, currencies: ["USD", "EUR"] },
|
|
234
|
-
fiscalYearStartMonth: 1,
|
|
235
|
-
idempotency: true,
|
|
236
|
-
strictness: { immutable: true, requireActor: true },
|
|
237
|
-
schemaOptions: {
|
|
238
|
-
journalEntry: {
|
|
239
|
-
extraItemFields: {
|
|
240
|
-
partnerId: { type: String, index: true },
|
|
241
|
-
departmentId: { type: mongoose.Schema.Types.ObjectId },
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## Built-in Plugins
|
|
249
|
-
|
|
250
|
-
| Plugin | Purpose |
|
|
251
|
-
| --- | --- |
|
|
252
|
-
| `doubleEntryPlugin` | Validates debits = credits, account existence, tenant integrity, posted-entry immutability |
|
|
253
|
-
| `fiscalLockPlugin` | Prevents posting into closed fiscal periods (auto-wired) |
|
|
254
|
-
| `dailyLockPlugin` | Per-branch `lastClosedDate` watermark for daily POS close |
|
|
255
|
-
| `createLockPlugin` | Generic lock factory — compose your own scopes (bank recon, payroll, tax filings) |
|
|
256
|
-
| `idempotencyPlugin` | Prevents duplicate entries by key (auto-wired when `idempotency: true`) |
|
|
257
|
-
| `creditLimitPlugin` | Per-partner A/R credit limit enforcement |
|
|
258
|
-
| `fxRealizationPlugin` | Books realized FX gain/loss when matched items have different exchange rates |
|
|
259
|
-
|
|
260
|
-
`doubleEntryPlugin`, `fiscalLockPlugin`, and `idempotencyPlugin` (when enabled) are wired automatically by the engine. The others are opt-in via `.apply(engine.repositories.journalEntries)` or `.apply(engine.repositories.reconciliations)`.
|
|
261
|
-
|
|
262
|
-
## Custom Journal Types
|
|
263
|
-
|
|
264
|
-
The 15 built-in journal types (SALES, PURCHASES, GENERAL, PAYROLL, …) cover standard accounting. Register custom types **before** the first engine call:
|
|
265
|
-
|
|
266
|
-
```ts
|
|
267
|
-
import { registerJournalType } from "@classytic/ledger";
|
|
268
|
-
|
|
269
|
-
registerJournalType("POS_SALES", { code: "POS_SALES", name: "POS Sales Journal" });
|
|
270
|
-
registerJournalType("ECOM_SALES", { code: "ECOM_SALES", name: "E-Commerce Sales" });
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
Reference numbers use the type prefix (`POS_SALES/2025/03/0001`). The registry freezes after the first schema is created.
|
|
274
|
-
|
|
275
|
-
## Country Packs
|
|
276
|
-
|
|
277
|
-
A country pack ships the **chart of accounts** + accounting conventions for a jurisdiction. Tax (VAT/GST/HST/income-tax) lives in separate tax packages — see "Tax" below.
|
|
278
|
-
|
|
279
91
|
```ts
|
|
280
|
-
import {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
],
|
|
292
|
-
});
|
|
92
|
+
import {
|
|
93
|
+
generateTrialBalance,
|
|
94
|
+
generateBalanceSheet,
|
|
95
|
+
generateIncomeStatement,
|
|
96
|
+
generateCashFlow,
|
|
97
|
+
generateGeneralLedger,
|
|
98
|
+
generateAgedBalance,
|
|
99
|
+
generateBudgetVsActual,
|
|
100
|
+
generatePartnerLedger,
|
|
101
|
+
generateRevaluation,
|
|
102
|
+
} from '@classytic/ledger';
|
|
293
103
|
```
|
|
294
104
|
|
|
295
|
-
|
|
105
|
+
All reports accept `{ startDate, endDate, organizationId }` and return typed result objects.
|
|
296
106
|
|
|
297
|
-
##
|
|
107
|
+
## Bridges
|
|
298
108
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
For tax computation, return generation, and repartition:
|
|
302
|
-
|
|
303
|
-
- **`@classytic/bd-tax`** — Bangladesh income tax + VAT compute, IT-11GA forms, Mushak 9.1 returns, deduction optimizer, depreciation
|
|
304
|
-
- **`@classytic/ca-tax`** *(planned)* — Canadian GST/HST/PST/QST compute, CRA GST34 form, ITC tracking
|
|
305
|
-
- **Or your own** — tax engines just call `engine.repositories.journalEntries.create()` with the tax line items they want posted
|
|
306
|
-
|
|
307
|
-
The country packs (`ledger-bd`, `ledger-ca`) still re-export their raw tax data tables (`TAX_CODES`, `TAX_CODES_BY_REGION`, `mushakReturnTemplate`, `craReturnTemplate`) as named exports so tax packages can lift them — they're just no longer wired into the `CountryPack` contract.
|
|
308
|
-
|
|
309
|
-
## Invoice Engine Integration
|
|
310
|
-
|
|
311
|
-
Wire `@classytic/invoice` to the ledger with one call — no manual journal wiring needed:
|
|
109
|
+
All optional. All methods optional. Features degrade gracefully.
|
|
312
110
|
|
|
313
111
|
```ts
|
|
314
|
-
import {
|
|
315
|
-
import { createLedgerBridge } from "@classytic/ledger/sync";
|
|
316
|
-
import { createInvoiceEngine } from "@classytic/invoice";
|
|
317
|
-
import { canadaPack } from "@classytic/ledger-ca";
|
|
318
|
-
|
|
319
|
-
const accounting = createAccountingEngine({
|
|
320
|
-
mongoose: mongoose.connection,
|
|
321
|
-
country: canadaPack,
|
|
322
|
-
currency: "CAD",
|
|
323
|
-
multiTenant: { orgField: "organizationId", orgRef: "Organization" },
|
|
324
|
-
idempotency: true,
|
|
325
|
-
});
|
|
112
|
+
import type { ExchangeRateBridge, SourceBridge, NotificationBridge } from '@classytic/ledger';
|
|
326
113
|
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
expense: "5000", // Expenses
|
|
335
|
-
taxPayable: "2100", // Tax Payable
|
|
336
|
-
taxReceivable: "1150", // Tax Receivable
|
|
337
|
-
cash: "1000", // Cash / Bank
|
|
338
|
-
},
|
|
339
|
-
}),
|
|
114
|
+
const engine = createAccountingEngine({
|
|
115
|
+
// ...
|
|
116
|
+
bridges: {
|
|
117
|
+
exchangeRate: myRateBridge, // FX rate lookup
|
|
118
|
+
source: mySourceBridge, // resolve external doc refs
|
|
119
|
+
notification: myNotifBridge, // alert on reversals, period locks
|
|
120
|
+
},
|
|
340
121
|
});
|
|
341
122
|
```
|
|
342
123
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
For custom subledgers (inventory, payroll, etc.) that don't use `@classytic/invoice`, see [docs/subledger-integration.md](docs/subledger-integration.md) for the manual `PostingContract` pattern.
|
|
346
|
-
|
|
347
|
-
## URL-Driven Queries
|
|
348
|
-
|
|
349
|
-
Parse URL query parameters directly into paginated repository queries via mongokit's `QueryParser`:
|
|
124
|
+
## Plugins
|
|
350
125
|
|
|
351
126
|
```ts
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
127
|
+
import {
|
|
128
|
+
doubleEntryPlugin,
|
|
129
|
+
idempotencyPlugin,
|
|
130
|
+
creditLimitPlugin,
|
|
131
|
+
fxRealizationPlugin,
|
|
132
|
+
immutableGuardPlugin,
|
|
133
|
+
createLockPlugin,
|
|
134
|
+
} from '@classytic/ledger';
|
|
360
135
|
```
|
|
361
136
|
|
|
362
|
-
|
|
137
|
+
Plugins attach to mongokit repository hooks. They run at POLICY priority before any query.
|
|
363
138
|
|
|
364
139
|
## Subpath Exports
|
|
365
140
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
| `@classytic/ledger/reports` | Standalone report generators |
|
|
372
|
-
| `@classytic/ledger/plugins` | All plugins |
|
|
373
|
-
| `@classytic/ledger/exports` | CSV export + QuickBooks field maps |
|
|
374
|
-
| `@classytic/ledger/country` | `defineCountryPack`, `CountryPack` |
|
|
375
|
-
| `@classytic/ledger/constants` | Categories, journal types, currencies |
|
|
376
|
-
|
|
377
|
-
## Testing
|
|
378
|
-
|
|
379
|
-
```bash
|
|
380
|
-
npm test # 1327 tests, 77 files
|
|
381
|
-
npm run smoke # full pipeline against published dist/
|
|
382
|
-
npx vitest run tests/e2e/ # end-to-end scenarios
|
|
383
|
-
npx vitest run tests/scenarios/ # multi-step business scenarios
|
|
384
|
-
npx vitest run tests/hardening/ # edge cases & invariants
|
|
141
|
+
```ts
|
|
142
|
+
import { Money } from '@classytic/ledger/money';
|
|
143
|
+
import { CATEGORIES, CURRENCIES } from '@classytic/ledger/constants';
|
|
144
|
+
import { defineCountryPack } from '@classytic/ledger/country';
|
|
145
|
+
import { exportToCsv, quickbooksFieldMap } from '@classytic/ledger/exports';
|
|
385
146
|
```
|
|
386
147
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
- Canadian small-business full-year lifecycle (open → post → close → reopen)
|
|
390
|
-
- Multi-year fiscal cycles with retained-earnings rollover
|
|
391
|
-
- Multi-currency trading with realized + unrealized FX
|
|
392
|
-
- Multi-tenant report isolation (org A cannot see org B)
|
|
393
|
-
- All 12 reports with month / quarter / year / custom date ranges
|
|
394
|
-
- Reversal and correction workflows
|
|
395
|
-
- Custom journal type registry → schema → posting pipeline
|
|
396
|
-
- Item-level matching: 1-to-1, 1-to-many, partial settlement, unmatch
|
|
397
|
-
- Per-partner credit limit enforcement + reversal exemption
|
|
398
|
-
- FX realization plugin auto-booking gain/loss on cross-rate match
|
|
399
|
-
- Full ERP A/P + A/R cycle (bill receipt → match → supplier statement → aged balance)
|
|
400
|
-
- Double-entry conservation across all entries
|
|
401
|
-
- Money arithmetic hardening (overflow, penny-leak, float traps)
|
|
402
|
-
- O-Level / A-Level / university textbook accounting problems
|
|
403
|
-
|
|
404
|
-
## Requirements
|
|
148
|
+
## Architecture
|
|
405
149
|
|
|
406
|
-
-
|
|
407
|
-
-
|
|
408
|
-
-
|
|
409
|
-
-
|
|
150
|
+
- Repositories extend `@classytic/mongokit` Repository directly
|
|
151
|
+
- No service layer — domain verbs live on the repository
|
|
152
|
+
- No barrel re-exports — import from source paths
|
|
153
|
+
- Events: arc-compatible `DomainEvent` / `EventTransport` shapes
|
|
154
|
+
- Country packs: pluggable chart of accounts + journal type seeds
|
|
155
|
+
- Tax: NOT in ledger. Use `@classytic/bd-tax` for Bangladesh tax calculations.
|
|
410
156
|
|
|
411
157
|
## License
|
|
412
158
|
|
package/dist/bridges/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as EntryReversedNotification, c as PeriodLockedNotification, i as SourceRef, l as ReconciliationMismatchNotification, n as SourceBridge, o as NotificationBridge, r as SourceBridgeContext, s as NotificationBridgeContext, t as LedgerBridges } from "../index-
|
|
2
|
-
export { EntryReversedNotification, LedgerBridges, NotificationBridge, NotificationBridgeContext, PeriodLockedNotification, ReconciliationMismatchNotification, SourceBridge, SourceBridgeContext, SourceRef };
|
|
1
|
+
import { a as EntryReversedNotification, c as PeriodLockedNotification, i as SourceRef, l as ReconciliationMismatchNotification, n as SourceBridge, o as NotificationBridge, r as SourceBridgeContext, s as NotificationBridgeContext, t as LedgerBridges, u as ExchangeRateBridge } from "../index-Bl0gP9lD.mjs";
|
|
2
|
+
export { EntryReversedNotification, ExchangeRateBridge, LedgerBridges, NotificationBridge, NotificationBridgeContext, PeriodLockedNotification, ReconciliationMismatchNotification, SourceBridge, SourceBridgeContext, SourceRef };
|
|
@@ -109,7 +109,7 @@ function classifyDuplicateKey(err) {
|
|
|
109
109
|
let keyPattern = e.keyPattern;
|
|
110
110
|
if (!keyPattern && Array.isArray(e.writeErrors)) keyPattern = e.writeErrors.find((w) => w?.code === 11e3)?.keyPattern;
|
|
111
111
|
if (!keyPattern && wrappedMsgMatch) {
|
|
112
|
-
const fields = wrappedMsgMatch[1]
|
|
112
|
+
const fields = wrappedMsgMatch[1]?.split(",").map((f) => f.trim()).filter(Boolean);
|
|
113
113
|
keyPattern = Object.fromEntries(fields.map((f) => [f, 1]));
|
|
114
114
|
}
|
|
115
115
|
return {
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
|
|
1
|
+
import { $ as ReconciliationUnmatchedPayload, A as LedgerEventDefinition, B as InProcessLedgerBusOptions, C as EntryPostedPayloadSchema, D as EntryUnpostedPayloadSchema, E as EntryUnposted, F as ReconciliationUnmatched, G as EntryArchivedPayload, H as createEvent, I as ReconciliationUnmatchedPayloadSchema, J as EntryPostedPayload, K as EntryCreatedPayload, L as ledgerEventDefinitions, M as LedgerEventSchema, N as ReconciliationMatched, O as JournalSeeded, P as ReconciliationMatchedPayloadSchema, Q as ReconciliationMatchedPayload, R as EventLogger, S as EntryPosted, T as EntryReversedPayloadSchema, U as AccountBulkCreatedPayload, V as EventContext, W as AccountSeededPayload, X as EntryUnpostedPayload, Y as EntryReversedPayload, Z as JournalSeededPayload, _ as EntryArchivedPayloadSchema, a as OutboxFailOptions, b as EntryDuplicated, c as OutboxFailurePolicy, d as OutboxWriteOptions, et as LEDGER_EVENTS, f as AccountBulkCreated, g as EntryArchived, h as AccountSeededPayloadSchema, i as OutboxErrorInfo, j as LedgerEventPayloadOf, k as JournalSeededPayloadSchema, l as OutboxOwnershipError, m as AccountSeeded, n as OutboxAcknowledgeOptions, o as OutboxFailureContext, p as AccountBulkCreatedPayloadSchema, q as EntryDuplicatedPayload, r as OutboxClaimOptions, s as OutboxFailureDecision, t as InvalidOutboxEventError, tt as LedgerEventName, u as OutboxStore, v as EntryCreated, w as EntryReversed, x as EntryDuplicatedPayloadSchema, y as EntryCreatedPayloadSchema, z as InProcessLedgerBus } from "../outbox-store-BcCiHMPw.mjs";
|
|
2
|
+
import { DomainEvent, EventHandler, EventTransport, PublishManyResult } from "@classytic/primitives/events";
|
|
3
|
+
export { AccountBulkCreated, type AccountBulkCreatedPayload, type AccountBulkCreatedPayloadSchema, AccountSeeded, type AccountSeededPayload, type AccountSeededPayloadSchema, type DomainEvent, EntryArchived, type EntryArchivedPayload, type EntryArchivedPayloadSchema, EntryCreated, type EntryCreatedPayload, type EntryCreatedPayloadSchema, EntryDuplicated, type EntryDuplicatedPayload, type EntryDuplicatedPayloadSchema, EntryPosted, type EntryPostedPayload, type EntryPostedPayloadSchema, EntryReversed, type EntryReversedPayload, type EntryReversedPayloadSchema, EntryUnposted, type EntryUnpostedPayload, type EntryUnpostedPayloadSchema, type EventContext, type EventHandler, type EventLogger, type EventTransport, InProcessLedgerBus, type InProcessLedgerBusOptions, InvalidOutboxEventError, JournalSeeded, type JournalSeededPayload, type JournalSeededPayloadSchema, LEDGER_EVENTS, type LedgerEventDefinition, type LedgerEventName, type LedgerEventPayloadOf, type LedgerEventSchema, type OutboxAcknowledgeOptions, type OutboxClaimOptions, type OutboxErrorInfo, type OutboxFailOptions, type OutboxFailureContext, type OutboxFailureDecision, type OutboxFailurePolicy, OutboxOwnershipError, type OutboxStore, type OutboxWriteOptions, type PublishManyResult, ReconciliationMatched, type ReconciliationMatchedPayload, type ReconciliationMatchedPayloadSchema, ReconciliationUnmatched, type ReconciliationUnmatchedPayload, type ReconciliationUnmatchedPayloadSchema, createEvent, ledgerEventDefinitions };
|
package/dist/events/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { InProcessLedgerBus, LEDGER_EVENTS, OutboxOwnershipError, createEvent };
|
|
1
|
+
import { _ as LEDGER_EVENTS, a as EntryArchived, c as EntryPosted, d as JournalSeeded, f as ReconciliationMatched, g as createEvent, h as InProcessLedgerBus, i as AccountSeeded, l as EntryReversed, m as ledgerEventDefinitions, n as OutboxOwnershipError, o as EntryCreated, p as ReconciliationUnmatched, r as AccountBulkCreated, s as EntryDuplicated, t as InvalidOutboxEventError, u as EntryUnposted } from "../outbox-store-BbKdQ2eT.mjs";
|
|
2
|
+
export { AccountBulkCreated, AccountSeeded, EntryArchived, EntryCreated, EntryDuplicated, EntryPosted, EntryReversed, EntryUnposted, InProcessLedgerBus, InvalidOutboxEventError, JournalSeeded, LEDGER_EVENTS, OutboxOwnershipError, ReconciliationMatched, ReconciliationUnmatched, createEvent, ledgerEventDefinitions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as IdempotencyConflictError, i as Errors, t as AccountingError } from "./errors-
|
|
1
|
+
import { a as IdempotencyConflictError, i as Errors, t as AccountingError } from "./errors-vXd932rB.mjs";
|
|
2
2
|
//#region src/plugins/double-entry.plugin.ts
|
|
3
3
|
function doubleEntryPlugin(options = {}) {
|
|
4
4
|
const { onlyOnPost = true, JournalEntryModel, AccountModel, orgField } = options;
|
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
//#region src/bridges/exchange-rate.bridge.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Exchange Rate Bridge — host-injected rate sourcing.
|
|
4
|
+
*
|
|
5
|
+
* The ledger never hardcodes where exchange rates come from. The host
|
|
6
|
+
* provides an implementation at engine init time:
|
|
7
|
+
*
|
|
8
|
+
* - ManualRateBridge: user-entered Currency Exchange records
|
|
9
|
+
* - BangladeshBankRateBridge: BB daily rate API
|
|
10
|
+
* - FixedRateBridge: hardcoded for testing
|
|
11
|
+
* - ProviderRateBridge: third-party rate API (Wise, Open Exchange Rates)
|
|
12
|
+
*
|
|
13
|
+
* When no bridge is provided, the caller must supply the exchange rate
|
|
14
|
+
* explicitly on every journal item (the existing behavior). The bridge
|
|
15
|
+
* is a convenience for hosts that want automatic rate lookup.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const engine = createAccountingEngine({
|
|
20
|
+
* country: bdPack,
|
|
21
|
+
* currency: 'BDT',
|
|
22
|
+
* multiCurrency: { enabled: true, currencies: ['USD', 'EUR'] },
|
|
23
|
+
* bridges: {
|
|
24
|
+
* exchangeRate: {
|
|
25
|
+
* async getRate(from, to, date) {
|
|
26
|
+
* const row = await CurrencyExchange.findOne({ from, to, date });
|
|
27
|
+
* if (!row) throw new Error(`No rate for ${from}→${to} on ${date}`);
|
|
28
|
+
* return row.rate;
|
|
29
|
+
* },
|
|
30
|
+
* },
|
|
31
|
+
* },
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
interface ExchangeRateBridge {
|
|
36
|
+
/**
|
|
37
|
+
* Get the exchange rate for converting `fromCurrency` to `toCurrency`
|
|
38
|
+
* on a given date.
|
|
39
|
+
*
|
|
40
|
+
* @param fromCurrency ISO 4217 code (e.g., 'USD')
|
|
41
|
+
* @param toCurrency ISO 4217 code (e.g., 'BDT')
|
|
42
|
+
* @param date Transaction date (rate as of this date)
|
|
43
|
+
* @param purpose Optional: 'buying' or 'selling' rate (some central
|
|
44
|
+
* banks publish separate rates)
|
|
45
|
+
* @returns The rate as a positive decimal. Example: 120.50 means
|
|
46
|
+
* 1 USD = 120.50 BDT.
|
|
47
|
+
* @throws When no rate is available for the requested pair + date.
|
|
48
|
+
*/
|
|
49
|
+
getRate(fromCurrency: string, toCurrency: string, date: Date, purpose?: 'buying' | 'selling'): Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* Optional batch lookup — avoids N+1 when posting multi-line entries
|
|
52
|
+
* with different currencies. Host implementations that cache rates
|
|
53
|
+
* benefit from a single DB round-trip.
|
|
54
|
+
*
|
|
55
|
+
* When absent, the engine falls back to calling `getRate()` per item.
|
|
56
|
+
*/
|
|
57
|
+
getRates?(requests: Array<{
|
|
58
|
+
fromCurrency: string;
|
|
59
|
+
toCurrency: string;
|
|
60
|
+
date: Date;
|
|
61
|
+
purpose?: 'buying' | 'selling';
|
|
62
|
+
}>): Promise<Map<string, number>>;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
1
65
|
//#region src/bridges/notification.bridge.d.ts
|
|
2
66
|
/**
|
|
3
67
|
* NotificationBridge — host-implemented delivery for ledger-originated alerts.
|
|
@@ -95,10 +159,11 @@ interface SourceBridge {
|
|
|
95
159
|
}
|
|
96
160
|
//#endregion
|
|
97
161
|
//#region src/bridges/index.d.ts
|
|
98
|
-
/** Collected bridges exposed as `engine.bridges`. */
|
|
162
|
+
/** Collected bridges exposed as `engine.bridges`. All optional per PACKAGE_RULES §23. */
|
|
99
163
|
interface LedgerBridges {
|
|
100
|
-
source?: SourceBridge;
|
|
101
|
-
notification?: NotificationBridge;
|
|
164
|
+
source?: SourceBridge | undefined;
|
|
165
|
+
notification?: NotificationBridge | undefined;
|
|
166
|
+
exchangeRate?: ExchangeRateBridge | undefined;
|
|
102
167
|
}
|
|
103
168
|
//#endregion
|
|
104
|
-
export { EntryReversedNotification as a, PeriodLockedNotification as c, SourceRef as i, ReconciliationMismatchNotification as l, SourceBridge as n, NotificationBridge as o, SourceBridgeContext as r, NotificationBridgeContext as s, LedgerBridges as t };
|
|
169
|
+
export { EntryReversedNotification as a, PeriodLockedNotification as c, SourceRef as i, ReconciliationMismatchNotification as l, SourceBridge as n, NotificationBridge as o, SourceBridgeContext as r, NotificationBridgeContext as s, LedgerBridges as t, ExchangeRateBridge as u };
|