@classytic/ledger 0.4.1 → 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.
Files changed (38) hide show
  1. package/README.md +227 -189
  2. package/dist/constants/index.d.mts +1 -1
  3. package/dist/constants/index.mjs +2 -3
  4. package/dist/country/index.d.mts +1 -1
  5. package/dist/{journals-BfwnCFam.mjs → currencies-CsuBGfgs.mjs} +80 -1
  6. package/dist/{date-lock.plugin-DL6pe24p.mjs → date-lock.plugin-B2Jy0ukX.mjs} +61 -10
  7. package/dist/errors-BmRjW38t.mjs +33 -0
  8. package/dist/exports/index.d.mts +1 -1
  9. package/dist/exports/index.mjs +1 -1
  10. package/dist/{fiscal-close-B2_7WMTe.mjs → fiscal-close-Dk3yRT9i.mjs} +14 -4
  11. package/dist/{index-CxZqRaOU.d.mts → index-GmfEFxVn.d.mts} +1 -1
  12. package/dist/index.d.mts +530 -338
  13. package/dist/index.mjs +1824 -175
  14. package/dist/{journals-DTipb_rz.d.mts → journals-C50E9mpo.d.mts} +1 -1
  15. package/dist/plugins/index.d.mts +1 -1
  16. package/dist/plugins/index.mjs +1 -1
  17. package/dist/reports/index.d.mts +1 -1
  18. package/dist/reports/index.mjs +1 -1
  19. package/dist/{trial-balance-DcQ0xj_4.d.mts → trial-balance-BZ7yOOFD.d.mts} +16 -4
  20. package/package.json +1 -11
  21. package/dist/currencies-W8kQAkm0.mjs +0 -80
  22. package/dist/engine-scgOvxHJ.d.mts +0 -130
  23. package/dist/errors-B_dyYZc_.mjs +0 -26
  24. package/dist/journal-entry.schema-JqrfbvB4.d.mts +0 -103
  25. package/dist/logger-UbTdBb1x.d.mts +0 -14
  26. package/dist/reconciliation.repository-D-D_ITL-.d.mts +0 -135
  27. package/dist/reconciliation.repository-fPwFKvrk.mjs +0 -542
  28. package/dist/reconciliation.schema-BA1lPv4t.mjs +0 -666
  29. package/dist/repositories/index.d.mts +0 -2
  30. package/dist/repositories/index.mjs +0 -2
  31. package/dist/schemas/index.d.mts +0 -71
  32. package/dist/schemas/index.mjs +0 -2
  33. package/dist/tenant-guard-r17Se3Bb.mjs +0 -13
  34. /package/dist/{categories-DWogBUgQ.mjs → categories-BkKdv16V.mjs} +0 -0
  35. /package/dist/{core-8Xfnpn6g.d.mts → core-BkGjuVZj.d.mts} +0 -0
  36. /package/dist/{exports-DoGQQtMQ.mjs → exports-BP-0Ni5W.mjs} +0 -0
  37. /package/dist/{idempotency.plugin-zU-GKJ0-.d.mts → idempotency.plugin-CK7LHnBn.d.mts} +0 -0
  38. /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. Extensible journal types, multi-tenant isolation at every layer.
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 # Canada (GIFI, GST/HST, CRA)
12
- npm install @classytic/ledger-bd # Bangladesh (BFRS, VAT/TDS, Mushak)
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
- ```typescript
18
- import mongoose from 'mongoose';
19
- import { createAccountingEngine } from '@classytic/ledger';
20
- import { canadaPack } from '@classytic/ledger-ca';
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: 'CAD',
27
- multiTenant: { orgField: 'organizationId', orgRef: 'Organization' },
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
- ### Engine-owned models (recommended)
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
- Pass `mongoose: connection` in config and the engine creates and wires everything:
45
+ The engine owns the models. After `createAccountingEngine` you have:
39
46
 
40
47
  | Property | What it gives you |
41
- |----------|-------------------|
42
- | `engine.models.Account` / `JournalEntry` / `FiscalPeriod` / `Budget` / `Reconciliation` | Mongoose models, ready to query |
43
- | `engine.repositories.accounts.seedAccounts()` / `bulkCreate()` | Account repo with domain methods |
44
- | `engine.repositories.journalEntries.post()` / `reverse()` / `unpost()` / `duplicate()` | JE repo with plugins pre-wired (double-entry, fiscal-lock, idempotency) |
45
- | `engine.repositories.fiscalPeriods` / `budgets` | Plain CRUD repos |
46
- | `engine.repositories.reconciliations.reconcile()` / `unreconcile()` / `getUnreconciled()` | Reconciliation repo with domain methods |
47
- | `engine.reports.trialBalance()` / `balanceSheet()` / etc. | All 10 reports, bound to the owned models |
48
-
49
- This pattern unblocks framework auto-discovery (Arc `loadResources`, Fastify plugins, etc.) because resources can be defined at module top-level without factory wrappers.
50
-
51
- ### Low-level (manual schema/model setup)
52
-
53
- If you need custom naming or don't want the engine to own models, omit `mongoose` from config:
54
-
55
- ```typescript
56
- const engine = createAccountingEngine({ country: canadaPack, currency: 'CAD' });
57
- const Account = mongoose.model('GLAccount', engine.createAccountSchema());
58
- const JournalEntry = mongoose.model('GLEntry', engine.createJournalEntrySchema('GLAccount'));
59
- const accountRepo = engine.wireAccountRepository(new Repository(Account), Account);
60
- const reports = engine.createReports({ Account, JournalEntry });
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
- ## Core Features
64
-
65
- **Accounting Engine**
66
- - Double-entry validation with balance enforcement
67
- - Integer-cents storage zero floating-point drift
68
- - Draft → Posted → Reversed state machine
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
- ## Engine Configuration
77
+ await engine.record.transfer(orgId, { date, amount: 5000, fromAccount: "1001", toAccount: "1002" });
100
78
 
101
- ```typescript
102
- createAccountingEngine({
103
- country: canadaPack, // required
104
- currency: 'CAD', // required — base/functional currency
105
- multiTenant: { orgField, orgRef }, // optional — multi-tenant scoping
106
- multiCurrency: { enabled: true, currencies: ['USD', 'EUR'] },
107
- fiscalYearStartMonth: 1, // 1=Jan (default), 4=Apr, 7=Jul
108
- retainedEarningsAccountCode: '3600', // overrides country pack
109
- audit: { trackActor: true },
110
- idempotency: true,
111
- strictness: {
112
- immutable: true, // disable unpost, corrections via reverse only
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
- ## Reports API
120
-
121
- ```typescript
122
- const reports = accounting.createReports({ Account, JournalEntry, Budget });
123
-
124
- // All reports accept: { organizationId, dateOption, dateValue, filters? }
125
- await reports.trialBalance({ ... });
126
- await reports.balanceSheet({ ... });
127
- await reports.incomeStatement({ ... });
128
- await reports.generalLedger({ ... });
129
- await reports.cashFlow({ ... });
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
- All report data is sorted by account code. All monetary values are integer cents — use `Money.toDecimal()` at your API boundary.
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
- ## Schemas
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
- ```typescript
141
- accounting.createAccountSchema(options?)
142
- accounting.createJournalEntrySchema(accountModelName, {
143
- extraItemFields: { departmentId: { type: ObjectId, ref: 'Department' } },
144
- })
145
- accounting.createFiscalPeriodSchema(options?)
146
- accounting.createBudgetSchema(options?)
147
- accounting.createReconciliationSchema(accountModelName, journalEntryModelName, options?)
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
- ## Plugins
176
+ All values are integer cents. Use `Money.toDecimal()` at your API boundary.
151
177
 
152
- ```typescript
153
- import { dateLockPlugin, taxHookPlugin } from '@classytic/ledger';
178
+ The 10 reports:
154
179
 
155
- // Date lock block posting before a date
156
- dateLockPlugin({
157
- getLockDate: async (orgId) => db.getOrgLockDate(orgId),
158
- JournalEntryModel,
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
- // Tax hook — auto-generate tax lines
162
- taxHookPlugin({
163
- generator: {
164
- generateTaxLines(input) {
165
- if (!input.taxCode) return [];
166
- const tax = Money.percentage(input.amount, 1300); // 13%
167
- return [{ account: hstAccountId, debit: 0, credit: tax, taxDetails: [{ taxCode: 'HST' }] }];
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
- ## Subpath Exports
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
- | Path | Contents |
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
- registerJournalType('ECOM_SALES', {
202
- code: 'ECOM_SALES',
203
- name: 'E-Commerce Sales Journal',
204
- description: 'Per-order online transactions',
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
- // Custom types pass Mongoose enum validation, appear in all lookups
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
- // Reference numbers use the custom type prefix: POS_SALES/2025/03/0001
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
- The registry freezes when `createJournalEntrySchema()` is called. Late registration throws. Built-in types cannot be overridden.
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
- Build your own or use an existing one:
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: 'US',
225
- name: 'United States',
226
- defaultCurrency: 'USD',
227
- retainedEarningsAccountCode: '3200',
228
- accountTypes: [ /* your chart of accounts */ ],
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 packs: `@classytic/ledger-ca` (Canada), `@classytic/ledger-bd` (Bangladesh).
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 # run all
241
- npx vitest run tests/e2e/ # e2e scenarios only
242
- npx vitest run tests/scenarios/ # integration scenarios
243
- npx vitest run tests/hardening/ # edge cases & invariants
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
- Test suites cover:
247
- - Canadian small business full-year lifecycle
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
- - Posting pipeline Trial Balance Income Statement Balance Sheet
251
- - Reversal & correction workflows with audit trail
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 law (debit = credit across all entries)
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-DTipb_rz.mjs";
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 };
@@ -1,4 +1,3 @@
1
- import { a as getJournalType, c as registerJournalType, i as getCustomJournalTypes, n as JOURNAL_TYPES, o as getJournalTypeCodes, s as isValidJournalType, t as JOURNAL_CODES } from "../journals-BfwnCFam.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-DWogBUgQ.mjs";
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 };
@@ -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-CxZqRaOU.mjs";
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
- export { getJournalType as a, registerJournalType as c, getCustomJournalTypes as i, JOURNAL_TYPES as n, getJournalTypeCodes as o, _freezeJournalTypes as r, isValidJournalType as s, JOURNAL_CODES as t };
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 };