@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.
Files changed (38) hide show
  1. package/README.md +228 -188
  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-B6WyvqNG.mjs} +63 -11
  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/{idempotency.plugin-zU-GKJ0-.d.mts → idempotency.plugin-WcQLZU9n.d.mts} +38 -0
  12. package/dist/{index-CxZqRaOU.d.mts → index-GmfEFxVn.d.mts} +1 -1
  13. package/dist/index.d.mts +525 -344
  14. package/dist/index.mjs +1868 -170
  15. package/dist/{journals-DTipb_rz.d.mts → journals-C50E9mpo.d.mts} +1 -1
  16. package/dist/plugins/index.d.mts +1 -1
  17. package/dist/plugins/index.mjs +1 -1
  18. package/dist/reports/index.d.mts +1 -1
  19. package/dist/reports/index.mjs +1 -1
  20. package/dist/{trial-balance-DcQ0xj_4.d.mts → trial-balance-BZ7yOOFD.d.mts} +16 -4
  21. package/package.json +1 -11
  22. package/dist/currencies-W8kQAkm0.mjs +0 -80
  23. package/dist/engine-scgOvxHJ.d.mts +0 -130
  24. package/dist/errors-B_dyYZc_.mjs +0 -26
  25. package/dist/journal-entry.schema-JqrfbvB4.d.mts +0 -103
  26. package/dist/logger-UbTdBb1x.d.mts +0 -14
  27. package/dist/reconciliation.repository-D-D_ITL-.d.mts +0 -135
  28. package/dist/reconciliation.repository-fPwFKvrk.mjs +0 -542
  29. package/dist/reconciliation.schema-BA1lPv4t.mjs +0 -666
  30. package/dist/repositories/index.d.mts +0 -2
  31. package/dist/repositories/index.mjs +0 -2
  32. package/dist/schemas/index.d.mts +0 -71
  33. package/dist/schemas/index.mjs +0 -2
  34. package/dist/tenant-guard-r17Se3Bb.mjs +0 -13
  35. /package/dist/{categories-DWogBUgQ.mjs → categories-BkKdv16V.mjs} +0 -0
  36. /package/dist/{core-8Xfnpn6g.d.mts → core-BkGjuVZj.d.mts} +0 -0
  37. /package/dist/{exports-DoGQQtMQ.mjs → exports-BP-0Ni5W.mjs} +0 -0
  38. /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. Extensible journal types, multi-tenant isolation at every layer.
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
- Build QuickBooks, Xero, or TaxCycle-grade apps the engine handles the accounting, you handle the UX.
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 # Canada (GIFI, GST/HST, CRA)
12
- npm install @classytic/ledger-bd # Bangladesh (BFRS, VAT/TDS, Mushak)
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
- ```typescript
18
- import mongoose from 'mongoose';
19
- import { createAccountingEngine } from '@classytic/ledger';
20
- import { canadaPack } from '@classytic/ledger-ca';
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: 'CAD',
27
- multiTenant: { orgField: 'organizationId', orgRef: 'Organization' },
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
- ### Engine-owned models (recommended)
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
- Pass `mongoose: connection` in config and the engine creates and wires everything:
47
+ The engine owns the models. After `createAccountingEngine` you have:
39
48
 
40
49
  | 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
- ```
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
- ## 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
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
- ## Engine Configuration
79
+ await engine.record.transfer(orgId, { date, amount: 5000, fromAccount: "1001", toAccount: "1002" });
100
80
 
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
- },
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
- ## 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 }], ... });
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
- All report data is sorted by account code. All monetary values are integer cents — use `Money.toDecimal()` at your API boundary.
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
- ## Schemas
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
- ```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?)
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
- ## Plugins
178
+ All values are integer cents. Use `Money.toDecimal()` at your API boundary.
151
179
 
152
- ```typescript
153
- import { dateLockPlugin, taxHookPlugin } from '@classytic/ledger';
180
+ The 10 reports:
154
181
 
155
- // Date lock block posting before a date
156
- dateLockPlugin({
157
- getLockDate: async (orgId) => db.getOrgLockDate(orgId),
158
- JournalEntryModel,
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
- // 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' }] }];
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
- ## Subpath Exports
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
- | 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
- });
223
+ ## Built-in Plugins
200
224
 
201
- registerJournalType('ECOM_SALES', {
202
- code: 'ECOM_SALES',
203
- name: 'E-Commerce Sales Journal',
204
- description: 'Per-order online transactions',
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
- // Custom types pass Mongoose enum validation, appear in all lookups
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
- // Reference numbers use the custom type prefix: POS_SALES/2025/03/0001
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
- The registry freezes when `createJournalEntrySchema()` is called. Late registration throws. Built-in types cannot be overridden.
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
- Build your own or use an existing one:
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: '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: [],
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 packs: `@classytic/ledger-ca` (Canada), `@classytic/ledger-bd` (Bangladesh).
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 # 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
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
- Test suites cover:
247
- - Canadian small business full-year lifecycle
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
- - Posting pipeline Trial Balance Income Statement Balance Sheet
251
- - Reversal & correction workflows with audit trail
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 law (debit = credit across all entries)
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-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 };