@classytic/ledger 0.4.2 → 0.5.1
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 +228 -188
- package/dist/constants/index.d.mts +1 -1
- package/dist/constants/index.mjs +2 -3
- package/dist/country/index.d.mts +1 -1
- package/dist/{journals-BfwnCFam.mjs → currencies-CsuBGfgs.mjs} +80 -1
- package/dist/{date-lock.plugin-DL6pe24p.mjs → date-lock.plugin-B6WyvqNG.mjs} +63 -11
- package/dist/errors-BmRjW38t.mjs +33 -0
- package/dist/exports/index.d.mts +1 -1
- package/dist/exports/index.mjs +1 -1
- package/dist/{fiscal-close-B2_7WMTe.mjs → fiscal-close-Dk3yRT9i.mjs} +14 -4
- package/dist/{idempotency.plugin-zU-GKJ0-.d.mts → idempotency.plugin-WcQLZU9n.d.mts} +38 -0
- package/dist/{index-CxZqRaOU.d.mts → index-GmfEFxVn.d.mts} +1 -1
- package/dist/index.d.mts +525 -344
- package/dist/index.mjs +1868 -170
- package/dist/{journals-DTipb_rz.d.mts → journals-C50E9mpo.d.mts} +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/reports/index.d.mts +1 -1
- package/dist/reports/index.mjs +1 -1
- package/dist/{trial-balance-DcQ0xj_4.d.mts → trial-balance-BZ7yOOFD.d.mts} +16 -4
- package/package.json +1 -11
- package/dist/currencies-W8kQAkm0.mjs +0 -80
- package/dist/engine-scgOvxHJ.d.mts +0 -130
- package/dist/errors-B_dyYZc_.mjs +0 -26
- package/dist/journal-entry.schema-JqrfbvB4.d.mts +0 -103
- package/dist/logger-UbTdBb1x.d.mts +0 -14
- package/dist/reconciliation.repository-D-D_ITL-.d.mts +0 -135
- package/dist/reconciliation.repository-fPwFKvrk.mjs +0 -542
- package/dist/reconciliation.schema-BA1lPv4t.mjs +0 -666
- package/dist/repositories/index.d.mts +0 -2
- package/dist/repositories/index.mjs +0 -2
- package/dist/schemas/index.d.mts +0 -71
- package/dist/schemas/index.mjs +0 -2
- package/dist/tenant-guard-r17Se3Bb.mjs +0 -13
- /package/dist/{categories-DWogBUgQ.mjs → categories-BkKdv16V.mjs} +0 -0
- /package/dist/{core-8Xfnpn6g.d.mts → core-BkGjuVZj.d.mts} +0 -0
- /package/dist/{exports-DoGQQtMQ.mjs → exports-BP-0Ni5W.mjs} +0 -0
- /package/dist/{index-J-XIbXH-.d.mts → index-D1ZjgVxn.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -1,258 +1,298 @@
|
|
|
1
1
|
# @classytic/ledger
|
|
2
2
|
|
|
3
|
-
Embeddable double-entry accounting engine for MongoDB. Integer-cents arithmetic, plugin-based, country-agnostic
|
|
3
|
+
Embeddable double-entry accounting engine for MongoDB. Integer-cents arithmetic, plugin-based, country-agnostic, multi-tenant at every layer. Framework-agnostic — works with Express, Fastify, Nest, Arc, or any plain Mongoose app.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **0.5.1** — Critical plugin-pipeline fixes. `post()`, `unpost()`, `archive()`, and the `reverse()` mark-as-reversed step now route through `repository.update()` so `before:update` / `after:update` hooks fire on every state transition (period locks, audit, observability are no longer silently bypassed). `reverse()` and `duplicate()` propagate every consumer-defined top-level field (`departmentId`, `projectId`, `sourceRef`, `branchTag`, `organizationId`, …). New typed `_ledgerInternal` flag on `RepositoryContext` lets plugin authors observe internal transitions without casts. See [CHANGELOG.md](CHANGELOG.md).
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install @classytic/ledger @classytic/mongokit mongoose
|
|
11
|
-
npm install @classytic/ledger-ca
|
|
12
|
-
npm install @classytic/ledger-bd
|
|
11
|
+
npm install @classytic/ledger-ca # Canada (GIFI, GST/HST, CRA)
|
|
12
|
+
npm install @classytic/ledger-bd # Bangladesh (BFRS, VAT/TDS, Mushak)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
17
|
-
```
|
|
18
|
-
import mongoose from
|
|
19
|
-
import { createAccountingEngine } from
|
|
20
|
-
import { canadaPack } from
|
|
17
|
+
```ts
|
|
18
|
+
import mongoose from "mongoose";
|
|
19
|
+
import { createAccountingEngine } from "@classytic/ledger";
|
|
20
|
+
import { canadaPack } from "@classytic/ledger-ca";
|
|
21
21
|
|
|
22
|
-
// The engine owns the models — matches flow/promo pattern
|
|
23
22
|
const engine = createAccountingEngine({
|
|
24
23
|
mongoose: mongoose.connection,
|
|
25
24
|
country: canadaPack,
|
|
26
|
-
currency:
|
|
27
|
-
multiTenant: { orgField:
|
|
25
|
+
currency: "CAD",
|
|
26
|
+
multiTenant: { orgField: "organizationId", orgRef: "Organization" },
|
|
28
27
|
});
|
|
29
28
|
|
|
30
|
-
// Models, repositories, and reports are auto-created
|
|
31
29
|
await engine.repositories.accounts.seedAccounts(orgId);
|
|
32
|
-
const entry = await engine.repositories.journalEntries.post(entryId, orgId);
|
|
33
|
-
const bs = await engine.reports.balanceSheet({ organizationId: orgId, dateOption: 'year', dateValue: 2025 });
|
|
34
|
-
```
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
await engine.record.sale(orgId, {
|
|
32
|
+
date: new Date("2025-04-01"),
|
|
33
|
+
amount: 10000, // $100.00 in cents (tax-exclusive)
|
|
34
|
+
receivableAccount: "1200", // AR
|
|
35
|
+
revenueAccount: "4010", // Service Revenue
|
|
36
|
+
tax: { code: "HST", account: "2300" },
|
|
37
|
+
label: "INV-001",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const bs = await engine.reports.balanceSheet({
|
|
41
|
+
organizationId: orgId,
|
|
42
|
+
dateOption: "year",
|
|
43
|
+
dateValue: 2025,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
The engine owns the models. After `createAccountingEngine` you have:
|
|
39
48
|
|
|
40
49
|
| Property | What it gives you |
|
|
41
|
-
|
|
42
|
-
| `engine.models.Account
|
|
43
|
-
| `engine.repositories.accounts
|
|
44
|
-
| `engine.repositories.journalEntries
|
|
45
|
-
| `engine.repositories.fiscalPeriods
|
|
46
|
-
| `engine.repositories.reconciliations
|
|
47
|
-
| `engine.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `engine.models.{Account,JournalEntry,FiscalPeriod,Budget,Reconciliation}` | Mongoose models |
|
|
52
|
+
| `engine.repositories.accounts` | `seedAccounts()`, `bulkCreate()` + plugins |
|
|
53
|
+
| `engine.repositories.journalEntries` | `post()`, `unpost()`, `reverse()`, `duplicate()` + double-entry, fiscal-lock, idempotency |
|
|
54
|
+
| `engine.repositories.{fiscalPeriods,budgets}` | Plain CRUD |
|
|
55
|
+
| `engine.repositories.reconciliations` | `reconcile()`, `unreconcile()`, `getUnreconciled()` |
|
|
56
|
+
| `engine.record.*` | Domain verbs (`sale`, `expense`, `transfer`, `payment`, `adjustment`) |
|
|
57
|
+
| `engine.introspect.*` | Runtime catalog of accounts, tax codes, reports |
|
|
58
|
+
| `engine.reports.*` | All 10 reports, bound to owned models |
|
|
59
|
+
|
|
60
|
+
## Semantic Record API
|
|
61
|
+
|
|
62
|
+
Record business operations as domain verbs. The engine resolves account codes, splits tax, and produces a balanced journal entry — you never touch debits/credits.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// Cash sale with 13% HST
|
|
66
|
+
await engine.record.sale(orgId, {
|
|
67
|
+
date, amount: 10000,
|
|
68
|
+
receivableAccount: "1001", revenueAccount: "4010",
|
|
69
|
+
tax: { code: "HST", account: "2300" },
|
|
70
|
+
});
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- Configurable immutability (corrections only via reversal)
|
|
70
|
-
- Multi-tenant isolation at every layer (reports, schemas, repositories)
|
|
71
|
-
- Country packs for localized charts of accounts and tax codes
|
|
72
|
-
- Extensible journal type registry — add domain-specific types (POS, E-Commerce, Payroll) at startup
|
|
73
|
-
|
|
74
|
-
**10 Reports**
|
|
75
|
-
- Trial Balance (3-column: initial + period + ending)
|
|
76
|
-
- Balance Sheet (with computed retained earnings)
|
|
77
|
-
- Income Statement (revenue, COGS, gross profit, operating expenses, net income)
|
|
78
|
-
- General Ledger (per-account with running balances)
|
|
79
|
-
- Cash Flow (Operating / Investing / Financing)
|
|
80
|
-
- Aged Receivable / Payable (configurable buckets: current, 30, 60, 90+)
|
|
81
|
-
- Budget vs Actual (variance analysis)
|
|
82
|
-
- Dimension Breakdown (by department, project, cost center)
|
|
83
|
-
- Foreign Exchange Revaluation (unrealized gain/loss computation)
|
|
84
|
-
- Fiscal Year Close / Reopen (automatic closing entries)
|
|
85
|
-
|
|
86
|
-
**Plugins**
|
|
87
|
-
- `doubleEntryPlugin` — validates debits = credits, account existence, tenant integrity
|
|
88
|
-
- `fiscalLockPlugin` — prevents posting to closed fiscal periods
|
|
89
|
-
- `dateLockPlugin` — blocks entries before a configurable lock date
|
|
90
|
-
- `taxHookPlugin` — auto-generates tax lines via user-defined `TaxLineGenerator`
|
|
91
|
-
- `idempotencyPlugin` — prevents duplicate entries by key
|
|
92
|
-
|
|
93
|
-
**Utilities**
|
|
94
|
-
- `Money` — cents arithmetic, tax splitting, allocation with zero-sum guarantee
|
|
95
|
-
- `buildDimensionFields` — schema helpers for analytic dimensions
|
|
96
|
-
- `suggestMatches` — reconciliation matching suggestions
|
|
97
|
-
- `computeRevaluation` — FX gain/loss computation
|
|
72
|
+
// Expense with recoverable input tax credit
|
|
73
|
+
await engine.record.expense(orgId, {
|
|
74
|
+
date, amount: 3000,
|
|
75
|
+
expenseAccount: "6010", paidFromAccount: "2001",
|
|
76
|
+
tax: { code: "HST_ITC", account: "2400" },
|
|
77
|
+
});
|
|
98
78
|
|
|
99
|
-
|
|
79
|
+
await engine.record.transfer(orgId, { date, amount: 5000, fromAccount: "1001", toAccount: "1002" });
|
|
100
80
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
requireActor: true, // actorId required on post/reverse
|
|
114
|
-
requireApproval: true // entries must be approved before posting
|
|
115
|
-
},
|
|
81
|
+
await engine.record.payment(orgId, {
|
|
82
|
+
date, amount: 11300,
|
|
83
|
+
fromReceivableAccount: "1200", toCashAccount: "1001",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Multi-line adjustment (depreciation, accruals, corrections)
|
|
87
|
+
await engine.record.adjustment(orgId, {
|
|
88
|
+
date, label: "Monthly depreciation",
|
|
89
|
+
lines: [
|
|
90
|
+
{ account: "6030", debit: 1000 },
|
|
91
|
+
{ account: "1500", credit: 1000 },
|
|
92
|
+
],
|
|
116
93
|
});
|
|
117
94
|
```
|
|
118
95
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
await reports.agedBalance({ type: 'receivable', asOfDate: new Date() });
|
|
131
|
-
await reports.budgetVsActual({ ... }); // requires Budget model
|
|
132
|
-
await reports.dimensionBreakdown({ dimension: 'departmentId', ... });
|
|
133
|
-
await reports.revaluation({ rates: [{ currency: 'USD', rate: 1.40 }], ... });
|
|
96
|
+
All verbs accept `options.user`, `options.session`, `options.idempotencyKey`, plus any custom field — they all flow into mongokit's `RepositoryContext` so audit/observability plugins (and your hooks) pick them up automatically.
|
|
97
|
+
|
|
98
|
+
## Introspection
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const catalog = await engine.introspect.catalog(orgId);
|
|
102
|
+
// { accounts, journalTypes, reports, taxCodes, fiscalPeriods }
|
|
103
|
+
|
|
104
|
+
engine.introspect.accounts(orgId);
|
|
105
|
+
engine.introspect.taxCodes("ON");
|
|
106
|
+
engine.introspect.reports(); // sync — static catalog
|
|
134
107
|
```
|
|
135
108
|
|
|
136
|
-
|
|
109
|
+
## Structured Validation Errors
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
try {
|
|
113
|
+
await engine.record.sale(orgId, { ... });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err instanceof AccountingError) {
|
|
116
|
+
err.status // 400 | 403 | 404 | 409
|
|
117
|
+
err.code // 'VALIDATION_ERROR' | 'NOT_FOUND' | ...
|
|
118
|
+
err.fields // [{ path, issue, value }, ...]
|
|
119
|
+
err.toJSON();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
137
123
|
|
|
138
|
-
|
|
124
|
+
Field errors come straight from plugins (double-entry, fiscal-lock) and the semantic layer.
|
|
125
|
+
|
|
126
|
+
## Audit, Observability & Framework Integration
|
|
127
|
+
|
|
128
|
+
The ledger is **framework-agnostic** and operates at the model layer — Express, Fastify, Nest, Hono, and Arc all work the same way. Three places you can hook in, composable:
|
|
129
|
+
|
|
130
|
+
1. **Any mongokit plugin** via `config.plugins` — see the `@classytic/mongokit` docs for `auditTrailPlugin`, `observabilityPlugin`, and others.
|
|
131
|
+
```ts
|
|
132
|
+
const engine = createAccountingEngine({
|
|
133
|
+
mongoose: mongoose.connection, country: canadaPack, currency: "CAD",
|
|
134
|
+
plugins: {
|
|
135
|
+
journalEntry: [/* your mongokit plugins */],
|
|
136
|
+
account: [/* your mongokit plugins */],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
2. **Runtime listeners** — no plugin needed:
|
|
141
|
+
```ts
|
|
142
|
+
engine.repositories.journalEntries.on("after:create", ({ context, result }) => {
|
|
143
|
+
auditLog.write({ userId: context.user?._id, orgId: context.organizationId, entryId: result._id });
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
3. **Your framework's own audit (HTTP-level)** — e.g. `@classytic/arc`'s `auditPlugin` records resource CRUD with request context. Use it alone, or combine with a model-layer plugin.
|
|
147
|
+
|
|
148
|
+
| Layer | What it sees | What it misses |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| HTTP middleware (Arc / Express / Nest) | Request → user, IP, route, payload | Background jobs, CLI scripts, anything bypassing HTTP |
|
|
151
|
+
| Model-layer mongokit plugin | Every collection write, regardless of caller | HTTP context unless the caller forwards it |
|
|
152
|
+
|
|
153
|
+
For accounting compliance most teams want **both** — HTTP audit for traffic, model audit on `journalEntry` for an immutable trail. Forward request context on the call so both layers see it:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// Express
|
|
157
|
+
app.post("/sales", async (req, res) => {
|
|
158
|
+
await engine.record.sale(req.body.orgId, req.body, {
|
|
159
|
+
user: req.user, ip: req.ip, userAgent: req.headers["user-agent"],
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
```
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
## Reports
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
await engine.reports.trialBalance({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
168
|
+
await engine.reports.balanceSheet({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
169
|
+
await engine.reports.incomeStatement({ organizationId, dateOption: "quarter", dateValue: { year: 2025, quarter: 2 } });
|
|
170
|
+
await engine.reports.generalLedger({ organizationId, dateOption: "month", dateValue: { year: 2025, month: 4 } });
|
|
171
|
+
await engine.reports.cashFlow({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
172
|
+
await engine.reports.agedBalance({ organizationId, type: "receivable", asOfDate: new Date() });
|
|
173
|
+
await engine.reports.budgetVsActual({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
174
|
+
await engine.reports.dimensionBreakdown({ organizationId, dimension: "departmentId", dateOption: "year", dateValue: 2025 });
|
|
175
|
+
await engine.reports.revaluation({ organizationId, asOfDate: new Date(), rates: [{ currency: "USD", rate: 1.40 }], unrealizedGainLossAccountId });
|
|
148
176
|
```
|
|
149
177
|
|
|
150
|
-
|
|
178
|
+
All values are integer cents. Use `Money.toDecimal()` at your API boundary.
|
|
151
179
|
|
|
152
|
-
|
|
153
|
-
import { dateLockPlugin, taxHookPlugin } from '@classytic/ledger';
|
|
180
|
+
The 10 reports:
|
|
154
181
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
- **Trial Balance** (3-column: opening + period + ending)
|
|
183
|
+
- **Balance Sheet** (with computed retained earnings, multi-year aware)
|
|
184
|
+
- **Income Statement** (revenue, COGS, gross profit, operating expenses, net income)
|
|
185
|
+
- **General Ledger** (per-account with running balances)
|
|
186
|
+
- **Cash Flow** (operating / investing / financing)
|
|
187
|
+
- **Aged Receivable / Payable** (configurable buckets)
|
|
188
|
+
- **Budget vs Actual** (variance analysis)
|
|
189
|
+
- **Dimension Breakdown** (by department, project, cost center)
|
|
190
|
+
- **FX Revaluation** (unrealized gain/loss)
|
|
191
|
+
- **Fiscal Year Close / Reopen** (automatic closing entries)
|
|
192
|
+
|
|
193
|
+
## Engine Configuration
|
|
160
194
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
195
|
+
```ts
|
|
196
|
+
createAccountingEngine({
|
|
197
|
+
mongoose: mongoose.connection, // required
|
|
198
|
+
country: canadaPack, // required
|
|
199
|
+
currency: "CAD", // required — base/functional currency
|
|
200
|
+
multiTenant: { orgField, orgRef }, // optional
|
|
201
|
+
multiCurrency: { enabled: true, currencies: ["USD", "EUR"] },
|
|
202
|
+
fiscalYearStartMonth: 1, // 1=Jan (default), 4=Apr, 7=Jul
|
|
203
|
+
retainedEarningsAccountCode: "3600", // overrides country pack
|
|
204
|
+
modelNames: { account: "GLAccount", ... }, // custom collection names
|
|
205
|
+
schemaOptions: { // extra fields/indexes per model
|
|
206
|
+
journalEntry: {
|
|
207
|
+
extraFields: { aiJob: { status: String, generatedAt: Date } },
|
|
208
|
+
extraIndexes: [{ fields: { "aiJob.status": 1 }, options: { sparse: true } }],
|
|
168
209
|
},
|
|
169
210
|
},
|
|
211
|
+
strictness: {
|
|
212
|
+
immutable: true, // disable unpost — corrections only via reverse
|
|
213
|
+
requireActor: true, // actorId required on post/reverse
|
|
214
|
+
requireApproval: true, // entries must be approved before posting
|
|
215
|
+
},
|
|
216
|
+
plugins: { journalEntry: [...], account: [...] }, // any mongokit plugins
|
|
217
|
+
pagination: { account: { maxLimit: 5000 } }, // optional caps; no default cap
|
|
170
218
|
});
|
|
171
219
|
```
|
|
172
220
|
|
|
173
|
-
|
|
221
|
+
`pagination` has **no default cap** — large enterprise charts of accounts can be tens of thousands of rows. Pass `{ maxLimit: N }` per repository if you want to bound list queries.
|
|
174
222
|
|
|
175
|
-
|
|
176
|
-
|------|----------|
|
|
177
|
-
| `@classytic/ledger` | Engine, Money, all schemas, plugins, reports, types |
|
|
178
|
-
| `@classytic/ledger/money` | `Money` class |
|
|
179
|
-
| `@classytic/ledger/schemas` | Schema factories |
|
|
180
|
-
| `@classytic/ledger/reports` | Report generators |
|
|
181
|
-
| `@classytic/ledger/plugins` | All plugins |
|
|
182
|
-
| `@classytic/ledger/repositories` | Repository wiring |
|
|
183
|
-
| `@classytic/ledger/exports` | CSV export + QuickBooks field maps |
|
|
184
|
-
| `@classytic/ledger/country` | `defineCountryPack`, `CountryPack` interface |
|
|
185
|
-
| `@classytic/ledger/constants` | Categories, journal types (+ registry), currencies |
|
|
186
|
-
|
|
187
|
-
## Extensible Journal Types
|
|
188
|
-
|
|
189
|
-
The 15 built-in journal types (SALES, PURCHASES, GENERAL, PAYROLL, etc.) cover standard accounting. For domain-specific needs, register custom types **before** schema creation:
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
import { registerJournalType, getJournalTypeCodes, isValidJournalType } from '@classytic/ledger';
|
|
193
|
-
|
|
194
|
-
// Register at startup, before createJournalEntrySchema()
|
|
195
|
-
registerJournalType('POS_SALES', {
|
|
196
|
-
code: 'POS_SALES',
|
|
197
|
-
name: 'POS Sales Journal',
|
|
198
|
-
description: 'Daily aggregated point-of-sale transactions',
|
|
199
|
-
});
|
|
223
|
+
## Built-in Plugins
|
|
200
224
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
| Plugin | Purpose |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `doubleEntryPlugin` | Validates debits = credits, account existence, tenant integrity |
|
|
228
|
+
| `fiscalLockPlugin` | Prevents posting to closed fiscal periods |
|
|
229
|
+
| `dateLockPlugin` | Blocks entries before a configurable lock date |
|
|
230
|
+
| `taxHookPlugin` | Auto-generates tax lines via a `TaxLineGenerator` |
|
|
231
|
+
| `idempotencyPlugin` | Prevents duplicate entries by key |
|
|
206
232
|
|
|
207
|
-
|
|
208
|
-
isValidJournalType('POS_SALES'); // true
|
|
209
|
-
getJournalTypeCodes(); // [...15 built-in, 'POS_SALES', 'ECOM_SALES']
|
|
233
|
+
`doubleEntryPlugin`, `fiscalLockPlugin` and `idempotencyPlugin` are wired automatically by the engine. The others are opt-in via the second `createAccountingEngine` argument.
|
|
210
234
|
|
|
211
|
-
|
|
235
|
+
## Custom Journal Types
|
|
236
|
+
|
|
237
|
+
The 15 built-in journal types (SALES, PURCHASES, GENERAL, PAYROLL, …) cover standard accounting. Register custom types **before** the first engine call:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { registerJournalType } from "@classytic/ledger";
|
|
241
|
+
|
|
242
|
+
registerJournalType("POS_SALES", { code: "POS_SALES", name: "POS Sales Journal" });
|
|
243
|
+
registerJournalType("ECOM_SALES", { code: "ECOM_SALES", name: "E-Commerce Sales" });
|
|
212
244
|
```
|
|
213
245
|
|
|
214
|
-
|
|
246
|
+
Reference numbers use the type prefix (`POS_SALES/2025/03/0001`). The registry freezes after the first schema is created.
|
|
215
247
|
|
|
216
248
|
## Country Packs
|
|
217
249
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
import { defineCountryPack } from '@classytic/ledger';
|
|
250
|
+
```ts
|
|
251
|
+
import { defineCountryPack } from "@classytic/ledger";
|
|
222
252
|
|
|
223
253
|
export const myPack = defineCountryPack({
|
|
224
|
-
code:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
taxCodes: { /* your tax codes */ },
|
|
230
|
-
taxCodesByRegion: {},
|
|
231
|
-
regions: [],
|
|
254
|
+
code: "US", name: "United States", defaultCurrency: "USD",
|
|
255
|
+
retainedEarningsAccountCode: "3200",
|
|
256
|
+
accountTypes: [/* chart of accounts */],
|
|
257
|
+
taxCodes: {/* tax codes */},
|
|
258
|
+
taxCodesByRegion: {}, regions: [],
|
|
232
259
|
});
|
|
233
260
|
```
|
|
234
261
|
|
|
235
|
-
Available
|
|
262
|
+
Available: `@classytic/ledger-ca` (Canada), `@classytic/ledger-bd` (Bangladesh).
|
|
263
|
+
|
|
264
|
+
## Subpath Exports
|
|
265
|
+
|
|
266
|
+
| Path | Contents |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `@classytic/ledger` | Engine, Money, plugins, reports, types |
|
|
269
|
+
| `@classytic/ledger/money` | `Money` class |
|
|
270
|
+
| `@classytic/ledger/reports` | Standalone report generators |
|
|
271
|
+
| `@classytic/ledger/plugins` | All plugins |
|
|
272
|
+
| `@classytic/ledger/exports` | CSV export + QuickBooks field maps |
|
|
273
|
+
| `@classytic/ledger/country` | `defineCountryPack`, `CountryPack` |
|
|
274
|
+
| `@classytic/ledger/constants` | Categories, journal types, currencies |
|
|
236
275
|
|
|
237
276
|
## Testing
|
|
238
277
|
|
|
239
278
|
```bash
|
|
240
|
-
npm test
|
|
241
|
-
npx vitest run tests/e2e/
|
|
242
|
-
npx vitest run tests/scenarios/
|
|
243
|
-
npx vitest run tests/hardening/
|
|
279
|
+
npm test # 1273 tests, 67 files
|
|
280
|
+
npx vitest run tests/e2e/ # full-year scenarios
|
|
281
|
+
npx vitest run tests/scenarios/ # integration scenarios
|
|
282
|
+
npx vitest run tests/hardening/ # edge cases & invariants
|
|
244
283
|
```
|
|
245
284
|
|
|
246
|
-
|
|
247
|
-
|
|
285
|
+
Coverage includes:
|
|
286
|
+
|
|
287
|
+
- Canadian small-business full-year lifecycle (open → post → close → reopen)
|
|
288
|
+
- Multi-year fiscal cycles with retained-earnings rollover
|
|
248
289
|
- Multi-currency trading with FX revaluation
|
|
249
290
|
- Multi-tenant report isolation (org A cannot see org B)
|
|
250
|
-
-
|
|
251
|
-
- Reversal
|
|
291
|
+
- All 10 reports with month / quarter / year / custom date ranges
|
|
292
|
+
- Reversal and correction workflows
|
|
252
293
|
- Custom journal type registry → schema → posting pipeline
|
|
253
|
-
- Double-entry conservation
|
|
294
|
+
- Double-entry conservation across all entries
|
|
254
295
|
- Money arithmetic hardening (overflow, penny-leak, float traps)
|
|
255
|
-
- Public API surface & subpath export verification
|
|
256
296
|
- O-Level / A-Level / university textbook accounting problems
|
|
257
297
|
|
|
258
298
|
## Requirements
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { S as isValidCategory, _ as getCategoryMainType, a as getJournalTypeCodes, b as isBalanceSheet, c as CURRENCIES, d as isValidCurrency, f as CATEGORIES, g as extractStatementType, h as extractMainType, i as getJournalType, l as getCurrency, m as categoryKey, n as JOURNAL_TYPES, o as isValidJournalType, p as CATEGORY_KEYS, r as getCustomJournalTypes, s as registerJournalType, t as JOURNAL_CODES, u as getMinorUnit, v as getCategoryStatementType, x as isIncomeStatement, y as getNormalBalance } from "../journals-
|
|
1
|
+
import { S as isValidCategory, _ as getCategoryMainType, a as getJournalTypeCodes, b as isBalanceSheet, c as CURRENCIES, d as isValidCurrency, f as CATEGORIES, g as extractStatementType, h as extractMainType, i as getJournalType, l as getCurrency, m as categoryKey, n as JOURNAL_TYPES, o as isValidJournalType, p as CATEGORY_KEYS, r as getCustomJournalTypes, s as registerJournalType, t as JOURNAL_CODES, u as getMinorUnit, v as getCategoryStatementType, x as isIncomeStatement, y as getNormalBalance } from "../journals-C50E9mpo.mjs";
|
|
2
2
|
export { CATEGORIES, CATEGORY_KEYS, CURRENCIES, JOURNAL_CODES, JOURNAL_TYPES, categoryKey, extractMainType, extractStatementType, getCategoryMainType, getCategoryStatementType, getCurrency, getCustomJournalTypes, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, registerJournalType };
|
package/dist/constants/index.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as extractStatementType, c as getNormalBalance, d as isValidCategory, i as extractMainType, l as isBalanceSheet, n as CATEGORY_KEYS, o as getCategoryMainType, r as categoryKey, s as getCategoryStatementType, t as CATEGORIES, u as isIncomeStatement } from "../categories-
|
|
3
|
-
import { i as isValidCurrency, n as getCurrency, r as getMinorUnit, t as CURRENCIES } from "../currencies-W8kQAkm0.mjs";
|
|
1
|
+
import { a as JOURNAL_CODES, c as getCustomJournalTypes, d as isValidJournalType, f as registerJournalType, i as isValidCurrency, l as getJournalType, n as getCurrency, o as JOURNAL_TYPES, r as getMinorUnit, t as CURRENCIES, u as getJournalTypeCodes } from "../currencies-CsuBGfgs.mjs";
|
|
2
|
+
import { a as extractStatementType, c as getNormalBalance, d as isValidCategory, i as extractMainType, l as isBalanceSheet, n as CATEGORY_KEYS, o as getCategoryMainType, r as categoryKey, s as getCategoryStatementType, t as CATEGORIES, u as isIncomeStatement } from "../categories-BkKdv16V.mjs";
|
|
4
3
|
export { CATEGORIES, CATEGORY_KEYS, CURRENCIES, JOURNAL_CODES, JOURNAL_TYPES, categoryKey, extractMainType, extractStatementType, getCategoryMainType, getCategoryStatementType, getCurrency, getCustomJournalTypes, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, registerJournalType };
|
package/dist/country/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as TaxReportLine, i as TaxCodesByRegion, n as CountryPackInput, o as TaxReportTemplate, r as TaxCode, s as defineCountryPack, t as CountryPack } from "../index-
|
|
1
|
+
import { a as TaxReportLine, i as TaxCodesByRegion, n as CountryPackInput, o as TaxReportTemplate, r as TaxCode, s as defineCountryPack, t as CountryPack } from "../index-GmfEFxVn.mjs";
|
|
2
2
|
export { CountryPack, CountryPackInput, TaxCode, TaxCodesByRegion, TaxReportLine, TaxReportTemplate, defineCountryPack };
|
|
@@ -110,4 +110,83 @@ function getJournalType(code) {
|
|
|
110
110
|
return JOURNAL_TYPES[code] ?? _customTypes[code] ?? null;
|
|
111
111
|
}
|
|
112
112
|
//#endregion
|
|
113
|
-
|
|
113
|
+
//#region src/constants/currencies.ts
|
|
114
|
+
const CURRENCIES = Object.freeze({
|
|
115
|
+
CAD: {
|
|
116
|
+
code: "CAD",
|
|
117
|
+
name: "Canadian Dollar",
|
|
118
|
+
symbol: "$",
|
|
119
|
+
minorUnit: 2
|
|
120
|
+
},
|
|
121
|
+
USD: {
|
|
122
|
+
code: "USD",
|
|
123
|
+
name: "US Dollar",
|
|
124
|
+
symbol: "$",
|
|
125
|
+
minorUnit: 2
|
|
126
|
+
},
|
|
127
|
+
GBP: {
|
|
128
|
+
code: "GBP",
|
|
129
|
+
name: "British Pound",
|
|
130
|
+
symbol: "£",
|
|
131
|
+
minorUnit: 2
|
|
132
|
+
},
|
|
133
|
+
EUR: {
|
|
134
|
+
code: "EUR",
|
|
135
|
+
name: "Euro",
|
|
136
|
+
symbol: "€",
|
|
137
|
+
minorUnit: 2
|
|
138
|
+
},
|
|
139
|
+
JPY: {
|
|
140
|
+
code: "JPY",
|
|
141
|
+
name: "Japanese Yen",
|
|
142
|
+
symbol: "¥",
|
|
143
|
+
minorUnit: 0
|
|
144
|
+
},
|
|
145
|
+
AUD: {
|
|
146
|
+
code: "AUD",
|
|
147
|
+
name: "Australian Dollar",
|
|
148
|
+
symbol: "$",
|
|
149
|
+
minorUnit: 2
|
|
150
|
+
},
|
|
151
|
+
CHF: {
|
|
152
|
+
code: "CHF",
|
|
153
|
+
name: "Swiss Franc",
|
|
154
|
+
symbol: "CHF",
|
|
155
|
+
minorUnit: 2
|
|
156
|
+
},
|
|
157
|
+
INR: {
|
|
158
|
+
code: "INR",
|
|
159
|
+
name: "Indian Rupee",
|
|
160
|
+
symbol: "₹",
|
|
161
|
+
minorUnit: 2
|
|
162
|
+
},
|
|
163
|
+
BDT: {
|
|
164
|
+
code: "BDT",
|
|
165
|
+
name: "Bangladeshi Taka",
|
|
166
|
+
symbol: "৳",
|
|
167
|
+
minorUnit: 2
|
|
168
|
+
},
|
|
169
|
+
AED: {
|
|
170
|
+
code: "AED",
|
|
171
|
+
name: "UAE Dirham",
|
|
172
|
+
symbol: "د.إ",
|
|
173
|
+
minorUnit: 2
|
|
174
|
+
},
|
|
175
|
+
SAR: {
|
|
176
|
+
code: "SAR",
|
|
177
|
+
name: "Saudi Riyal",
|
|
178
|
+
symbol: "﷼",
|
|
179
|
+
minorUnit: 2
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
function getCurrency(code) {
|
|
183
|
+
return CURRENCIES[code] ?? null;
|
|
184
|
+
}
|
|
185
|
+
function isValidCurrency(code) {
|
|
186
|
+
return code in CURRENCIES;
|
|
187
|
+
}
|
|
188
|
+
function getMinorUnit(code) {
|
|
189
|
+
return CURRENCIES[code]?.minorUnit ?? 2;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
export { JOURNAL_CODES as a, getCustomJournalTypes as c, isValidJournalType as d, registerJournalType as f, isValidCurrency as i, getJournalType as l, getCurrency as n, JOURNAL_TYPES as o, getMinorUnit as r, _freezeJournalTypes as s, CURRENCIES as t, getJournalTypeCodes as u };
|