@classytic/ledger 0.1.3
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/account.repository-1C2sZvB2.d.mts +29 -0
- package/dist/account.repository-1C2sZvB2.d.mts.map +1 -0
- package/dist/account.repository-Crf5DGO4.mjs +393 -0
- package/dist/account.repository-Crf5DGO4.mjs.map +1 -0
- package/dist/categories-BNJBd4ze.mjs +70 -0
- package/dist/categories-BNJBd4ze.mjs.map +1 -0
- package/dist/constants/index.d.mts +2 -0
- package/dist/constants/index.mjs +5 -0
- package/dist/core-Cx0baosR.d.mts +104 -0
- package/dist/core-Cx0baosR.d.mts.map +1 -0
- package/dist/country/index.d.mts +105 -0
- package/dist/country/index.d.mts.map +1 -0
- package/dist/country/index.mjs +27 -0
- package/dist/country/index.mjs.map +1 -0
- package/dist/currencies-BBk3NwXn.mjs +82 -0
- package/dist/currencies-BBk3NwXn.mjs.map +1 -0
- package/dist/currencies-Bkn3FNkC.d.mts +38 -0
- package/dist/currencies-Bkn3FNkC.d.mts.map +1 -0
- package/dist/engine-Cd73EOT6.d.mts +72 -0
- package/dist/engine-Cd73EOT6.d.mts.map +1 -0
- package/dist/errors-CeqRahE-.mjs +28 -0
- package/dist/errors-CeqRahE-.mjs.map +1 -0
- package/dist/exports/index.d.mts +2 -0
- package/dist/exports/index.mjs +3 -0
- package/dist/fiscal-close-CNOwv_ud.mjs +934 -0
- package/dist/fiscal-close-CNOwv_ud.mjs.map +1 -0
- package/dist/fiscal-close-CzUzpnMg.d.mts +270 -0
- package/dist/fiscal-close-CzUzpnMg.d.mts.map +1 -0
- package/dist/fiscal-period.schema-CbALaaKl.mjs +477 -0
- package/dist/fiscal-period.schema-CbALaaKl.mjs.map +1 -0
- package/dist/fiscal-period.schema-DI2scngu.d.mts +38 -0
- package/dist/fiscal-period.schema-DI2scngu.d.mts.map +1 -0
- package/dist/idempotency.plugin-BESs9YPD.d.mts +58 -0
- package/dist/idempotency.plugin-BESs9YPD.d.mts.map +1 -0
- package/dist/idempotency.plugin-C6r8RI8d.mjs +165 -0
- package/dist/idempotency.plugin-C6r8RI8d.mjs.map +1 -0
- package/dist/index.d.mts +308 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +171 -0
- package/dist/index.mjs.map +1 -0
- package/dist/journals-CI3Wb4EF.mjs +92 -0
- package/dist/journals-CI3Wb4EF.mjs.map +1 -0
- package/dist/logger-Cv6VVc4r.d.mts +15 -0
- package/dist/logger-Cv6VVc4r.d.mts.map +1 -0
- package/dist/money.d.mts +129 -0
- package/dist/money.d.mts.map +1 -0
- package/dist/money.mjs +197 -0
- package/dist/money.mjs.map +1 -0
- package/dist/plugins/index.d.mts +2 -0
- package/dist/plugins/index.mjs +3 -0
- package/dist/reports/index.d.mts +2 -0
- package/dist/reports/index.mjs +3 -0
- package/dist/repositories/index.d.mts +2 -0
- package/dist/repositories/index.mjs +3 -0
- package/dist/schemas/index.d.mts +2 -0
- package/dist/schemas/index.mjs +3 -0
- package/dist/session-Dh0s6zG4.mjs +87 -0
- package/dist/session-Dh0s6zG4.mjs.map +1 -0
- package/dist/universal-CMfrZ2hG.mjs +257 -0
- package/dist/universal-CMfrZ2hG.mjs.map +1 -0
- package/dist/universal-x33ZJODp.d.mts +137 -0
- package/dist/universal-x33ZJODp.d.mts.map +1 -0
- package/docs/country-packs.md +117 -0
- package/docs/engine.md +147 -0
- package/docs/exports.md +81 -0
- package/docs/money.md +81 -0
- package/docs/plugins.md +136 -0
- package/docs/reports.md +154 -0
- package/docs/repositories.md +239 -0
- package/docs/schemas.md +146 -0
- package/docs/subledger-integration.md +287 -0
- package/package.json +116 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.mjs","names":[],"sources":["../src/money.ts"],"sourcesContent":["/**\n * Money — Integer-cents arithmetic helpers for safe financial computation.\n *\n * Provides utilities that operate on **integer minor-unit values** (cents) to\n * avoid floating-point rounding errors in intermediate calculations.\n *\n * **DB storage contract:**\n * Journal entry `debit` / `credit` / `totalDebit` / `totalCredit` fields are\n * stored as **integer cents** (e.g. 10050 for $100.50). All report outputs,\n * aggregation results, and repository methods return integer cents.\n *\n * Use `Money.fromDecimal()` to convert user-facing dollar inputs to cents\n * at the HTTP/API boundary. Use `Money.toDecimal()` or `Money.formatPlain()`\n * to convert cents back to dollars for display or CSV export.\n *\n * Example workflow:\n * const cents = Money.fromDecimal(req.body.debit); // 100.50 → 10050\n * const taxCents = Money.percentage(cents, 5); // 5% → 502 (rounded)\n * const display = Money.formatPlain(taxCents); // 502 → \"5.02\"\n *\n * Inspired by Stripe's money handling — simple, correct, auditable.\n *\n * @module @classytic/ledger/money\n */\n\n// ─── Core Arithmetic ─────────────────────────────────────────────────────────\n\n/** Round a floating-point value to the nearest integer cent */\nexport function round(amount: number): number {\n return Math.round(amount);\n}\n\n/** Convert a decimal dollar amount to integer cents: 10.50 → 1050 */\nexport function fromDecimal(dollars: number, minorUnit = 2): number {\n const factor = 10 ** minorUnit;\n return Math.round(dollars * factor);\n}\n\n/** Convert integer cents to a decimal dollar amount: 1050 → 10.50 */\nexport function toDecimal(cents: number, minorUnit = 2): number {\n const factor = 10 ** minorUnit;\n return cents / factor;\n}\n\n/** Add two cent amounts */\nexport function add(a: number, b: number): number {\n return a + b;\n}\n\n/** Subtract b from a in cents */\nexport function subtract(a: number, b: number): number {\n return a - b;\n}\n\n/** Multiply cents by a factor, rounding to nearest cent */\nexport function multiply(cents: number, factor: number): number {\n return Math.round(cents * factor);\n}\n\n/**\n * Calculate a percentage of a cent amount.\n * percentage(10000, 5) → 500 (5% of $100.00 = $5.00)\n */\nexport function percentage(cents: number, rate: number): number {\n return Math.round((cents * rate) / 100);\n}\n\n/**\n * Calculate tax from a tax-inclusive amount.\n * splitTaxInclusive(10500, 0.05) → { base: 10000, tax: 500 }\n */\nexport function splitTaxInclusive(\n inclusiveAmount: number,\n taxRate: number,\n): { base: number; tax: number } {\n const base = Math.round(inclusiveAmount / (1 + taxRate));\n const tax = inclusiveAmount - base;\n return { base, tax };\n}\n\n/**\n * Calculate tax from a tax-exclusive amount.\n * splitTaxExclusive(10000, 0.05) → { base: 10000, tax: 500, total: 10500 }\n */\nexport function splitTaxExclusive(\n exclusiveAmount: number,\n taxRate: number,\n): { base: number; tax: number; total: number } {\n const tax = Math.round(exclusiveAmount * taxRate);\n return { base: exclusiveAmount, tax, total: exclusiveAmount + tax };\n}\n\n// ─── Allocation ──────────────────────────────────────────────────────────────\n\n/**\n * Allocate cents across ratios with zero remainder error.\n * Uses largest-remainder method (same as parliamentary seat allocation).\n *\n * allocate(1000, [1, 1, 1]) → [334, 333, 333] (sums to 1000 exactly)\n * allocate(10000, [50, 30, 20]) → [5000, 3000, 2000]\n */\nexport function allocate(totalCents: number, ratios: number[]): number[] {\n if (ratios.length === 0) throw new Error('Ratios must be non-empty');\n if (ratios.some(r => r < 0)) throw new Error('Ratios must be non-negative');\n\n const ratioSum = ratios.reduce((s, r) => s + r, 0);\n if (ratioSum === 0) throw new Error('Sum of ratios must be > 0');\n\n // Base allocation (floor)\n const allocations = ratios.map(r => Math.floor((totalCents * r) / ratioSum));\n let remainder = totalCents - allocations.reduce((s, a) => s + a, 0);\n\n // Distribute remainder by largest fractional part\n const fractions = ratios.map((r, i) => ({\n index: i,\n frac: ((totalCents * r) / ratioSum) - allocations[i],\n }));\n fractions.sort((a, b) => b.frac - a.frac);\n\n for (let i = 0; i < remainder; i++) {\n allocations[fractions[i].index]++;\n }\n\n return allocations;\n}\n\n// ─── Comparison ──────────────────────────────────────────────────────────────\n\n/** Are two cent amounts equal? */\nexport function equals(a: number, b: number): boolean {\n return a === b;\n}\n\n/** Is the amount zero? */\nexport function isZero(cents: number): boolean {\n return cents === 0;\n}\n\n/** Is the amount positive (> 0)? */\nexport function isPositive(cents: number): boolean {\n return cents > 0;\n}\n\n/** Is the amount negative (< 0)? */\nexport function isNegative(cents: number): boolean {\n return cents < 0;\n}\n\n/** Absolute value */\nexport function abs(cents: number): number {\n return Math.abs(cents);\n}\n\n/** Negate */\nexport function negate(cents: number): number {\n return -cents;\n}\n\n/** Min of two amounts */\nexport function min(a: number, b: number): number {\n return Math.min(a, b);\n}\n\n/** Max of two amounts */\nexport function max(a: number, b: number): number {\n return Math.max(a, b);\n}\n\n// ─── Formatting ──────────────────────────────────────────────────────────────\n\n/**\n * Format cents as a currency string.\n * format(10550, 'CAD') → \"$105.50\"\n * format(10550, 'CAD', 'en-CA') → \"$105.50\"\n */\nexport function format(\n cents: number,\n currencyCode = 'CAD',\n locale = 'en-CA',\n minorUnit = 2,\n): string {\n const dollars = toDecimal(cents, minorUnit);\n return new Intl.NumberFormat(locale, {\n style: 'currency',\n currency: currencyCode,\n }).format(dollars);\n}\n\n/**\n * Format cents as a plain decimal string (no currency symbol).\n * formatPlain(10550) → \"105.50\"\n */\nexport function formatPlain(cents: number, minorUnit = 2): string {\n return toDecimal(cents, minorUnit).toFixed(minorUnit);\n}\n\n// ─── Validation ──────────────────────────────────────────────────────────────\n\n/** Is the value a valid integer cent amount? */\nexport function isValid(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value);\n}\n\n/**\n * Parse a string or number into cents.\n * parseCents(\"105.50\") → 10550\n * parseCents(105.50) → 10550\n */\nexport function parseCents(input: string | number, minorUnit = 2): number {\n if (typeof input === 'number') return fromDecimal(input, minorUnit);\n\n const cleaned = input.replace(/[$,\\s]/g, '');\n const parsed = parseFloat(cleaned);\n if (isNaN(parsed)) throw new Error(`Cannot parse \"${input}\" as money`);\n return fromDecimal(parsed, minorUnit);\n}\n\n// ─── Bundled Export ──────────────────────────────────────────────────────────\n\nexport const Money = {\n round,\n fromDecimal,\n toDecimal,\n add,\n subtract,\n multiply,\n percentage,\n splitTaxInclusive,\n splitTaxExclusive,\n allocate,\n equals,\n isZero,\n isPositive,\n isNegative,\n abs,\n negate,\n min,\n max,\n format,\n formatPlain,\n isValid,\n parseCents,\n} as const;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as DoubleEntryPluginOptions, i as fiscalLockPlugin, n as idempotencyPlugin, o as doubleEntryPlugin, r as FiscalLockPluginOptions, t as IdempotencyPluginOptions } from "../idempotency.plugin-BESs9YPD.mjs";
|
|
2
|
+
export { type DoubleEntryPluginOptions, type FiscalLockPluginOptions, type IdempotencyPluginOptions, doubleEntryPlugin, fiscalLockPlugin, idempotencyPlugin };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as reopenFiscalPeriod, c as GeneralLedgerOptions, d as generateIncomeStatement, f as BalanceSheetOptions, h as generateTrialBalance, i as closeFiscalPeriod, l as generateGeneralLedger, m as TrialBalanceOptions, n as FiscalCloseResult, o as CashFlowOptions, p as generateBalanceSheet, r as FiscalReopenResult, s as generateCashFlow, t as FiscalCloseOptions, u as IncomeStatementOptions } from "../fiscal-close-CzUzpnMg.mjs";
|
|
2
|
+
export { type BalanceSheetOptions, type CashFlowOptions, type FiscalCloseOptions, type FiscalCloseResult, type FiscalReopenResult, type GeneralLedgerOptions, type IncomeStatementOptions, type TrialBalanceOptions, closeFiscalPeriod, generateBalanceSheet, generateCashFlow, generateGeneralLedger, generateIncomeStatement, generateTrialBalance, reopenFiscalPeriod };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as generateIncomeStatement, d as generateTrialBalance, i as generateGeneralLedger, n as reopenFiscalPeriod, o as generateBalanceSheet, r as generateCashFlow, t as closeFiscalPeriod } from "../fiscal-close-CNOwv_ud.mjs";
|
|
2
|
+
|
|
3
|
+
export { closeFiscalPeriod, generateBalanceSheet, generateCashFlow, generateGeneralLedger, generateIncomeStatement, generateTrialBalance, reopenFiscalPeriod };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { n as Errors } from "./errors-CeqRahE-.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/tenant-guard.ts
|
|
4
|
+
/**
|
|
5
|
+
* Multi-tenant scope guard.
|
|
6
|
+
*
|
|
7
|
+
* Throws when orgField is configured (multi-tenant mode active) but
|
|
8
|
+
* organizationId is missing — preventing unscoped cross-tenant queries.
|
|
9
|
+
*/
|
|
10
|
+
function requireOrgScope(orgField, organizationId) {
|
|
11
|
+
if (orgField && !organizationId) throw Errors.validation("organizationId is required when multi-tenant mode is configured (orgField: \"" + orgField + "\"). Refusing to run unscoped query.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/utils/logger.ts
|
|
16
|
+
/** Default console-based implementation */
|
|
17
|
+
const defaultLogger = {
|
|
18
|
+
warn: (msg, meta) => console.warn(`[accounting] ${msg}`, meta ?? ""),
|
|
19
|
+
error: (msg, meta) => console.error(`[accounting] ${msg}`, meta ?? ""),
|
|
20
|
+
info: (msg, meta) => console.info(`[accounting] ${msg}`, meta ?? "")
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/utils/session.ts
|
|
25
|
+
/**
|
|
26
|
+
* Acquire a session: uses external if provided, otherwise creates an internal one.
|
|
27
|
+
* Returns { session, ownSession } so callers can commit/abort/end appropriately.
|
|
28
|
+
*
|
|
29
|
+
* When transactions are unavailable (no replica set / standalone), returns
|
|
30
|
+
* session=null and the function runs without transactional safety.
|
|
31
|
+
*/
|
|
32
|
+
async function acquireSession(db, externalSession, logger = defaultLogger) {
|
|
33
|
+
if (externalSession) return {
|
|
34
|
+
session: externalSession,
|
|
35
|
+
ownSession: false
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const session = await db.startSession();
|
|
39
|
+
try {
|
|
40
|
+
if ((db.getClient?.() ?? db.client)?.topology?.description?.type === "Single") {
|
|
41
|
+
session.endSession();
|
|
42
|
+
logger.warn("Transactions unavailable (standalone MongoDB). Operation is not atomic.");
|
|
43
|
+
return {
|
|
44
|
+
session: null,
|
|
45
|
+
ownSession: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
try {
|
|
50
|
+
session.startTransaction();
|
|
51
|
+
return {
|
|
52
|
+
session,
|
|
53
|
+
ownSession: true
|
|
54
|
+
};
|
|
55
|
+
} catch (err) {
|
|
56
|
+
session.endSession();
|
|
57
|
+
logger.warn("Transactions unavailable (no replica set). Operation is not atomic.", { error: err.message });
|
|
58
|
+
return {
|
|
59
|
+
session: null,
|
|
60
|
+
ownSession: false
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
return {
|
|
65
|
+
session: null,
|
|
66
|
+
ownSession: false
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Finalize an owned session: commit or abort, then always end.
|
|
72
|
+
*/
|
|
73
|
+
async function finalizeSession(session, ownSession, success) {
|
|
74
|
+
if (!ownSession || !session) return;
|
|
75
|
+
try {
|
|
76
|
+
if (success && session.inTransaction()) await session.commitTransaction();
|
|
77
|
+
else if (!success && session.inTransaction()) try {
|
|
78
|
+
await session.abortTransaction();
|
|
79
|
+
} catch {}
|
|
80
|
+
} finally {
|
|
81
|
+
session.endSession();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { requireOrgScope as i, finalizeSession as n, defaultLogger as r, acquireSession as t };
|
|
87
|
+
//# sourceMappingURL=session-Dh0s6zG4.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-Dh0s6zG4.mjs","names":[],"sources":["../src/utils/tenant-guard.ts","../src/utils/logger.ts","../src/utils/session.ts"],"sourcesContent":["import { Errors } from './errors.js';\r\n\r\n/**\r\n * Multi-tenant scope guard.\r\n *\r\n * Throws when orgField is configured (multi-tenant mode active) but\r\n * organizationId is missing — preventing unscoped cross-tenant queries.\r\n */\r\nexport function requireOrgScope(orgField: string | undefined, organizationId: unknown): void {\r\n if (orgField && !organizationId) {\r\n throw Errors.validation(\r\n 'organizationId is required when multi-tenant mode is configured (orgField: \"' +\r\n orgField +\r\n '\"). Refusing to run unscoped query.',\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Build org-scoped query filter.\r\n * Returns an object like `{ business: orgId }` or `{}`.\r\n */\r\nexport function orgFilter(orgField: string | undefined, organizationId: unknown): Record<string, unknown> {\r\n if (!orgField) return {};\r\n // requireOrgScope should be called first; this is a convenience builder\r\n return organizationId ? { [orgField]: organizationId } : {};\r\n}\r\n","/**\r\n * Minimal logger interface for the accounting package.\r\n * Defaults to console. App layer can inject a real logger (Winston/Pino).\r\n */\r\nexport interface Logger {\r\n warn(message: string, meta?: Record<string, unknown>): void;\r\n error(message: string, meta?: Record<string, unknown>): void;\r\n info(message: string, meta?: Record<string, unknown>): void;\r\n}\r\n\r\n/** Default console-based implementation */\r\nexport const defaultLogger: Logger = {\r\n warn: (msg, meta) => console.warn(`[accounting] ${msg}`, meta ?? ''),\r\n error: (msg, meta) => console.error(`[accounting] ${msg}`, meta ?? ''),\r\n info: (msg, meta) => console.info(`[accounting] ${msg}`, meta ?? ''),\r\n};\r\n","/**\r\n * Shared session/transaction helpers.\r\n *\r\n * Provides a safe acquire/finalize pattern that:\r\n * - Detects standalone topology (no transactions) before starting one\r\n * - Manages commit/abort lifecycle with proper cleanup\r\n * - Always calls endSession() even when abort fails\r\n *\r\n * NOTE: session.startTransaction() is synchronous and does NOT throw on\r\n * standalone MongoDB — the \"Transaction numbers are only allowed on a replica\r\n * set member or mongos\" error only surfaces at the first database operation.\r\n * We detect standalone topology proactively via the driver's topology\r\n * description to avoid this trap.\r\n */\r\n\r\nimport type { ClientSession, Connection } from 'mongoose';\r\nimport type { Logger } from './logger.js';\r\nimport { defaultLogger } from './logger.js';\r\n\r\nexport interface SessionResult {\r\n session: ClientSession | null;\r\n ownSession: boolean;\r\n}\r\n\r\n/**\r\n * Acquire a session: uses external if provided, otherwise creates an internal one.\r\n * Returns { session, ownSession } so callers can commit/abort/end appropriately.\r\n *\r\n * When transactions are unavailable (no replica set / standalone), returns\r\n * session=null and the function runs without transactional safety.\r\n */\r\nexport async function acquireSession(\r\n db: Connection,\r\n externalSession: ClientSession | undefined | null,\r\n logger: Logger = defaultLogger,\r\n): Promise<SessionResult> {\r\n if (externalSession) {\r\n return { session: externalSession, ownSession: false };\r\n }\r\n\r\n try {\r\n const session = await db.startSession();\r\n\r\n // Detect standalone topology before starting a transaction.\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const client = (db as any).getClient?.() ?? (db as any).client;\r\n const topologyType = client?.topology?.description?.type;\r\n if (topologyType === 'Single') {\r\n session.endSession();\r\n logger.warn(\r\n 'Transactions unavailable (standalone MongoDB). Operation is not atomic.',\r\n );\r\n return { session: null, ownSession: false };\r\n }\r\n } catch {\r\n // Topology detection failed — proceed optimistically\r\n }\r\n\r\n try {\r\n session.startTransaction();\r\n return { session, ownSession: true };\r\n } catch (err) {\r\n // startTransaction failed for unexpected reasons — clean up and fall back\r\n session.endSession();\r\n logger.warn(\r\n 'Transactions unavailable (no replica set). Operation is not atomic.',\r\n { error: (err as Error).message },\r\n );\r\n return { session: null, ownSession: false };\r\n }\r\n } catch {\r\n // startSession itself failed — run without session\r\n return { session: null, ownSession: false };\r\n }\r\n}\r\n\r\n/**\r\n * Finalize an owned session: commit or abort, then always end.\r\n */\r\nexport async function finalizeSession(\r\n session: ClientSession | null,\r\n ownSession: boolean,\r\n success: boolean,\r\n): Promise<void> {\r\n if (!ownSession || !session) return;\r\n\r\n try {\r\n if (success && session.inTransaction()) {\r\n await session.commitTransaction();\r\n } else if (!success && session.inTransaction()) {\r\n try {\r\n await session.abortTransaction();\r\n } catch {\r\n // Swallow abort errors — the original error is more important\r\n }\r\n }\r\n } finally {\r\n session.endSession();\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;AAQA,SAAgB,gBAAgB,UAA8B,gBAA+B;AAC3F,KAAI,YAAY,CAAC,eACf,OAAM,OAAO,WACX,kFACA,WACA,uCACD;;;;;;ACHL,MAAa,gBAAwB;CACnC,OAAO,KAAK,SAAS,QAAQ,KAAK,gBAAgB,OAAO,QAAQ,GAAG;CACpE,QAAQ,KAAK,SAAS,QAAQ,MAAM,gBAAgB,OAAO,QAAQ,GAAG;CACtE,OAAO,KAAK,SAAS,QAAQ,KAAK,gBAAgB,OAAO,QAAQ,GAAG;CACrE;;;;;;;;;;;ACgBD,eAAsB,eACpB,IACA,iBACA,SAAiB,eACO;AACxB,KAAI,gBACF,QAAO;EAAE,SAAS;EAAiB,YAAY;EAAO;AAGxD,KAAI;EACF,MAAM,UAAU,MAAM,GAAG,cAAc;AAGvC,MAAI;AAIF,QAFgB,GAAW,aAAa,IAAK,GAAW,SAC3B,UAAU,aAAa,SAC/B,UAAU;AAC7B,YAAQ,YAAY;AACpB,WAAO,KACL,0EACD;AACD,WAAO;KAAE,SAAS;KAAM,YAAY;KAAO;;UAEvC;AAIR,MAAI;AACF,WAAQ,kBAAkB;AAC1B,UAAO;IAAE;IAAS,YAAY;IAAM;WAC7B,KAAK;AAEZ,WAAQ,YAAY;AACpB,UAAO,KACL,uEACA,EAAE,OAAQ,IAAc,SAAS,CAClC;AACD,UAAO;IAAE,SAAS;IAAM,YAAY;IAAO;;SAEvC;AAEN,SAAO;GAAE,SAAS;GAAM,YAAY;GAAO;;;;;;AAO/C,eAAsB,gBACpB,SACA,YACA,SACe;AACf,KAAI,CAAC,cAAc,CAAC,QAAS;AAE7B,KAAI;AACF,MAAI,WAAW,QAAQ,eAAe,CACpC,OAAM,QAAQ,mBAAmB;WACxB,CAAC,WAAW,QAAQ,eAAe,CAC5C,KAAI;AACF,SAAM,QAAQ,kBAAkB;UAC1B;WAIF;AACR,UAAQ,YAAY"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Money } from "./money.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/exports/csv-serializer.ts
|
|
4
|
+
const NEEDS_QUOTING = /[",\r\n]/;
|
|
5
|
+
/** Escape a single CSV cell value per RFC 4180. */
|
|
6
|
+
function escapeCell(value) {
|
|
7
|
+
if (NEEDS_QUOTING.test(value)) return "\"" + value.replace(/"/g, "\"\"") + "\"";
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
/** Serialize a 2D array of strings into a CSV string. */
|
|
11
|
+
function serializeCsv(rows, options = {}) {
|
|
12
|
+
const { delimiter = ",", lineTerminator = "\r\n" } = options;
|
|
13
|
+
return rows.map((row) => row.map(escapeCell).join(delimiter)).join(lineTerminator);
|
|
14
|
+
}
|
|
15
|
+
/** Build a CSV string with optional header row. */
|
|
16
|
+
function buildCsv(headers, dataRows, options = {}) {
|
|
17
|
+
const { includeHeaders = true } = options;
|
|
18
|
+
return serializeCsv(includeHeaders ? [headers, ...dataRows] : [...dataRows], options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/exports/field-map.ts
|
|
23
|
+
/** Extract headers from a field map. */
|
|
24
|
+
function getHeaders(fieldMap) {
|
|
25
|
+
return fieldMap.fields.map((f) => f.header);
|
|
26
|
+
}
|
|
27
|
+
/** Apply a field map to a single row, producing an array of cell strings. */
|
|
28
|
+
function extractRow(fieldMap, row) {
|
|
29
|
+
return fieldMap.fields.map((f) => f.extract(row));
|
|
30
|
+
}
|
|
31
|
+
/** Apply a field map to an array of rows, producing a 2D string array. */
|
|
32
|
+
function extractAllRows(fieldMap, rows) {
|
|
33
|
+
return rows.map((row) => extractRow(fieldMap, row));
|
|
34
|
+
}
|
|
35
|
+
/** One-shot: map + serialize to CSV string. */
|
|
36
|
+
function exportToCsv(fieldMap, rows, options) {
|
|
37
|
+
return buildCsv(getHeaders(fieldMap), extractAllRows(fieldMap, rows), options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/exports/flatten-journal.ts
|
|
42
|
+
function toDate(value) {
|
|
43
|
+
if (!value) return /* @__PURE__ */ new Date(0);
|
|
44
|
+
if (value instanceof Date) return value;
|
|
45
|
+
return new Date(value);
|
|
46
|
+
}
|
|
47
|
+
function resolveAccount(account) {
|
|
48
|
+
if (!account) return {
|
|
49
|
+
id: "",
|
|
50
|
+
name: "",
|
|
51
|
+
typeCode: ""
|
|
52
|
+
};
|
|
53
|
+
if (typeof account === "string") return {
|
|
54
|
+
id: account,
|
|
55
|
+
name: "",
|
|
56
|
+
typeCode: ""
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
id: String(account._id ?? ""),
|
|
60
|
+
name: account.name ?? "",
|
|
61
|
+
typeCode: account.accountTypeCode ?? ""
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Flatten a single journal entry into one FlatJournalRow per journal item. */
|
|
65
|
+
function flattenJournalEntry(entry) {
|
|
66
|
+
const entryDate = toDate(entry.date);
|
|
67
|
+
const items = entry.journalItems ?? [];
|
|
68
|
+
const itemCount = items.length;
|
|
69
|
+
const KNOWN_ITEM_KEYS = new Set([
|
|
70
|
+
"account",
|
|
71
|
+
"label",
|
|
72
|
+
"date",
|
|
73
|
+
"debit",
|
|
74
|
+
"credit",
|
|
75
|
+
"taxDetails"
|
|
76
|
+
]);
|
|
77
|
+
return items.map((item, index) => {
|
|
78
|
+
const acct = resolveAccount(item.account);
|
|
79
|
+
const firstTax = item.taxDetails?.[0];
|
|
80
|
+
const extraItemFields = {};
|
|
81
|
+
for (const key of Object.keys(item)) if (!KNOWN_ITEM_KEYS.has(key)) extraItemFields[key] = item[key];
|
|
82
|
+
return {
|
|
83
|
+
entryId: String(entry._id ?? ""),
|
|
84
|
+
journalType: entry.journalType ?? "",
|
|
85
|
+
referenceNumber: entry.referenceNumber ?? "",
|
|
86
|
+
entryLabel: entry.label ?? "",
|
|
87
|
+
entryDate,
|
|
88
|
+
state: entry.state,
|
|
89
|
+
reversed: entry.reversed ?? false,
|
|
90
|
+
totalDebit: entry.totalDebit ?? 0,
|
|
91
|
+
totalCredit: entry.totalCredit ?? 0,
|
|
92
|
+
accountId: acct.id,
|
|
93
|
+
accountName: acct.name,
|
|
94
|
+
accountTypeCode: acct.typeCode,
|
|
95
|
+
itemLabel: item.label ?? "",
|
|
96
|
+
itemDate: item.date ? toDate(item.date) : entryDate,
|
|
97
|
+
debit: item.debit ?? 0,
|
|
98
|
+
credit: item.credit ?? 0,
|
|
99
|
+
taxCode: firstTax?.taxCode ?? "",
|
|
100
|
+
taxName: firstTax?.taxName ?? "",
|
|
101
|
+
itemIndex: index,
|
|
102
|
+
itemCount,
|
|
103
|
+
...extraItemFields
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Flatten multiple journal entries into a single flat row array. */
|
|
108
|
+
function flattenJournalEntries(entries) {
|
|
109
|
+
const rows = [];
|
|
110
|
+
for (const entry of entries) rows.push(...flattenJournalEntry(entry));
|
|
111
|
+
return rows;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/exports/field-maps/quickbooks.ts
|
|
116
|
+
function formatQbDate(date) {
|
|
117
|
+
return `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}/${date.getFullYear()}`;
|
|
118
|
+
}
|
|
119
|
+
/** Convert integer cents to dollar string, blank for zero. */
|
|
120
|
+
function amountOrBlank(cents) {
|
|
121
|
+
if (cents === 0) return "";
|
|
122
|
+
return Money.formatPlain(cents);
|
|
123
|
+
}
|
|
124
|
+
const quickbooksFieldMap = {
|
|
125
|
+
name: "QuickBooks General Journal",
|
|
126
|
+
target: "quickbooks",
|
|
127
|
+
fields: [
|
|
128
|
+
{
|
|
129
|
+
header: "Date",
|
|
130
|
+
extract: (row) => formatQbDate(row.entryDate)
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
header: "Transaction Type",
|
|
134
|
+
extract: () => "General Journal"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
header: "Num",
|
|
138
|
+
extract: (row) => row.referenceNumber
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
header: "Name",
|
|
142
|
+
extract: () => ""
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
header: "Memo/Description",
|
|
146
|
+
extract: (row) => row.itemLabel || row.entryLabel
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
header: "Account",
|
|
150
|
+
extract: (row) => row.accountName || row.accountTypeCode || row.accountId
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
header: "Debit",
|
|
154
|
+
extract: (row) => amountOrBlank(row.debit)
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
header: "Credit",
|
|
158
|
+
extract: (row) => amountOrBlank(row.credit)
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
header: "Class",
|
|
162
|
+
extract: () => ""
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/exports/field-maps/universal.ts
|
|
169
|
+
function formatIsoDate(date) {
|
|
170
|
+
return date.toISOString().split("T")[0];
|
|
171
|
+
}
|
|
172
|
+
/** Convert integer cents to dollar string (e.g. 10050 → "100.50"). */
|
|
173
|
+
function centsToDisplay(cents) {
|
|
174
|
+
return Money.formatPlain(cents);
|
|
175
|
+
}
|
|
176
|
+
const universalFieldMap = {
|
|
177
|
+
name: "Universal Journal Export",
|
|
178
|
+
target: "universal",
|
|
179
|
+
fields: [
|
|
180
|
+
{
|
|
181
|
+
header: "Entry ID",
|
|
182
|
+
extract: (row) => row.entryId
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
header: "Date",
|
|
186
|
+
extract: (row) => formatIsoDate(row.entryDate)
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
header: "Journal Type",
|
|
190
|
+
extract: (row) => row.journalType
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
header: "Reference Number",
|
|
194
|
+
extract: (row) => row.referenceNumber
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
header: "Entry Description",
|
|
198
|
+
extract: (row) => row.entryLabel
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
header: "State",
|
|
202
|
+
extract: (row) => row.state
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
header: "Reversed",
|
|
206
|
+
extract: (row) => row.reversed ? "Yes" : "No"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
header: "Account Code",
|
|
210
|
+
extract: (row) => row.accountTypeCode
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
header: "Account Name",
|
|
214
|
+
extract: (row) => row.accountName
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
header: "Line Description",
|
|
218
|
+
extract: (row) => row.itemLabel
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
header: "Debit",
|
|
222
|
+
extract: (row) => centsToDisplay(row.debit)
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
header: "Credit",
|
|
226
|
+
extract: (row) => centsToDisplay(row.credit)
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
header: "Tax Code",
|
|
230
|
+
extract: (row) => row.taxCode
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
header: "Tax Name",
|
|
234
|
+
extract: (row) => row.taxName
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
header: "Entry Total Debit",
|
|
238
|
+
extract: (row) => centsToDisplay(row.totalDebit)
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
header: "Entry Total Credit",
|
|
242
|
+
extract: (row) => centsToDisplay(row.totalCredit)
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
header: "Line",
|
|
246
|
+
extract: (row) => String(row.itemIndex + 1)
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
header: "Line Count",
|
|
250
|
+
extract: (row) => String(row.itemCount)
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
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 };
|
|
257
|
+
//# sourceMappingURL=universal-CMfrZ2hG.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"universal-CMfrZ2hG.mjs","names":[],"sources":["../src/exports/csv-serializer.ts","../src/exports/field-map.ts","../src/exports/flatten-journal.ts","../src/exports/field-maps/quickbooks.ts","../src/exports/field-maps/universal.ts"],"sourcesContent":["/**\n * CSV Serializer — RFC 4180 compliant CSV string builder.\n *\n * Pure function. No I/O, no side effects.\n *\n * @module @classytic/ledger/exports\n */\n\nimport type { CsvOptions } from './types.js';\n\nconst NEEDS_QUOTING = /[\",\\r\\n]/;\n\n/** Escape a single CSV cell value per RFC 4180. */\nexport function escapeCell(value: string): string {\n if (NEEDS_QUOTING.test(value)) {\n return '\"' + value.replace(/\"/g, '\"\"') + '\"';\n }\n return value;\n}\n\n/** Serialize a 2D array of strings into a CSV string. */\nexport function serializeCsv(\n rows: readonly (readonly string[])[],\n options: CsvOptions = {},\n): string {\n const { delimiter = ',', lineTerminator = '\\r\\n' } = options;\n\n return rows\n .map(row => row.map(escapeCell).join(delimiter))\n .join(lineTerminator);\n}\n\n/** Build a CSV string with optional header row. */\nexport function buildCsv(\n headers: readonly string[],\n dataRows: readonly (readonly string[])[],\n options: CsvOptions = {},\n): string {\n const { includeHeaders = true } = options;\n const allRows = includeHeaders ? [headers, ...dataRows] : [...dataRows];\n return serializeCsv(allRows, options);\n}\n","/**\n * Field Map Application — Applies an ExportFieldMap to data rows.\n *\n * @module @classytic/ledger/exports\n */\n\nimport type { ExportFieldMap, CsvOptions } from './types.js';\nimport { buildCsv } from './csv-serializer.js';\n\n/** Extract headers from a field map. */\nexport function getHeaders<TRow>(fieldMap: ExportFieldMap<TRow>): string[] {\n return fieldMap.fields.map(f => f.header);\n}\n\n/** Apply a field map to a single row, producing an array of cell strings. */\nexport function extractRow<TRow>(fieldMap: ExportFieldMap<TRow>, row: TRow): string[] {\n return fieldMap.fields.map(f => f.extract(row));\n}\n\n/** Apply a field map to an array of rows, producing a 2D string array. */\nexport function extractAllRows<TRow>(\n fieldMap: ExportFieldMap<TRow>,\n rows: readonly TRow[],\n): string[][] {\n return rows.map(row => extractRow(fieldMap, row));\n}\n\n/** One-shot: map + serialize to CSV string. */\nexport function exportToCsv<TRow>(\n fieldMap: ExportFieldMap<TRow>,\n rows: readonly TRow[],\n options?: CsvOptions,\n): string {\n const headers = getHeaders(fieldMap);\n const dataRows = extractAllRows(fieldMap, rows);\n return buildCsv(headers, dataRows, options);\n}\n","/**\n * Journal Entry Flattener — Denormalizes journal entries into flat rows.\n *\n * Each journal item becomes one row with entry-level fields repeated.\n * Monetary values stay as integer cents (matching DB storage).\n *\n * @module @classytic/ledger/exports\n */\n\nimport type {\n PopulatedJournalEntry,\n PopulatedAccount,\n FlatJournalRow,\n} from './types.js';\n\nfunction toDate(value: Date | string | undefined | null): Date {\n if (!value) return new Date(0);\n if (value instanceof Date) return value;\n return new Date(value);\n}\n\nfunction resolveAccount(\n account: PopulatedAccount | string | null | undefined,\n): { id: string; name: string; typeCode: string } {\n if (!account) return { id: '', name: '', typeCode: '' };\n if (typeof account === 'string') return { id: account, name: '', typeCode: '' };\n return {\n id: String(account._id ?? ''),\n name: account.name ?? '',\n typeCode: account.accountTypeCode ?? '',\n };\n}\n\n/** Flatten a single journal entry into one FlatJournalRow per journal item. */\nexport function flattenJournalEntry(entry: PopulatedJournalEntry): FlatJournalRow[] {\n const entryDate = toDate(entry.date);\n const items = entry.journalItems ?? [];\n const itemCount = items.length;\n\n // Known core item keys — everything else is an extra dimension field\n const KNOWN_ITEM_KEYS = new Set(['account', 'label', 'date', 'debit', 'credit', 'taxDetails']);\n\n return items.map((item, index) => {\n const acct = resolveAccount(item.account);\n const firstTax = item.taxDetails?.[0];\n\n // Collect extra item fields (dimensions like departmentId, projectId, etc.)\n const extraItemFields: Record<string, unknown> = {};\n for (const key of Object.keys(item)) {\n if (!KNOWN_ITEM_KEYS.has(key)) {\n extraItemFields[key] = (item as Record<string, unknown>)[key];\n }\n }\n\n return {\n entryId: String(entry._id ?? ''),\n journalType: entry.journalType ?? '',\n referenceNumber: entry.referenceNumber ?? '',\n entryLabel: entry.label ?? '',\n entryDate,\n state: entry.state,\n reversed: entry.reversed ?? false,\n totalDebit: entry.totalDebit ?? 0,\n totalCredit: entry.totalCredit ?? 0,\n\n accountId: acct.id,\n accountName: acct.name,\n accountTypeCode: acct.typeCode,\n itemLabel: item.label ?? '',\n itemDate: item.date ? toDate(item.date) : entryDate,\n debit: item.debit ?? 0,\n credit: item.credit ?? 0,\n taxCode: firstTax?.taxCode ?? '',\n taxName: firstTax?.taxName ?? '',\n\n itemIndex: index,\n itemCount,\n ...extraItemFields,\n };\n });\n}\n\n/** Flatten multiple journal entries into a single flat row array. */\nexport function flattenJournalEntries(\n entries: readonly PopulatedJournalEntry[],\n): FlatJournalRow[] {\n const rows: FlatJournalRow[] = [];\n for (const entry of entries) {\n rows.push(...flattenJournalEntry(entry));\n }\n return rows;\n}\n","/**\n * QuickBooks General Journal Import Field Map\n *\n * Produces CSV compatible with QuickBooks Desktop and Online\n * \"Import General Journal Entries\" feature.\n *\n * @module @classytic/ledger/exports\n */\n\nimport type { ExportFieldMap, FlatJournalRow } from '../types.js';\nimport { Money } from '../../money.js';\n\nfunction formatQbDate(date: Date): string {\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n const y = date.getFullYear();\n return `${m}/${d}/${y}`;\n}\n\n/** Convert integer cents to dollar string, blank for zero. */\nfunction amountOrBlank(cents: number): string {\n if (cents === 0) return '';\n return Money.formatPlain(cents);\n}\n\nexport const quickbooksFieldMap: ExportFieldMap<FlatJournalRow> = {\n name: 'QuickBooks General Journal',\n target: 'quickbooks',\n fields: [\n { header: 'Date', extract: (row) => formatQbDate(row.entryDate) },\n { header: 'Transaction Type', extract: () => 'General Journal' },\n { header: 'Num', extract: (row) => row.referenceNumber },\n { header: 'Name', extract: () => '' },\n { header: 'Memo/Description', extract: (row) => row.itemLabel || row.entryLabel },\n { header: 'Account', extract: (row) => row.accountName || row.accountTypeCode || row.accountId },\n { header: 'Debit', extract: (row) => amountOrBlank(row.debit) },\n { header: 'Credit', extract: (row) => amountOrBlank(row.credit) },\n { header: 'Class', extract: () => '' },\n ],\n};\n","/**\n * Universal Export Field Map\n *\n * Comprehensive CSV export with all available fields.\n * Useful for data portability, auditing, or spreadsheet import.\n *\n * @module @classytic/ledger/exports\n */\n\nimport type { ExportFieldMap, FlatJournalRow } from '../types.js';\nimport { Money } from '../../money.js';\n\nfunction formatIsoDate(date: Date): string {\n return date.toISOString().split('T')[0];\n}\n\n/** Convert integer cents to dollar string (e.g. 10050 → \"100.50\"). */\nfunction centsToDisplay(cents: number): string {\n return Money.formatPlain(cents);\n}\n\nexport const universalFieldMap: ExportFieldMap<FlatJournalRow> = {\n name: 'Universal Journal Export',\n target: 'universal',\n fields: [\n { header: 'Entry ID', extract: (row) => row.entryId },\n { header: 'Date', extract: (row) => formatIsoDate(row.entryDate) },\n { header: 'Journal Type', extract: (row) => row.journalType },\n { header: 'Reference Number', extract: (row) => row.referenceNumber },\n { header: 'Entry Description', extract: (row) => row.entryLabel },\n { header: 'State', extract: (row) => row.state },\n { header: 'Reversed', extract: (row) => row.reversed ? 'Yes' : 'No' },\n { header: 'Account Code', extract: (row) => row.accountTypeCode },\n { header: 'Account Name', extract: (row) => row.accountName },\n { header: 'Line Description', extract: (row) => row.itemLabel },\n { header: 'Debit', extract: (row) => centsToDisplay(row.debit) },\n { header: 'Credit', extract: (row) => centsToDisplay(row.credit) },\n { header: 'Tax Code', extract: (row) => row.taxCode },\n { header: 'Tax Name', extract: (row) => row.taxName },\n { header: 'Entry Total Debit', extract: (row) => centsToDisplay(row.totalDebit) },\n { header: 'Entry Total Credit',extract: (row) => centsToDisplay(row.totalCredit) },\n { header: 'Line', extract: (row) => String(row.itemIndex + 1) },\n { header: 'Line Count', extract: (row) => String(row.itemCount) },\n ],\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
//#region src/exports/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Export Types — Field maps, flat rows, and CSV builder configuration.
|
|
4
|
+
*
|
|
5
|
+
* All monetary values are in integer minor units (cents). For example,
|
|
6
|
+
* 10050 represents $100.50. Field maps convert to dollar strings at CSV output.
|
|
7
|
+
*
|
|
8
|
+
* @module @classytic/ledger/exports
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A populated account object (the shape after .populate('journalItems.account')).
|
|
12
|
+
* Only the fields the export module reads. Extra fields are ignored.
|
|
13
|
+
*/
|
|
14
|
+
interface PopulatedAccount {
|
|
15
|
+
_id: unknown;
|
|
16
|
+
accountTypeCode: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
active?: boolean;
|
|
19
|
+
isCashAccount?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A single journal item with its account populated to an object.
|
|
23
|
+
*/
|
|
24
|
+
interface PopulatedJournalItem {
|
|
25
|
+
account: PopulatedAccount | string;
|
|
26
|
+
label?: string;
|
|
27
|
+
date?: Date | string;
|
|
28
|
+
debit: number;
|
|
29
|
+
credit: number;
|
|
30
|
+
taxDetails?: Array<{
|
|
31
|
+
taxCode?: string;
|
|
32
|
+
taxName?: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Extra dimension fields from extraItemFields */
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A journal entry as returned from the DB after populate.
|
|
39
|
+
* The export module never queries the DB — it receives these plain objects.
|
|
40
|
+
*/
|
|
41
|
+
interface PopulatedJournalEntry {
|
|
42
|
+
_id?: unknown;
|
|
43
|
+
journalType: string;
|
|
44
|
+
referenceNumber: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
date: Date | string;
|
|
47
|
+
journalItems: PopulatedJournalItem[];
|
|
48
|
+
totalDebit: number;
|
|
49
|
+
totalCredit: number;
|
|
50
|
+
state: 'draft' | 'posted' | 'archived';
|
|
51
|
+
reversed?: boolean;
|
|
52
|
+
createdAt?: Date | string;
|
|
53
|
+
updatedAt?: Date | string;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* One flat row = one journal item with entry-level fields repeated.
|
|
58
|
+
* All monetary values are in integer minor units (cents).
|
|
59
|
+
*/
|
|
60
|
+
interface FlatJournalRow {
|
|
61
|
+
entryId: string;
|
|
62
|
+
journalType: string;
|
|
63
|
+
referenceNumber: string;
|
|
64
|
+
entryLabel: string;
|
|
65
|
+
entryDate: Date;
|
|
66
|
+
state: 'draft' | 'posted' | 'archived';
|
|
67
|
+
reversed: boolean;
|
|
68
|
+
totalDebit: number;
|
|
69
|
+
totalCredit: number;
|
|
70
|
+
accountId: string;
|
|
71
|
+
accountName: string;
|
|
72
|
+
accountTypeCode: string;
|
|
73
|
+
itemLabel: string;
|
|
74
|
+
itemDate: Date;
|
|
75
|
+
debit: number;
|
|
76
|
+
credit: number;
|
|
77
|
+
taxCode: string;
|
|
78
|
+
taxName: string;
|
|
79
|
+
itemIndex: number;
|
|
80
|
+
itemCount: number;
|
|
81
|
+
/** Extra dimension fields carried from journal items */
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A single export field definition.
|
|
86
|
+
* The extract function returns a string value for the CSV column.
|
|
87
|
+
*/
|
|
88
|
+
interface ExportField<TRow = FlatJournalRow> {
|
|
89
|
+
readonly header: string;
|
|
90
|
+
readonly extract: (row: TRow) => string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A named, reusable collection of export fields.
|
|
94
|
+
*/
|
|
95
|
+
interface ExportFieldMap<TRow = FlatJournalRow> {
|
|
96
|
+
readonly name: string;
|
|
97
|
+
readonly target: string;
|
|
98
|
+
readonly fields: readonly ExportField<TRow>[];
|
|
99
|
+
}
|
|
100
|
+
interface CsvOptions {
|
|
101
|
+
delimiter?: string;
|
|
102
|
+
lineTerminator?: string;
|
|
103
|
+
includeHeaders?: boolean;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/exports/csv-serializer.d.ts
|
|
107
|
+
/** Escape a single CSV cell value per RFC 4180. */
|
|
108
|
+
declare function escapeCell(value: string): string;
|
|
109
|
+
/** Serialize a 2D array of strings into a CSV string. */
|
|
110
|
+
declare function serializeCsv(rows: readonly (readonly string[])[], options?: CsvOptions): string;
|
|
111
|
+
/** Build a CSV string with optional header row. */
|
|
112
|
+
declare function buildCsv(headers: readonly string[], dataRows: readonly (readonly string[])[], options?: CsvOptions): string;
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/exports/field-map.d.ts
|
|
115
|
+
/** Extract headers from a field map. */
|
|
116
|
+
declare function getHeaders<TRow>(fieldMap: ExportFieldMap<TRow>): string[];
|
|
117
|
+
/** Apply a field map to a single row, producing an array of cell strings. */
|
|
118
|
+
declare function extractRow<TRow>(fieldMap: ExportFieldMap<TRow>, row: TRow): string[];
|
|
119
|
+
/** Apply a field map to an array of rows, producing a 2D string array. */
|
|
120
|
+
declare function extractAllRows<TRow>(fieldMap: ExportFieldMap<TRow>, rows: readonly TRow[]): string[][];
|
|
121
|
+
/** One-shot: map + serialize to CSV string. */
|
|
122
|
+
declare function exportToCsv<TRow>(fieldMap: ExportFieldMap<TRow>, rows: readonly TRow[], options?: CsvOptions): string;
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/exports/flatten-journal.d.ts
|
|
125
|
+
/** Flatten a single journal entry into one FlatJournalRow per journal item. */
|
|
126
|
+
declare function flattenJournalEntry(entry: PopulatedJournalEntry): FlatJournalRow[];
|
|
127
|
+
/** Flatten multiple journal entries into a single flat row array. */
|
|
128
|
+
declare function flattenJournalEntries(entries: readonly PopulatedJournalEntry[]): FlatJournalRow[];
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/exports/field-maps/quickbooks.d.ts
|
|
131
|
+
declare const quickbooksFieldMap: ExportFieldMap<FlatJournalRow>;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/exports/field-maps/universal.d.ts
|
|
134
|
+
declare const universalFieldMap: ExportFieldMap<FlatJournalRow>;
|
|
135
|
+
//#endregion
|
|
136
|
+
export { PopulatedJournalEntry as _, exportToCsv as a, getHeaders as c, serializeCsv as d, CsvOptions as f, PopulatedAccount as g, FlatJournalRow as h, flattenJournalEntry as i, buildCsv as l, ExportFieldMap as m, quickbooksFieldMap as n, extractAllRows as o, ExportField as p, flattenJournalEntries as r, extractRow as s, universalFieldMap as t, escapeCell as u, PopulatedJournalItem as v };
|
|
137
|
+
//# sourceMappingURL=universal-x33ZJODp.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"universal-x33ZJODp.d.mts","names":[],"sources":["../src/exports/types.ts","../src/exports/csv-serializer.ts","../src/exports/field-map.ts","../src/exports/flatten-journal.ts","../src/exports/field-maps/quickbooks.ts","../src/exports/field-maps/universal.ts"],"mappings":";;AAeA;;;;;;;;;;;UAAiB,gBAAA;EACf,GAAA;EACA,eAAA;EACA,IAAA;EACA,MAAA;EACA,aAAA;AAAA;;;;UAMe,oBAAA;EACf,OAAA,EAAS,gBAAA;EACT,KAAA;EACA,IAAA,GAAO,IAAA;EACP,KAAA;EACA,MAAA;EACA,UAAA,GAAa,KAAA;IAAQ,OAAA;IAAkB,OAAA;EAAA;EAE3B;EAAA,CAAX,GAAA;AAAA;;;;;UAOc,qBAAA;EACf,GAAA;EACA,WAAA;EACA,eAAA;EACA,KAAA;EACA,IAAA,EAAM,IAAA;EACN,YAAA,EAAc,oBAAA;EACd,UAAA;EACA,WAAA;EACA,KAAA;EACA,QAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;EAAA,CACX,GAAA;AAAA;;;;;UASc,cAAA;EACf,OAAA;EACA,WAAA;EACA,eAAA;EACA,UAAA;EACA,SAAA,EAAW,IAAA;EACX,KAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EAEA,SAAA;EACA,WAAA;EACA,eAAA;EACA,SAAA;EACA,QAAA,EAAU,IAAA;EACV,KAAA;EACA,MAAA;EACA,OAAA;EACA,OAAA;EAEA,SAAA;EACA,SAAA;EARA;EAAA,CAWC,GAAA;AAAA;;;;;UASc,WAAA,QAAmB,cAAA;EAAA,SACzB,MAAA;EAAA,SACA,OAAA,GAAU,GAAA,EAAK,IAAA;AAAA;;AAF1B;;UAQiB,cAAA,QAAsB,cAAA;EAAA,SAC5B,IAAA;EAAA,SACA,MAAA;EAAA,SACA,MAAA,WAAiB,WAAA,CAAY,IAAA;AAAA;AAAA,UAKvB,UAAA;EACf,SAAA;EACA,cAAA;EACA,cAAA;AAAA;;;;iBCvGc,UAAA,CAAW,KAAA;;iBAQX,YAAA,CACd,IAAA,kCACA,OAAA,GAAS,UAAA;ADGX;AAAA,iBCOgB,QAAA,CACd,OAAA,qBACA,QAAA,kCACA,OAAA,GAAS,UAAA;;;;iBC1BK,UAAA,MAAA,CAAiB,QAAA,EAAU,cAAA,CAAe,IAAA;;iBAK1C,UAAA,MAAA,CAAiB,QAAA,EAAU,cAAA,CAAe,IAAA,GAAO,GAAA,EAAK,IAAA;;iBAKtD,cAAA,MAAA,CACd,QAAA,EAAU,cAAA,CAAe,IAAA,GACzB,IAAA,WAAe,IAAA;AFIjB;AAAA,iBEEgB,WAAA,MAAA,CACd,QAAA,EAAU,cAAA,CAAe,IAAA,GACzB,IAAA,WAAe,IAAA,IACf,OAAA,GAAU,UAAA;;;;iBCGI,mBAAA,CAAoB,KAAA,EAAO,qBAAA,GAAwB,cAAA;;iBAiDnD,qBAAA,CACd,OAAA,WAAkB,qBAAA,KACjB,cAAA;;;cC5DU,kBAAA,EAAoB,cAAA,CAAe,cAAA;;;cCJnC,iBAAA,EAAmB,cAAA,CAAe,cAAA"}
|