@classytic/ledger 0.4.2 → 0.5.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 +227 -189
- 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-B2Jy0ukX.mjs} +61 -10
- 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/{index-CxZqRaOU.d.mts → index-GmfEFxVn.d.mts} +1 -1
- package/dist/index.d.mts +525 -344
- package/dist/index.mjs +1814 -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/{idempotency.plugin-zU-GKJ0-.d.mts → idempotency.plugin-CK7LHnBn.d.mts} +0 -0
- /package/dist/{index-J-XIbXH-.d.mts → index-D1ZjgVxn.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -1,258 +1,296 @@
|
|
|
1
1
|
# @classytic/ledger
|
|
2
2
|
|
|
3
|
-
Embeddable double-entry accounting engine for MongoDB. Integer-cents arithmetic, plugin-based, country-agnostic
|
|
4
|
-
|
|
5
|
-
Build QuickBooks, Xero, or TaxCycle-grade apps — the engine handles the accounting, you handle the UX.
|
|
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.
|
|
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
|
|
12
|
-
npm install @classytic/ledger-bd
|
|
9
|
+
npm install @classytic/ledger-ca # Canada (GIFI, GST/HST, CRA)
|
|
10
|
+
npm install @classytic/ledger-bd # Bangladesh (BFRS, VAT/TDS, Mushak)
|
|
13
11
|
```
|
|
14
12
|
|
|
15
13
|
## Quick Start
|
|
16
14
|
|
|
17
|
-
```
|
|
18
|
-
import mongoose from
|
|
19
|
-
import { createAccountingEngine } from
|
|
20
|
-
import { canadaPack } from
|
|
15
|
+
```ts
|
|
16
|
+
import mongoose from "mongoose";
|
|
17
|
+
import { createAccountingEngine } from "@classytic/ledger";
|
|
18
|
+
import { canadaPack } from "@classytic/ledger-ca";
|
|
21
19
|
|
|
22
|
-
// The engine owns the models — matches flow/promo pattern
|
|
23
20
|
const engine = createAccountingEngine({
|
|
24
21
|
mongoose: mongoose.connection,
|
|
25
22
|
country: canadaPack,
|
|
26
|
-
currency:
|
|
27
|
-
multiTenant: { orgField:
|
|
23
|
+
currency: "CAD",
|
|
24
|
+
multiTenant: { orgField: "organizationId", orgRef: "Organization" },
|
|
28
25
|
});
|
|
29
26
|
|
|
30
|
-
// Models, repositories, and reports are auto-created
|
|
31
27
|
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
28
|
|
|
36
|
-
|
|
29
|
+
await engine.record.sale(orgId, {
|
|
30
|
+
date: new Date("2025-04-01"),
|
|
31
|
+
amount: 10000, // $100.00 in cents (tax-exclusive)
|
|
32
|
+
receivableAccount: "1200", // AR
|
|
33
|
+
revenueAccount: "4010", // Service Revenue
|
|
34
|
+
tax: { code: "HST", account: "2300" },
|
|
35
|
+
label: "INV-001",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const bs = await engine.reports.balanceSheet({
|
|
39
|
+
organizationId: orgId,
|
|
40
|
+
dateOption: "year",
|
|
41
|
+
dateValue: 2025,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
The engine owns the models. After `createAccountingEngine` you have:
|
|
39
46
|
|
|
40
47
|
| 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
|
-
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `engine.models.{Account,JournalEntry,FiscalPeriod,Budget,Reconciliation}` | Mongoose models |
|
|
50
|
+
| `engine.repositories.accounts` | `seedAccounts()`, `bulkCreate()` + plugins |
|
|
51
|
+
| `engine.repositories.journalEntries` | `post()`, `unpost()`, `reverse()`, `duplicate()` + double-entry, fiscal-lock, idempotency |
|
|
52
|
+
| `engine.repositories.{fiscalPeriods,budgets}` | Plain CRUD |
|
|
53
|
+
| `engine.repositories.reconciliations` | `reconcile()`, `unreconcile()`, `getUnreconciled()` |
|
|
54
|
+
| `engine.record.*` | Domain verbs (`sale`, `expense`, `transfer`, `payment`, `adjustment`) |
|
|
55
|
+
| `engine.introspect.*` | Runtime catalog of accounts, tax codes, reports |
|
|
56
|
+
| `engine.reports.*` | All 10 reports, bound to owned models |
|
|
57
|
+
|
|
58
|
+
## Semantic Record API
|
|
59
|
+
|
|
60
|
+
Record business operations as domain verbs. The engine resolves account codes, splits tax, and produces a balanced journal entry — you never touch debits/credits.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Cash sale with 13% HST
|
|
64
|
+
await engine.record.sale(orgId, {
|
|
65
|
+
date, amount: 10000,
|
|
66
|
+
receivableAccount: "1001", revenueAccount: "4010",
|
|
67
|
+
tax: { code: "HST", account: "2300" },
|
|
68
|
+
});
|
|
62
69
|
|
|
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
|
|
70
|
+
// Expense with recoverable input tax credit
|
|
71
|
+
await engine.record.expense(orgId, {
|
|
72
|
+
date, amount: 3000,
|
|
73
|
+
expenseAccount: "6010", paidFromAccount: "2001",
|
|
74
|
+
tax: { code: "HST_ITC", account: "2400" },
|
|
75
|
+
});
|
|
98
76
|
|
|
99
|
-
|
|
77
|
+
await engine.record.transfer(orgId, { date, amount: 5000, fromAccount: "1001", toAccount: "1002" });
|
|
100
78
|
|
|
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
|
-
},
|
|
79
|
+
await engine.record.payment(orgId, {
|
|
80
|
+
date, amount: 11300,
|
|
81
|
+
fromReceivableAccount: "1200", toCashAccount: "1001",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Multi-line adjustment (depreciation, accruals, corrections)
|
|
85
|
+
await engine.record.adjustment(orgId, {
|
|
86
|
+
date, label: "Monthly depreciation",
|
|
87
|
+
lines: [
|
|
88
|
+
{ account: "6030", debit: 1000 },
|
|
89
|
+
{ account: "1500", credit: 1000 },
|
|
90
|
+
],
|
|
116
91
|
});
|
|
117
92
|
```
|
|
118
93
|
|
|
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 }], ... });
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
## Introspection
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const catalog = await engine.introspect.catalog(orgId);
|
|
100
|
+
// { accounts, journalTypes, reports, taxCodes, fiscalPeriods }
|
|
101
|
+
|
|
102
|
+
engine.introspect.accounts(orgId);
|
|
103
|
+
engine.introspect.taxCodes("ON");
|
|
104
|
+
engine.introspect.reports(); // sync — static catalog
|
|
134
105
|
```
|
|
135
106
|
|
|
136
|
-
|
|
107
|
+
## Structured Validation Errors
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
try {
|
|
111
|
+
await engine.record.sale(orgId, { ... });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof AccountingError) {
|
|
114
|
+
err.status // 400 | 403 | 404 | 409
|
|
115
|
+
err.code // 'VALIDATION_ERROR' | 'NOT_FOUND' | ...
|
|
116
|
+
err.fields // [{ path, issue, value }, ...]
|
|
117
|
+
err.toJSON();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
137
121
|
|
|
138
|
-
|
|
122
|
+
Field errors come straight from plugins (double-entry, fiscal-lock) and the semantic layer.
|
|
123
|
+
|
|
124
|
+
## Audit, Observability & Framework Integration
|
|
125
|
+
|
|
126
|
+
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:
|
|
127
|
+
|
|
128
|
+
1. **Any mongokit plugin** via `config.plugins` — see the `@classytic/mongokit` docs for `auditTrailPlugin`, `observabilityPlugin`, and others.
|
|
129
|
+
```ts
|
|
130
|
+
const engine = createAccountingEngine({
|
|
131
|
+
mongoose: mongoose.connection, country: canadaPack, currency: "CAD",
|
|
132
|
+
plugins: {
|
|
133
|
+
journalEntry: [/* your mongokit plugins */],
|
|
134
|
+
account: [/* your mongokit plugins */],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
2. **Runtime listeners** — no plugin needed:
|
|
139
|
+
```ts
|
|
140
|
+
engine.repositories.journalEntries.on("after:create", ({ context, result }) => {
|
|
141
|
+
auditLog.write({ userId: context.user?._id, orgId: context.organizationId, entryId: result._id });
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
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.
|
|
145
|
+
|
|
146
|
+
| Layer | What it sees | What it misses |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| HTTP middleware (Arc / Express / Nest) | Request → user, IP, route, payload | Background jobs, CLI scripts, anything bypassing HTTP |
|
|
149
|
+
| Model-layer mongokit plugin | Every collection write, regardless of caller | HTTP context unless the caller forwards it |
|
|
150
|
+
|
|
151
|
+
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:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Express
|
|
155
|
+
app.post("/sales", async (req, res) => {
|
|
156
|
+
await engine.record.sale(req.body.orgId, req.body, {
|
|
157
|
+
user: req.user, ip: req.ip, userAgent: req.headers["user-agent"],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
```
|
|
139
161
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
162
|
+
## Reports
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await engine.reports.trialBalance({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
166
|
+
await engine.reports.balanceSheet({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
167
|
+
await engine.reports.incomeStatement({ organizationId, dateOption: "quarter", dateValue: { year: 2025, quarter: 2 } });
|
|
168
|
+
await engine.reports.generalLedger({ organizationId, dateOption: "month", dateValue: { year: 2025, month: 4 } });
|
|
169
|
+
await engine.reports.cashFlow({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
170
|
+
await engine.reports.agedBalance({ organizationId, type: "receivable", asOfDate: new Date() });
|
|
171
|
+
await engine.reports.budgetVsActual({ organizationId, dateOption: "year", dateValue: 2025 });
|
|
172
|
+
await engine.reports.dimensionBreakdown({ organizationId, dimension: "departmentId", dateOption: "year", dateValue: 2025 });
|
|
173
|
+
await engine.reports.revaluation({ organizationId, asOfDate: new Date(), rates: [{ currency: "USD", rate: 1.40 }], unrealizedGainLossAccountId });
|
|
148
174
|
```
|
|
149
175
|
|
|
150
|
-
|
|
176
|
+
All values are integer cents. Use `Money.toDecimal()` at your API boundary.
|
|
151
177
|
|
|
152
|
-
|
|
153
|
-
import { dateLockPlugin, taxHookPlugin } from '@classytic/ledger';
|
|
178
|
+
The 10 reports:
|
|
154
179
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
180
|
+
- **Trial Balance** (3-column: opening + period + ending)
|
|
181
|
+
- **Balance Sheet** (with computed retained earnings, multi-year aware)
|
|
182
|
+
- **Income Statement** (revenue, COGS, gross profit, operating expenses, net income)
|
|
183
|
+
- **General Ledger** (per-account with running balances)
|
|
184
|
+
- **Cash Flow** (operating / investing / financing)
|
|
185
|
+
- **Aged Receivable / Payable** (configurable buckets)
|
|
186
|
+
- **Budget vs Actual** (variance analysis)
|
|
187
|
+
- **Dimension Breakdown** (by department, project, cost center)
|
|
188
|
+
- **FX Revaluation** (unrealized gain/loss)
|
|
189
|
+
- **Fiscal Year Close / Reopen** (automatic closing entries)
|
|
160
190
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
191
|
+
## Engine Configuration
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
createAccountingEngine({
|
|
195
|
+
mongoose: mongoose.connection, // required
|
|
196
|
+
country: canadaPack, // required
|
|
197
|
+
currency: "CAD", // required — base/functional currency
|
|
198
|
+
multiTenant: { orgField, orgRef }, // optional
|
|
199
|
+
multiCurrency: { enabled: true, currencies: ["USD", "EUR"] },
|
|
200
|
+
fiscalYearStartMonth: 1, // 1=Jan (default), 4=Apr, 7=Jul
|
|
201
|
+
retainedEarningsAccountCode: "3600", // overrides country pack
|
|
202
|
+
modelNames: { account: "GLAccount", ... }, // custom collection names
|
|
203
|
+
schemaOptions: { // extra fields/indexes per model
|
|
204
|
+
journalEntry: {
|
|
205
|
+
extraFields: { aiJob: { status: String, generatedAt: Date } },
|
|
206
|
+
extraIndexes: [{ fields: { "aiJob.status": 1 }, options: { sparse: true } }],
|
|
168
207
|
},
|
|
169
208
|
},
|
|
209
|
+
strictness: {
|
|
210
|
+
immutable: true, // disable unpost — corrections only via reverse
|
|
211
|
+
requireActor: true, // actorId required on post/reverse
|
|
212
|
+
requireApproval: true, // entries must be approved before posting
|
|
213
|
+
},
|
|
214
|
+
plugins: { journalEntry: [...], account: [...] }, // any mongokit plugins
|
|
215
|
+
pagination: { account: { maxLimit: 5000 } }, // optional caps; no default cap
|
|
170
216
|
});
|
|
171
217
|
```
|
|
172
218
|
|
|
173
|
-
|
|
219
|
+
`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
220
|
|
|
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
|
-
});
|
|
221
|
+
## Built-in Plugins
|
|
200
222
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
223
|
+
| Plugin | Purpose |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| `doubleEntryPlugin` | Validates debits = credits, account existence, tenant integrity |
|
|
226
|
+
| `fiscalLockPlugin` | Prevents posting to closed fiscal periods |
|
|
227
|
+
| `dateLockPlugin` | Blocks entries before a configurable lock date |
|
|
228
|
+
| `taxHookPlugin` | Auto-generates tax lines via a `TaxLineGenerator` |
|
|
229
|
+
| `idempotencyPlugin` | Prevents duplicate entries by key |
|
|
230
|
+
|
|
231
|
+
`doubleEntryPlugin`, `fiscalLockPlugin` and `idempotencyPlugin` are wired automatically by the engine. The others are opt-in via the second `createAccountingEngine` argument.
|
|
232
|
+
|
|
233
|
+
## Custom Journal Types
|
|
206
234
|
|
|
207
|
-
|
|
208
|
-
isValidJournalType('POS_SALES'); // true
|
|
209
|
-
getJournalTypeCodes(); // [...15 built-in, 'POS_SALES', 'ECOM_SALES']
|
|
235
|
+
The 15 built-in journal types (SALES, PURCHASES, GENERAL, PAYROLL, …) cover standard accounting. Register custom types **before** the first engine call:
|
|
210
236
|
|
|
211
|
-
|
|
237
|
+
```ts
|
|
238
|
+
import { registerJournalType } from "@classytic/ledger";
|
|
239
|
+
|
|
240
|
+
registerJournalType("POS_SALES", { code: "POS_SALES", name: "POS Sales Journal" });
|
|
241
|
+
registerJournalType("ECOM_SALES", { code: "ECOM_SALES", name: "E-Commerce Sales" });
|
|
212
242
|
```
|
|
213
243
|
|
|
214
|
-
|
|
244
|
+
Reference numbers use the type prefix (`POS_SALES/2025/03/0001`). The registry freezes after the first schema is created.
|
|
215
245
|
|
|
216
246
|
## Country Packs
|
|
217
247
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
import { defineCountryPack } from '@classytic/ledger';
|
|
248
|
+
```ts
|
|
249
|
+
import { defineCountryPack } from "@classytic/ledger";
|
|
222
250
|
|
|
223
251
|
export const myPack = defineCountryPack({
|
|
224
|
-
code:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
taxCodes: { /* your tax codes */ },
|
|
230
|
-
taxCodesByRegion: {},
|
|
231
|
-
regions: [],
|
|
252
|
+
code: "US", name: "United States", defaultCurrency: "USD",
|
|
253
|
+
retainedEarningsAccountCode: "3200",
|
|
254
|
+
accountTypes: [/* chart of accounts */],
|
|
255
|
+
taxCodes: {/* tax codes */},
|
|
256
|
+
taxCodesByRegion: {}, regions: [],
|
|
232
257
|
});
|
|
233
258
|
```
|
|
234
259
|
|
|
235
|
-
Available
|
|
260
|
+
Available: `@classytic/ledger-ca` (Canada), `@classytic/ledger-bd` (Bangladesh).
|
|
261
|
+
|
|
262
|
+
## Subpath Exports
|
|
263
|
+
|
|
264
|
+
| Path | Contents |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| `@classytic/ledger` | Engine, Money, plugins, reports, types |
|
|
267
|
+
| `@classytic/ledger/money` | `Money` class |
|
|
268
|
+
| `@classytic/ledger/reports` | Standalone report generators |
|
|
269
|
+
| `@classytic/ledger/plugins` | All plugins |
|
|
270
|
+
| `@classytic/ledger/exports` | CSV export + QuickBooks field maps |
|
|
271
|
+
| `@classytic/ledger/country` | `defineCountryPack`, `CountryPack` |
|
|
272
|
+
| `@classytic/ledger/constants` | Categories, journal types, currencies |
|
|
236
273
|
|
|
237
274
|
## Testing
|
|
238
275
|
|
|
239
276
|
```bash
|
|
240
|
-
npm test
|
|
241
|
-
npx vitest run tests/e2e/
|
|
242
|
-
npx vitest run tests/scenarios/
|
|
243
|
-
npx vitest run tests/hardening/
|
|
277
|
+
npm test # 1273 tests, 67 files
|
|
278
|
+
npx vitest run tests/e2e/ # full-year scenarios
|
|
279
|
+
npx vitest run tests/scenarios/ # integration scenarios
|
|
280
|
+
npx vitest run tests/hardening/ # edge cases & invariants
|
|
244
281
|
```
|
|
245
282
|
|
|
246
|
-
|
|
247
|
-
|
|
283
|
+
Coverage includes:
|
|
284
|
+
|
|
285
|
+
- Canadian small-business full-year lifecycle (open → post → close → reopen)
|
|
286
|
+
- Multi-year fiscal cycles with retained-earnings rollover
|
|
248
287
|
- Multi-currency trading with FX revaluation
|
|
249
288
|
- Multi-tenant report isolation (org A cannot see org B)
|
|
250
|
-
-
|
|
251
|
-
- Reversal
|
|
289
|
+
- All 10 reports with month / quarter / year / custom date ranges
|
|
290
|
+
- Reversal and correction workflows
|
|
252
291
|
- Custom journal type registry → schema → posting pipeline
|
|
253
|
-
- Double-entry conservation
|
|
292
|
+
- Double-entry conservation across all entries
|
|
254
293
|
- Money arithmetic hardening (overflow, penny-leak, float traps)
|
|
255
|
-
- Public API surface & subpath export verification
|
|
256
294
|
- O-Level / A-Level / university textbook accounting problems
|
|
257
295
|
|
|
258
296
|
## 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 };
|