@classytic/ledger 0.3.0 → 0.4.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 (35) hide show
  1. package/README.md +82 -20
  2. package/dist/constants/index.d.mts +2 -2
  3. package/dist/constants/index.mjs +3 -3
  4. package/dist/{date-lock.plugin-eYAJ9h_u.mjs → date-lock.plugin-DL6pe24p.mjs} +2 -2
  5. package/dist/{engine-Cn-9yerQ.d.mts → engine-scgOvxHJ.d.mts} +30 -2
  6. package/dist/exports/index.d.mts +1 -1
  7. package/dist/exports/index.mjs +1 -1
  8. package/dist/{exports-I5Xkq-9_.mjs → exports-DoGQQtMQ.mjs} +96 -75
  9. package/dist/{fiscal-close-B6LhQ10f.mjs → fiscal-close-B2_7WMTe.mjs} +748 -751
  10. package/dist/{index-BPukb3L8.d.mts → index-J-XIbXH-.d.mts} +7 -7
  11. package/dist/index.d.mts +239 -87
  12. package/dist/index.mjs +149 -12
  13. package/dist/{fiscal-period.schema-BMnlI9H5.d.mts → journal-entry.schema-JqrfbvB4.d.mts} +12 -12
  14. package/dist/{journals-oH-FK3g8.mjs → journals-BfwnCFam.mjs} +27 -4
  15. package/dist/{currencies-4WAbFRlw.d.mts → journals-DTipb_rz.d.mts} +16 -7
  16. package/dist/money.mjs +2 -2
  17. package/dist/plugins/index.d.mts +1 -1
  18. package/dist/plugins/index.mjs +1 -1
  19. package/dist/{reconciliation.repository-CW4-8q90.d.mts → reconciliation.repository-D-D_ITL-.d.mts} +14 -14
  20. package/dist/{account.repository-BpkSd6q3.mjs → reconciliation.repository-fPwFKvrk.mjs} +255 -255
  21. package/dist/{reconciliation.schema-BuetvZTd.mjs → reconciliation.schema-BA1lPv4t.mjs} +174 -173
  22. package/dist/reports/index.d.mts +1 -1
  23. package/dist/reports/index.mjs +1 -1
  24. package/dist/repositories/index.d.mts +1 -1
  25. package/dist/repositories/index.mjs +1 -1
  26. package/dist/schemas/index.d.mts +6 -6
  27. package/dist/schemas/index.mjs +1 -1
  28. package/dist/{tenant-guard-Fm6AID_6.mjs → tenant-guard-r17Se3Bb.mjs} +1 -1
  29. package/dist/{revaluation-D9x0NE8w.d.mts → trial-balance-DcQ0xj_4.d.mts} +124 -124
  30. package/docs/schemas.md +2 -2
  31. package/package.json +14 -6
  32. /package/dist/{categories-CclX7Q94.mjs → categories-DWogBUgQ.mjs} +0 -0
  33. /package/dist/{errors-B7yC-Jfw.mjs → errors-B_dyYZc_.mjs} +0 -0
  34. /package/dist/{idempotency.plugin-B_CNsInz.d.mts → idempotency.plugin-zU-GKJ0-.d.mts} +0 -0
  35. /package/dist/{logger-CbHWZl7v.d.mts → logger-UbTdBb1x.d.mts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @classytic/ledger
2
2
 
3
- Embeddable double-entry accounting engine for MongoDB. Integer-cents arithmetic, plugin-based, country-agnostic.
3
+ Embeddable double-entry accounting engine for MongoDB. Integer-cents arithmetic, plugin-based, country-agnostic. Extensible journal types, multi-tenant isolation at every layer.
4
4
 
5
5
  Build QuickBooks, Xero, or TaxCycle-grade apps — the engine handles the accounting, you handle the UX.
6
6
 
@@ -15,23 +15,49 @@ npm install @classytic/ledger-bd # Bangladesh (BFRS, VAT/TDS, Mushak)
15
15
  ## Quick Start
16
16
 
17
17
  ```typescript
18
+ import mongoose from 'mongoose';
18
19
  import { createAccountingEngine } from '@classytic/ledger';
19
20
  import { canadaPack } from '@classytic/ledger-ca';
20
21
 
21
- const accounting = createAccountingEngine({
22
+ // The engine owns the models — matches flow/promo pattern
23
+ const engine = createAccountingEngine({
24
+ mongoose: mongoose.connection,
22
25
  country: canadaPack,
23
26
  currency: 'CAD',
24
- multiTenant: { orgField: 'organization', orgRef: 'Organization' },
27
+ multiTenant: { orgField: 'organizationId', orgRef: 'Organization' },
25
28
  });
26
29
 
27
- // Schemas
28
- const Account = mongoose.model('Account', accounting.createAccountSchema());
29
- const JournalEntry = mongoose.model('JournalEntry', accounting.createJournalEntrySchema('Account'));
30
- const FiscalPeriod = mongoose.model('FiscalPeriod', accounting.createFiscalPeriodSchema());
30
+ // Models, repositories, and reports are auto-created
31
+ 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
+
36
+ ### Engine-owned models (recommended)
37
+
38
+ Pass `mongoose: connection` in config and the engine creates and wires everything:
39
+
40
+ | 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)
31
52
 
32
- // Reports
33
- const reports = accounting.createReports({ Account, JournalEntry });
34
- const bs = await reports.balanceSheet({ organizationId, dateOption: 'year', dateValue: 2025 });
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 });
35
61
  ```
36
62
 
37
63
  ## Core Features
@@ -41,8 +67,9 @@ const bs = await reports.balanceSheet({ organizationId, dateOption: 'year', date
41
67
  - Integer-cents storage — zero floating-point drift
42
68
  - Draft → Posted → Reversed state machine
43
69
  - Configurable immutability (corrections only via reversal)
44
- - Multi-tenant isolation at every layer
70
+ - Multi-tenant isolation at every layer (reports, schemas, repositories)
45
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
46
73
 
47
74
  **10 Reports**
48
75
  - Trial Balance (3-column: initial + period + ending)
@@ -155,7 +182,36 @@ taxHookPlugin({
155
182
  | `@classytic/ledger/repositories` | Repository wiring |
156
183
  | `@classytic/ledger/exports` | CSV export + QuickBooks field maps |
157
184
  | `@classytic/ledger/country` | `defineCountryPack`, `CountryPack` interface |
158
- | `@classytic/ledger/constants` | Categories, journal types, currencies |
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
+ });
200
+
201
+ registerJournalType('ECOM_SALES', {
202
+ code: 'ECOM_SALES',
203
+ name: 'E-Commerce Sales Journal',
204
+ description: 'Per-order online transactions',
205
+ });
206
+
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']
210
+
211
+ // Reference numbers use the custom type prefix: POS_SALES/2025/03/0001
212
+ ```
213
+
214
+ The registry freezes when `createJournalEntrySchema()` is called. Late registration throws. Built-in types cannot be overridden.
159
215
 
160
216
  ## Country Packs
161
217
 
@@ -180,25 +236,31 @@ Available packs: `@classytic/ledger-ca` (Canada), `@classytic/ledger-bd` (Bangla
180
236
 
181
237
  ## Testing
182
238
 
183
- 949 tests covering unit, integration, and end-to-end scenarios:
184
-
185
239
  ```bash
186
- npm test # run all
187
- npx vitest run tests/e2e/ # e2e scenarios only
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
188
244
  ```
189
245
 
190
- E2E suites include:
246
+ Test suites cover:
191
247
  - Canadian small business full-year lifecycle
192
248
  - Multi-currency trading with FX revaluation
193
- - All plugins + dimensions + budgets + fiscal close
249
+ - 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
252
+ - Custom journal type registry → schema → posting pipeline
253
+ - Double-entry conservation law (debit = credit across all entries)
254
+ - Money arithmetic hardening (overflow, penny-leak, float traps)
255
+ - Public API surface & subpath export verification
194
256
  - O-Level / A-Level / university textbook accounting problems
195
257
 
196
258
  ## Requirements
197
259
 
198
260
  - Node.js >= 22
199
261
  - MongoDB (replica set recommended for transactions)
200
- - Mongoose >= 9
201
- - @classytic/mongokit >= 3
262
+ - Mongoose >= 9.4.1
263
+ - @classytic/mongokit >= 3.5.3
202
264
 
203
265
  ## License
204
266
 
@@ -1,2 +1,2 @@
1
- import { _ as getNormalBalance, a as JOURNAL_CODES, b as isValidCategory, c as getJournalTypeCodes, d as CATEGORY_KEYS, f as categoryKey, g as getCategoryStatementType, h as getCategoryMainType, i as isValidCurrency, l as isValidJournalType, m as extractStatementType, n as getCurrency, o as JOURNAL_TYPES, p as extractMainType, r as getMinorUnit, s as getJournalType, t as CURRENCIES, u as CATEGORIES, v as isBalanceSheet, y as isIncomeStatement } from "../currencies-4WAbFRlw.mjs";
2
- export { CATEGORIES, CATEGORY_KEYS, CURRENCIES, JOURNAL_CODES, JOURNAL_TYPES, categoryKey, extractMainType, extractStatementType, getCategoryMainType, getCategoryStatementType, getCurrency, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType };
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";
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,4 @@
1
- import { a as isValidJournalType, i as getJournalTypeCodes, n as JOURNAL_TYPES, r as getJournalType, t as JOURNAL_CODES } from "../journals-oH-FK3g8.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-CclX7Q94.mjs";
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
3
  import { i as isValidCurrency, n as getCurrency, r as getMinorUnit, t as CURRENCIES } from "../currencies-W8kQAkm0.mjs";
4
- export { CATEGORIES, CATEGORY_KEYS, CURRENCIES, JOURNAL_CODES, JOURNAL_TYPES, categoryKey, extractMainType, extractStatementType, getCategoryMainType, getCategoryStatementType, getCurrency, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType };
4
+ 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,4 @@
1
- import { n as Errors } from "./errors-B7yC-Jfw.mjs";
1
+ import { n as Errors } from "./errors-B_dyYZc_.mjs";
2
2
  //#region src/plugins/double-entry.plugin.ts
3
3
  function doubleEntryPlugin(options = {}) {
4
4
  const { onlyOnPost = true, JournalEntryModel, AccountModel, orgField } = options;
@@ -36,7 +36,7 @@ function doubleEntryPlugin(options = {}) {
36
36
  const accountIds = items.map((i) => i.account).filter((a) => a != null && a !== "");
37
37
  if (accountIds.length === 0) throw Errors.validation("Posted entry has items with missing accounts.");
38
38
  const selectFields = orgField ? `_id ${orgField}` : "_id";
39
- const accounts = await AccountModel.find({ _id: { $in: accountIds } }).select(selectFields).session(context.session ?? null).lean();
39
+ const accounts = await AccountModel?.find({ _id: { $in: accountIds } }).select(selectFields).session(context.session ?? null).lean();
40
40
  const foundIds = new Set(accounts.map((a) => String(a._id)));
41
41
  const missingCount = accountIds.filter((id) => !foundIds.has(String(id))).length;
42
42
  if (missingCount > 0) throw Errors.validation(`${missingCount} item(s) reference non-existent accounts.`);
@@ -1,5 +1,6 @@
1
1
  import { t as CountryPack } from "./index-CxZqRaOU.mjs";
2
- import { t as Logger } from "./logger-CbHWZl7v.mjs";
2
+ import { t as Logger } from "./logger-UbTdBb1x.mjs";
3
+ import { Connection } from "mongoose";
3
4
 
4
5
  //#region src/types/engine.d.ts
5
6
  /** Multi-tenant configuration */
@@ -68,8 +69,35 @@ interface MultiCurrencyConfig {
68
69
  /** Allowed foreign currency codes. If omitted, any ISO 4217 code is accepted. */
69
70
  currencies?: readonly string[];
70
71
  }
72
+ /**
73
+ * Override default model names. Useful when you want to avoid collisions
74
+ * with existing models or use custom naming conventions.
75
+ */
76
+ interface ModelNames {
77
+ account?: string;
78
+ journalEntry?: string;
79
+ fiscalPeriod?: string;
80
+ budget?: string;
81
+ reconciliation?: string;
82
+ }
71
83
  /** Main engine configuration */
72
84
  interface AccountingEngineConfig {
85
+ /**
86
+ * Mongoose connection. Required for `engine.models` and `engine.repositories`
87
+ * to be auto-populated. If omitted, you must use the low-level schema
88
+ * factories (`engine.createAccountSchema()`) and register models yourself.
89
+ */
90
+ mongoose?: Connection;
91
+ /** Override default model names (e.g. 'Account' → 'GLAccount') */
92
+ modelNames?: ModelNames;
93
+ /** Extra fields / indexes per model */
94
+ schemaOptions?: {
95
+ account?: SchemaOptions;
96
+ journalEntry?: JournalSchemaOptions;
97
+ fiscalPeriod?: SchemaOptions;
98
+ budget?: SchemaOptions;
99
+ reconciliation?: SchemaOptions;
100
+ };
73
101
  /** Country pack providing account types, tax codes, and templates */
74
102
  country: CountryPack;
75
103
  /** Default ISO 4217 currency code — the functional/base currency (e.g., 'CAD', 'BDT') */
@@ -99,4 +127,4 @@ interface AccountingEngineConfig {
99
127
  strictness?: StrictnessConfig;
100
128
  }
101
129
  //#endregion
102
- export { MultiTenantConfig as a, MultiCurrencyConfig as i, AuditConfig as n, SchemaOptions as o, JournalSchemaOptions as r, StrictnessConfig as s, AccountingEngineConfig as t };
130
+ export { MultiCurrencyConfig as a, StrictnessConfig as c, ModelNames as i, AuditConfig as n, MultiTenantConfig as o, JournalSchemaOptions as r, SchemaOptions as s, AccountingEngineConfig as t };
@@ -1,2 +1,2 @@
1
- import { _ as PopulatedJournalEntry, a as exportToCsv, c as getHeaders, d as serializeCsv, f as CsvOptions, g as PopulatedAccount, h as FlatJournalRow, i as flattenJournalEntry, l as buildCsv, m as ExportFieldMap, n as quickbooksFieldMap, o as extractAllRows, p as ExportField, r as flattenJournalEntries, s as extractRow, t as universalFieldMap, u as escapeCell, v as PopulatedJournalItem } from "../index-BPukb3L8.mjs";
1
+ import { _ as PopulatedJournalEntry, a as exportToCsv, c as getHeaders, d as serializeCsv, f as CsvOptions, g as PopulatedAccount, h as FlatJournalRow, i as quickbooksFieldMap, l as buildCsv, m as ExportFieldMap, n as flattenJournalEntry, o as extractAllRows, p as ExportField, r as universalFieldMap, s as extractRow, t as flattenJournalEntries, u as escapeCell, v as PopulatedJournalItem } from "../index-J-XIbXH-.mjs";
2
2
  export { CsvOptions, ExportField, ExportFieldMap, FlatJournalRow, PopulatedAccount, PopulatedJournalEntry, PopulatedJournalItem, buildCsv, escapeCell, exportToCsv, extractAllRows, extractRow, flattenJournalEntries, flattenJournalEntry, getHeaders, quickbooksFieldMap, serializeCsv, universalFieldMap };
@@ -1,2 +1,2 @@
1
- import { a as exportToCsv, c as getHeaders, d as serializeCsv, i as flattenJournalEntry, l as buildCsv, n as quickbooksFieldMap, o as extractAllRows, r as flattenJournalEntries, s as extractRow, t as universalFieldMap, u as escapeCell } from "../exports-I5Xkq-9_.mjs";
1
+ import { a as exportToCsv, c as getHeaders, d as serializeCsv, i as quickbooksFieldMap, l as buildCsv, n as flattenJournalEntry, o as extractAllRows, r as universalFieldMap, s as extractRow, t as flattenJournalEntries, u as escapeCell } from "../exports-DoGQQtMQ.mjs";
2
2
  export { buildCsv, escapeCell, exportToCsv, extractAllRows, extractRow, flattenJournalEntries, flattenJournalEntry, getHeaders, quickbooksFieldMap, serializeCsv, universalFieldMap };
@@ -3,7 +3,7 @@ import { Money } from "./money.mjs";
3
3
  const NEEDS_QUOTING = /[",\r\n]/;
4
4
  /** Escape a single CSV cell value per RFC 4180. */
5
5
  function escapeCell(value) {
6
- if (NEEDS_QUOTING.test(value)) return "\"" + value.replace(/"/g, "\"\"") + "\"";
6
+ if (NEEDS_QUOTING.test(value)) return `"${value.replace(/"/g, "\"\"")}"`;
7
7
  return value;
8
8
  }
9
9
  /** Serialize a 2D array of strings into a CSV string. */
@@ -18,6 +18,11 @@ function buildCsv(headers, dataRows, options = {}) {
18
18
  }
19
19
  //#endregion
20
20
  //#region src/exports/field-map.ts
21
+ /**
22
+ * Field Map Application — Applies an ExportFieldMap to data rows.
23
+ *
24
+ * @module @classytic/ledger/exports
25
+ */
21
26
  /** Extract headers from a field map. */
22
27
  function getHeaders(fieldMap) {
23
28
  return fieldMap.fields.map((f) => f.header);
@@ -35,80 +40,15 @@ function exportToCsv(fieldMap, rows, options) {
35
40
  return buildCsv(getHeaders(fieldMap), extractAllRows(fieldMap, rows), options);
36
41
  }
37
42
  //#endregion
38
- //#region src/exports/flatten-journal.ts
39
- function toDate(value) {
40
- if (!value) return /* @__PURE__ */ new Date(0);
41
- if (value instanceof Date) return value;
42
- return new Date(value);
43
- }
44
- function resolveAccount(account) {
45
- if (!account) return {
46
- id: "",
47
- name: "",
48
- typeCode: ""
49
- };
50
- if (typeof account === "string") return {
51
- id: account,
52
- name: "",
53
- typeCode: ""
54
- };
55
- return {
56
- id: String(account._id ?? ""),
57
- name: account.name ?? "",
58
- typeCode: account.accountTypeCode ?? ""
59
- };
60
- }
61
- /** Flatten a single journal entry into one FlatJournalRow per journal item. */
62
- function flattenJournalEntry(entry) {
63
- const entryDate = toDate(entry.date);
64
- const items = entry.journalItems ?? [];
65
- const itemCount = items.length;
66
- const KNOWN_ITEM_KEYS = new Set([
67
- "account",
68
- "label",
69
- "date",
70
- "debit",
71
- "credit",
72
- "taxDetails"
73
- ]);
74
- return items.map((item, index) => {
75
- const acct = resolveAccount(item.account);
76
- const firstTax = item.taxDetails?.[0];
77
- const extraItemFields = {};
78
- for (const key of Object.keys(item)) if (!KNOWN_ITEM_KEYS.has(key)) extraItemFields[key] = item[key];
79
- return {
80
- entryId: String(entry._id ?? ""),
81
- journalType: entry.journalType ?? "",
82
- referenceNumber: entry.referenceNumber ?? "",
83
- entryLabel: entry.label ?? "",
84
- entryDate,
85
- state: entry.state,
86
- reversed: entry.reversed ?? false,
87
- totalDebit: entry.totalDebit ?? 0,
88
- totalCredit: entry.totalCredit ?? 0,
89
- accountId: acct.id,
90
- accountName: acct.name,
91
- accountTypeCode: acct.typeCode,
92
- itemLabel: item.label ?? "",
93
- itemDate: item.date ? toDate(item.date) : entryDate,
94
- debit: item.debit ?? 0,
95
- credit: item.credit ?? 0,
96
- taxCode: firstTax?.taxCode ?? "",
97
- taxName: firstTax?.taxName ?? "",
98
- itemIndex: index,
99
- itemCount,
100
- ...extraItemFields
101
- };
102
- });
103
- }
104
- /** Flatten multiple journal entries into a single flat row array. */
105
- function flattenJournalEntries(entries) {
106
- const rows = [];
107
- for (const entry of entries) rows.push(...flattenJournalEntry(entry));
108
- return rows;
109
- }
110
- //#endregion
111
43
  //#region src/exports/field-maps/quickbooks.ts
44
+ /**
45
+ * QuickBooks General Journal Import Field Map
46
+ *
47
+ * Produces CSV compatible with QuickBooks Desktop and Online
48
+ * "Import General Journal Entries" feature.
49
+ *
50
+ * @module @classytic/ledger/exports
51
+ */
112
52
  function formatQbDate(date) {
113
53
  return `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}/${date.getFullYear()}`;
114
54
  }
@@ -161,6 +101,14 @@ const quickbooksFieldMap = {
161
101
  };
162
102
  //#endregion
163
103
  //#region src/exports/field-maps/universal.ts
104
+ /**
105
+ * Universal Export Field Map
106
+ *
107
+ * Comprehensive CSV export with all available fields.
108
+ * Useful for data portability, auditing, or spreadsheet import.
109
+ *
110
+ * @module @classytic/ledger/exports
111
+ */
164
112
  function formatIsoDate(date) {
165
113
  return date.toISOString().split("T")[0];
166
114
  }
@@ -247,4 +195,77 @@ const universalFieldMap = {
247
195
  ]
248
196
  };
249
197
  //#endregion
250
- export { exportToCsv as a, getHeaders as c, serializeCsv as d, flattenJournalEntry as i, buildCsv as l, quickbooksFieldMap as n, extractAllRows as o, flattenJournalEntries as r, extractRow as s, universalFieldMap as t, escapeCell as u };
198
+ //#region src/exports/flatten-journal.ts
199
+ function toDate(value) {
200
+ if (!value) return /* @__PURE__ */ new Date(0);
201
+ if (value instanceof Date) return value;
202
+ return new Date(value);
203
+ }
204
+ function resolveAccount(account) {
205
+ if (!account) return {
206
+ id: "",
207
+ name: "",
208
+ typeCode: ""
209
+ };
210
+ if (typeof account === "string") return {
211
+ id: account,
212
+ name: "",
213
+ typeCode: ""
214
+ };
215
+ return {
216
+ id: String(account._id ?? ""),
217
+ name: account.name ?? "",
218
+ typeCode: account.accountTypeCode ?? ""
219
+ };
220
+ }
221
+ /** Flatten a single journal entry into one FlatJournalRow per journal item. */
222
+ function flattenJournalEntry(entry) {
223
+ const entryDate = toDate(entry.date);
224
+ const items = entry.journalItems ?? [];
225
+ const itemCount = items.length;
226
+ const KNOWN_ITEM_KEYS = new Set([
227
+ "account",
228
+ "label",
229
+ "date",
230
+ "debit",
231
+ "credit",
232
+ "taxDetails"
233
+ ]);
234
+ return items.map((item, index) => {
235
+ const acct = resolveAccount(item.account);
236
+ const firstTax = item.taxDetails?.[0];
237
+ const extraItemFields = {};
238
+ for (const key of Object.keys(item)) if (!KNOWN_ITEM_KEYS.has(key)) extraItemFields[key] = item[key];
239
+ return {
240
+ entryId: String(entry._id ?? ""),
241
+ journalType: entry.journalType ?? "",
242
+ referenceNumber: entry.referenceNumber ?? "",
243
+ entryLabel: entry.label ?? "",
244
+ entryDate,
245
+ state: entry.state,
246
+ reversed: entry.reversed ?? false,
247
+ totalDebit: entry.totalDebit ?? 0,
248
+ totalCredit: entry.totalCredit ?? 0,
249
+ accountId: acct.id,
250
+ accountName: acct.name,
251
+ accountTypeCode: acct.typeCode,
252
+ itemLabel: item.label ?? "",
253
+ itemDate: item.date ? toDate(item.date) : entryDate,
254
+ debit: item.debit ?? 0,
255
+ credit: item.credit ?? 0,
256
+ taxCode: firstTax?.taxCode ?? "",
257
+ taxName: firstTax?.taxName ?? "",
258
+ itemIndex: index,
259
+ itemCount,
260
+ ...extraItemFields
261
+ };
262
+ });
263
+ }
264
+ /** Flatten multiple journal entries into a single flat row array. */
265
+ function flattenJournalEntries(entries) {
266
+ const rows = [];
267
+ for (const entry of entries) rows.push(...flattenJournalEntry(entry));
268
+ return rows;
269
+ }
270
+ //#endregion
271
+ export { exportToCsv as a, getHeaders as c, serializeCsv as d, quickbooksFieldMap as i, buildCsv as l, flattenJournalEntry as n, extractAllRows as o, universalFieldMap as r, extractRow as s, flattenJournalEntries as t, escapeCell as u };