@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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/account.repository-1C2sZvB2.d.mts +29 -0
  4. package/dist/account.repository-1C2sZvB2.d.mts.map +1 -0
  5. package/dist/account.repository-Crf5DGO4.mjs +393 -0
  6. package/dist/account.repository-Crf5DGO4.mjs.map +1 -0
  7. package/dist/categories-BNJBd4ze.mjs +70 -0
  8. package/dist/categories-BNJBd4ze.mjs.map +1 -0
  9. package/dist/constants/index.d.mts +2 -0
  10. package/dist/constants/index.mjs +5 -0
  11. package/dist/core-Cx0baosR.d.mts +104 -0
  12. package/dist/core-Cx0baosR.d.mts.map +1 -0
  13. package/dist/country/index.d.mts +105 -0
  14. package/dist/country/index.d.mts.map +1 -0
  15. package/dist/country/index.mjs +27 -0
  16. package/dist/country/index.mjs.map +1 -0
  17. package/dist/currencies-BBk3NwXn.mjs +82 -0
  18. package/dist/currencies-BBk3NwXn.mjs.map +1 -0
  19. package/dist/currencies-Bkn3FNkC.d.mts +38 -0
  20. package/dist/currencies-Bkn3FNkC.d.mts.map +1 -0
  21. package/dist/engine-Cd73EOT6.d.mts +72 -0
  22. package/dist/engine-Cd73EOT6.d.mts.map +1 -0
  23. package/dist/errors-CeqRahE-.mjs +28 -0
  24. package/dist/errors-CeqRahE-.mjs.map +1 -0
  25. package/dist/exports/index.d.mts +2 -0
  26. package/dist/exports/index.mjs +3 -0
  27. package/dist/fiscal-close-CNOwv_ud.mjs +934 -0
  28. package/dist/fiscal-close-CNOwv_ud.mjs.map +1 -0
  29. package/dist/fiscal-close-CzUzpnMg.d.mts +270 -0
  30. package/dist/fiscal-close-CzUzpnMg.d.mts.map +1 -0
  31. package/dist/fiscal-period.schema-CbALaaKl.mjs +477 -0
  32. package/dist/fiscal-period.schema-CbALaaKl.mjs.map +1 -0
  33. package/dist/fiscal-period.schema-DI2scngu.d.mts +38 -0
  34. package/dist/fiscal-period.schema-DI2scngu.d.mts.map +1 -0
  35. package/dist/idempotency.plugin-BESs9YPD.d.mts +58 -0
  36. package/dist/idempotency.plugin-BESs9YPD.d.mts.map +1 -0
  37. package/dist/idempotency.plugin-C6r8RI8d.mjs +165 -0
  38. package/dist/idempotency.plugin-C6r8RI8d.mjs.map +1 -0
  39. package/dist/index.d.mts +308 -0
  40. package/dist/index.d.mts.map +1 -0
  41. package/dist/index.mjs +171 -0
  42. package/dist/index.mjs.map +1 -0
  43. package/dist/journals-CI3Wb4EF.mjs +92 -0
  44. package/dist/journals-CI3Wb4EF.mjs.map +1 -0
  45. package/dist/logger-Cv6VVc4r.d.mts +15 -0
  46. package/dist/logger-Cv6VVc4r.d.mts.map +1 -0
  47. package/dist/money.d.mts +129 -0
  48. package/dist/money.d.mts.map +1 -0
  49. package/dist/money.mjs +197 -0
  50. package/dist/money.mjs.map +1 -0
  51. package/dist/plugins/index.d.mts +2 -0
  52. package/dist/plugins/index.mjs +3 -0
  53. package/dist/reports/index.d.mts +2 -0
  54. package/dist/reports/index.mjs +3 -0
  55. package/dist/repositories/index.d.mts +2 -0
  56. package/dist/repositories/index.mjs +3 -0
  57. package/dist/schemas/index.d.mts +2 -0
  58. package/dist/schemas/index.mjs +3 -0
  59. package/dist/session-Dh0s6zG4.mjs +87 -0
  60. package/dist/session-Dh0s6zG4.mjs.map +1 -0
  61. package/dist/universal-CMfrZ2hG.mjs +257 -0
  62. package/dist/universal-CMfrZ2hG.mjs.map +1 -0
  63. package/dist/universal-x33ZJODp.d.mts +137 -0
  64. package/dist/universal-x33ZJODp.d.mts.map +1 -0
  65. package/docs/country-packs.md +117 -0
  66. package/docs/engine.md +147 -0
  67. package/docs/exports.md +81 -0
  68. package/docs/money.md +81 -0
  69. package/docs/plugins.md +136 -0
  70. package/docs/reports.md +154 -0
  71. package/docs/repositories.md +239 -0
  72. package/docs/schemas.md +146 -0
  73. package/docs/subledger-integration.md +287 -0
  74. package/package.json +116 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,171 @@
1
+ import { n as createJournalEntrySchema, r as createAccountSchema, t as createFiscalPeriodSchema } from "./fiscal-period.schema-CbALaaKl.mjs";
2
+ import { a as isValidJournalType, i as getJournalTypeCodes, n as JOURNAL_TYPES, t as JOURNAL_CODES } from "./journals-CI3Wb4EF.mjs";
3
+ import { a as generateIncomeStatement, c as calculateTotal, d as generateTrialBalance, f as buildItemFilters, i as generateGeneralLedger, l as computeEndingBalance, m as getFiscalYearStart, n as reopenFiscalPeriod, o as generateBalanceSheet, p as getDateRange, r as generateCashFlow, s as buildAccountTypeMap, t as closeFiscalPeriod, u as isVirtualTaxAccount } from "./fiscal-close-CNOwv_ud.mjs";
4
+ import { n as Errors, t as AccountingError } from "./errors-CeqRahE-.mjs";
5
+ import { n as finalizeSession, r as defaultLogger, t as acquireSession } from "./session-Dh0s6zG4.mjs";
6
+ import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-BNJBd4ze.mjs";
7
+ import { Money, add, allocate, format, formatPlain, fromDecimal, multiply, parseCents, percentage, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal } from "./money.mjs";
8
+ import { n as wireJournalEntryMethods, t as wireAccountMethods } from "./account.repository-Crf5DGO4.mjs";
9
+ import { n as fiscalLockPlugin, r as doubleEntryPlugin, t as idempotencyPlugin } from "./idempotency.plugin-C6r8RI8d.mjs";
10
+ import { i as isValidCurrency, n as getCurrency, r as getMinorUnit, t as CURRENCIES } from "./currencies-BBk3NwXn.mjs";
11
+ import { defineCountryPack } from "./country/index.mjs";
12
+ import { a as exportToCsv, n as quickbooksFieldMap, r as flattenJournalEntries, t as universalFieldMap } from "./universal-CMfrZ2hG.mjs";
13
+
14
+ //#region src/engine.ts
15
+ var AccountingEngine = class {
16
+ config;
17
+ country;
18
+ currency;
19
+ money = Money;
20
+ constructor(config) {
21
+ this.config = config;
22
+ this.country = config.country;
23
+ this.currency = config.currency;
24
+ }
25
+ createAccountSchema(options) {
26
+ return createAccountSchema(this.config, options);
27
+ }
28
+ createJournalEntrySchema(accountModelName, options) {
29
+ return createJournalEntrySchema(this.config, accountModelName, options);
30
+ }
31
+ createFiscalPeriodSchema(options) {
32
+ return createFiscalPeriodSchema(this.config, options);
33
+ }
34
+ createReports(models) {
35
+ const { Account: AccountModel, JournalEntry: JournalEntryModel } = models;
36
+ const { country, config } = this;
37
+ const orgField = config.multiTenant?.orgField;
38
+ const fiscalYearStartMonth = config.fiscalYearStartMonth ?? 1;
39
+ const retainedEarningsCode = config.retainedEarningsCode;
40
+ const currentYearEarningsCode = config.currentYearEarningsCode;
41
+ return {
42
+ trialBalance: (params) => generateTrialBalance({
43
+ AccountModel,
44
+ JournalEntryModel,
45
+ country,
46
+ orgField,
47
+ fiscalYearStartMonth
48
+ }, params),
49
+ balanceSheet: (params) => generateBalanceSheet({
50
+ AccountModel,
51
+ JournalEntryModel,
52
+ country,
53
+ orgField,
54
+ fiscalYearStartMonth,
55
+ retainedEarningsCode,
56
+ currentYearEarningsCode
57
+ }, params),
58
+ incomeStatement: (params) => generateIncomeStatement({
59
+ AccountModel,
60
+ JournalEntryModel,
61
+ country,
62
+ orgField
63
+ }, params),
64
+ generalLedger: (params) => generateGeneralLedger({
65
+ AccountModel,
66
+ JournalEntryModel,
67
+ country,
68
+ orgField,
69
+ fiscalYearStartMonth
70
+ }, params),
71
+ cashFlow: (params) => generateCashFlow({
72
+ AccountModel,
73
+ JournalEntryModel,
74
+ country,
75
+ orgField
76
+ }, params)
77
+ };
78
+ }
79
+ /** Get all posting account types (accounts you can post transactions to) */
80
+ getPostingAccountTypes() {
81
+ return this.country.getPostingAccountTypes();
82
+ }
83
+ /** Validate an account type code */
84
+ isValidAccountType(code) {
85
+ return this.country.isValidAccountType(code);
86
+ }
87
+ /** Get account type definition by code */
88
+ getAccountType(code) {
89
+ return this.country.getAccountType(code);
90
+ }
91
+ /** Get tax codes for a region */
92
+ getTaxCodesForRegion(region) {
93
+ return this.country.getTaxCodesForRegion(region);
94
+ }
95
+ /**
96
+ * Create a fully-configured journal entry repository with secure plugin wiring.
97
+ * This is the **recommended** way to set up journal entry repositories.
98
+ *
99
+ * Includes:
100
+ * - Double-entry plugin with account existence + tenant integrity validation
101
+ * - Fiscal lock plugin (when FiscalPeriodModel is provided)
102
+ * - post(), unpost(), reverse(), and duplicate() domain methods
103
+ *
104
+ * @param createRepository - The `createRepository` function from @classytic/mongokit
105
+ * @param models.JournalEntryModel - Mongoose model for journal entries
106
+ * @param models.AccountModel - Mongoose model for accounts (required for secure posted-create validation)
107
+ * @param models.FiscalPeriodModel - Mongoose model for fiscal periods (optional, enables fiscal lock)
108
+ * @param additionalPlugins - Extra plugins to include (e.g. timestampPlugin)
109
+ * @returns A wired repository with post(), unpost(), reverse(), duplicate(), and all plugins configured
110
+ */
111
+ createJournalEntryRepository(createRepository, models, additionalPlugins = []) {
112
+ const orgField = this.config.multiTenant?.orgField;
113
+ const { JournalEntryModel, AccountModel, FiscalPeriodModel } = models;
114
+ const plugins = [...additionalPlugins, doubleEntryPlugin({
115
+ JournalEntryModel,
116
+ AccountModel,
117
+ orgField
118
+ })];
119
+ if (FiscalPeriodModel) plugins.push(fiscalLockPlugin({
120
+ FiscalPeriodModel,
121
+ JournalEntryModel,
122
+ orgField
123
+ }));
124
+ if (this.config.idempotency) plugins.push(idempotencyPlugin({
125
+ JournalEntryModel,
126
+ orgField
127
+ }));
128
+ const repository = createRepository(JournalEntryModel, plugins);
129
+ wireJournalEntryMethods(repository, JournalEntryModel, orgField, this.config.strictness);
130
+ return repository;
131
+ }
132
+ /**
133
+ * Wire post/reverse domain methods onto a mongokit Repository
134
+ * for journal entries. The repository must already be created via
135
+ * `createRepository(Model, plugins)` from @classytic/mongokit.
136
+ *
137
+ * **Note:** Prefer `createJournalEntryRepository()` which guarantees
138
+ * secure plugin wiring. This method only adds domain methods and does
139
+ * not validate plugin configuration.
140
+ *
141
+ * @param repository - An existing mongokit Repository instance
142
+ * @param JournalEntryModel - The Mongoose model for journal entries
143
+ * @returns The same repository, now with `.post()` and `.reverse()`
144
+ */
145
+ wireJournalEntryRepository(repository, JournalEntryModel) {
146
+ const orgField = this.config.multiTenant?.orgField;
147
+ wireJournalEntryMethods(repository, JournalEntryModel, orgField, this.config.strictness);
148
+ return repository;
149
+ }
150
+ /**
151
+ * Wire seedAccounts/bulkCreate and posting-account validation onto a
152
+ * mongokit Repository for accounts. The repository must already be
153
+ * created via `createRepository(Model, plugins)` from @classytic/mongokit.
154
+ *
155
+ * @param repository - An existing mongokit Repository instance
156
+ * @param AccountModel - The Mongoose model for accounts
157
+ * @returns The same repository, now with `.seedAccounts()` and `.bulkCreate()`
158
+ */
159
+ wireAccountRepository(repository, AccountModel) {
160
+ const orgField = this.config.multiTenant?.orgField;
161
+ wireAccountMethods(repository, AccountModel, this.country, orgField);
162
+ return repository;
163
+ }
164
+ };
165
+ function createAccountingEngine(config) {
166
+ return new AccountingEngine(config);
167
+ }
168
+
169
+ //#endregion
170
+ export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, Errors, JOURNAL_CODES, JOURNAL_TYPES, Money, acquireSession, add, allocate, buildAccountTypeMap, buildItemFilters, calculateTotal, closeFiscalPeriod, computeEndingBalance, createAccountSchema, createAccountingEngine, createFiscalPeriodSchema, createJournalEntrySchema, defaultLogger, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, generateBalanceSheet, generateCashFlow, generateGeneralLedger, generateIncomeStatement, generateTrialBalance, getCurrency, getDateRange, getFiscalYearStart, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, quickbooksFieldMap, reopenFiscalPeriod, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal, universalFieldMap, wireAccountMethods, wireJournalEntryMethods };
171
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/engine.ts"],"sourcesContent":["/**\n * AccountingEngine — The main entry point for @classytic/ledger.\n *\n * Usage:\n * const accounting = createAccountingEngine({\n * country: canadaPack,\n * currency: 'CAD',\n * multiTenant: { orgField: 'business', orgRef: 'Business' },\n * });\n *\n * const AccountSchema = accounting.createAccountSchema();\n * const JournalEntrySchema = accounting.createJournalEntrySchema('Account');\n * const FiscalPeriodSchema = accounting.createFiscalPeriodSchema();\n *\n * // Register models\n * const Account = mongoose.model('Account', AccountSchema);\n * const JournalEntry = mongoose.model('JournalEntry', JournalEntrySchema);\n * const FiscalPeriod = mongoose.model('FiscalPeriod', FiscalPeriodSchema);\n *\n * // Reports\n * const reports = accounting.createReports({ Account, JournalEntry });\n * const bs = await reports.balanceSheet({ dateOption: 'year', dateValue: 2025, organizationId: '...' });\n */\n\nimport type { Model } from 'mongoose';\nimport type { AccountingEngineConfig, SchemaOptions, JournalSchemaOptions } from './types/engine.js';\nimport type { CountryPack } from './country/index.js';\nimport { createAccountSchema } from './schemas/account.schema.js';\nimport { createJournalEntrySchema } from './schemas/journal-entry.schema.js';\nimport { createFiscalPeriodSchema } from './schemas/fiscal-period.schema.js';\nimport { generateTrialBalance } from './reports/trial-balance.js';\nimport { generateBalanceSheet } from './reports/balance-sheet.js';\nimport { generateIncomeStatement } from './reports/income-statement.js';\nimport { generateGeneralLedger } from './reports/general-ledger.js';\nimport { generateCashFlow } from './reports/cash-flow.js';\nimport { Money } from './money.js';\nimport { wireJournalEntryMethods } from './repositories/journal-entry.repository.js';\nimport { wireAccountMethods } from './repositories/account.repository.js';\nimport { doubleEntryPlugin } from './plugins/double-entry.plugin.js';\nimport { fiscalLockPlugin } from './plugins/fiscal-lock.plugin.js';\nimport { idempotencyPlugin } from './plugins/idempotency.plugin.js';\n\nexport class AccountingEngine {\n readonly config: AccountingEngineConfig;\n readonly country: CountryPack;\n readonly currency: string;\n readonly money = Money;\n\n constructor(config: AccountingEngineConfig) {\n this.config = config;\n this.country = config.country;\n this.currency = config.currency;\n }\n\n // ── Schema Factories ───────────────────────────────────────────────────────\n\n createAccountSchema(options?: SchemaOptions) {\n return createAccountSchema(this.config, options);\n }\n\n createJournalEntrySchema(accountModelName: string, options?: JournalSchemaOptions) {\n return createJournalEntrySchema(this.config, accountModelName, options);\n }\n\n createFiscalPeriodSchema(options?: SchemaOptions) {\n return createFiscalPeriodSchema(this.config, options);\n }\n\n // ── Report Engine ──────────────────────────────────────────────────────────\n\n createReports(models: {\n Account: Model<unknown>;\n JournalEntry: Model<unknown>;\n }) {\n const { Account: AccountModel, JournalEntry: JournalEntryModel } = models;\n const { country, config } = this;\n const orgField = config.multiTenant?.orgField;\n const fiscalYearStartMonth = config.fiscalYearStartMonth ?? 1;\n const retainedEarningsCode = config.retainedEarningsCode;\n const currentYearEarningsCode = config.currentYearEarningsCode;\n\n return {\n trialBalance: (params: {\n organizationId?: unknown;\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\n dateValue: unknown;\n accountId?: string;\n filters?: Record<string, unknown>;\n }) =>\n generateTrialBalance(\n { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth },\n params,\n ),\n\n balanceSheet: (params: {\n organizationId?: unknown;\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\n dateValue: unknown;\n businessName?: string;\n filters?: Record<string, unknown>;\n }) =>\n generateBalanceSheet(\n { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth, retainedEarningsCode, currentYearEarningsCode },\n params,\n ),\n\n incomeStatement: (params: {\n organizationId?: unknown;\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\n dateValue: unknown;\n businessName?: string;\n filters?: Record<string, unknown>;\n }) =>\n generateIncomeStatement(\n { AccountModel, JournalEntryModel, country, orgField },\n params,\n ),\n\n generalLedger: (params: {\n organizationId?: unknown;\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\n dateValue: unknown;\n accountId?: string;\n filters?: Record<string, unknown>;\n }) =>\n generateGeneralLedger(\n { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth },\n params,\n ),\n\n cashFlow: (params: {\n organizationId?: unknown;\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\n dateValue: unknown;\n businessName?: string;\n filters?: Record<string, unknown>;\n }) =>\n generateCashFlow(\n { AccountModel, JournalEntryModel, country, orgField },\n params,\n ),\n };\n }\n\n // ── Account Type Helpers ───────────────────────────────────────────────────\n\n /** Get all posting account types (accounts you can post transactions to) */\n getPostingAccountTypes() {\n return this.country.getPostingAccountTypes();\n }\n\n /** Validate an account type code */\n isValidAccountType(code: string) {\n return this.country.isValidAccountType(code);\n }\n\n /** Get account type definition by code */\n getAccountType(code: string) {\n return this.country.getAccountType(code);\n }\n\n /** Get tax codes for a region */\n getTaxCodesForRegion(region: string) {\n return this.country.getTaxCodesForRegion(region);\n }\n\n // ── Repository Factories ─────────────────────────────────────────────────\n\n /**\n * Create a fully-configured journal entry repository with secure plugin wiring.\n * This is the **recommended** way to set up journal entry repositories.\n *\n * Includes:\n * - Double-entry plugin with account existence + tenant integrity validation\n * - Fiscal lock plugin (when FiscalPeriodModel is provided)\n * - post(), unpost(), reverse(), and duplicate() domain methods\n *\n * @param createRepository - The `createRepository` function from @classytic/mongokit\n * @param models.JournalEntryModel - Mongoose model for journal entries\n * @param models.AccountModel - Mongoose model for accounts (required for secure posted-create validation)\n * @param models.FiscalPeriodModel - Mongoose model for fiscal periods (optional, enables fiscal lock)\n * @param additionalPlugins - Extra plugins to include (e.g. timestampPlugin)\n * @returns A wired repository with post(), unpost(), reverse(), duplicate(), and all plugins configured\n */\n createJournalEntryRepository(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n createRepository: (model: Model<unknown>, plugins: any[]) => any,\n models: {\n JournalEntryModel: Model<unknown>;\n AccountModel: Model<unknown>;\n FiscalPeriodModel?: Model<unknown>;\n },\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n additionalPlugins: any[] = [],\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ): any {\n const orgField = this.config.multiTenant?.orgField;\n const { JournalEntryModel, AccountModel, FiscalPeriodModel } = models;\n\n const plugins = [\n ...additionalPlugins,\n doubleEntryPlugin({\n JournalEntryModel,\n AccountModel,\n orgField,\n }),\n ];\n\n if (FiscalPeriodModel) {\n plugins.push(\n fiscalLockPlugin({\n FiscalPeriodModel,\n JournalEntryModel,\n orgField,\n }),\n );\n }\n\n if (this.config.idempotency) {\n plugins.push(\n idempotencyPlugin({\n JournalEntryModel,\n orgField,\n }),\n );\n }\n\n const repository = createRepository(JournalEntryModel, plugins);\n wireJournalEntryMethods(repository, JournalEntryModel, orgField, this.config.strictness);\n return repository;\n }\n\n /**\n * Wire post/reverse domain methods onto a mongokit Repository\n * for journal entries. The repository must already be created via\n * `createRepository(Model, plugins)` from @classytic/mongokit.\n *\n * **Note:** Prefer `createJournalEntryRepository()` which guarantees\n * secure plugin wiring. This method only adds domain methods and does\n * not validate plugin configuration.\n *\n * @param repository - An existing mongokit Repository instance\n * @param JournalEntryModel - The Mongoose model for journal entries\n * @returns The same repository, now with `.post()` and `.reverse()`\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n wireJournalEntryRepository(repository: any, JournalEntryModel: Model<unknown>): any {\n const orgField = this.config.multiTenant?.orgField;\n wireJournalEntryMethods(repository, JournalEntryModel, orgField, this.config.strictness);\n return repository;\n }\n\n /**\n * Wire seedAccounts/bulkCreate and posting-account validation onto a\n * mongokit Repository for accounts. The repository must already be\n * created via `createRepository(Model, plugins)` from @classytic/mongokit.\n *\n * @param repository - An existing mongokit Repository instance\n * @param AccountModel - The Mongoose model for accounts\n * @returns The same repository, now with `.seedAccounts()` and `.bulkCreate()`\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n wireAccountRepository(repository: any, AccountModel: Model<unknown>): any {\n const orgField = this.config.multiTenant?.orgField;\n wireAccountMethods(repository, AccountModel, this.country, orgField);\n return repository;\n }\n}\n\n// ── Factory ────────────────────────────────────────────────────────────────\n\nexport function createAccountingEngine(config: AccountingEngineConfig): AccountingEngine {\n return new AccountingEngine(config);\n}\n"],"mappings":";;;;;;;;;;;;;;AA0CA,IAAa,mBAAb,MAA8B;CAC5B,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS,QAAQ;CAEjB,YAAY,QAAgC;AAC1C,OAAK,SAAS;AACd,OAAK,UAAU,OAAO;AACtB,OAAK,WAAW,OAAO;;CAKzB,oBAAoB,SAAyB;AAC3C,SAAO,oBAAoB,KAAK,QAAQ,QAAQ;;CAGlD,yBAAyB,kBAA0B,SAAgC;AACjF,SAAO,yBAAyB,KAAK,QAAQ,kBAAkB,QAAQ;;CAGzE,yBAAyB,SAAyB;AAChD,SAAO,yBAAyB,KAAK,QAAQ,QAAQ;;CAKvD,cAAc,QAGX;EACD,MAAM,EAAE,SAAS,cAAc,cAAc,sBAAsB;EACnE,MAAM,EAAE,SAAS,WAAW;EAC5B,MAAM,WAAW,OAAO,aAAa;EACrC,MAAM,uBAAuB,OAAO,wBAAwB;EAC5D,MAAM,uBAAuB,OAAO;EACpC,MAAM,0BAA0B,OAAO;AAEvC,SAAO;GACL,eAAe,WAOb,qBACE;IAAE;IAAc;IAAmB;IAAS;IAAU;IAAsB,EAC5E,OACD;GAEH,eAAe,WAOb,qBACE;IAAE;IAAc;IAAmB;IAAS;IAAU;IAAsB;IAAsB;IAAyB,EAC3H,OACD;GAEH,kBAAkB,WAOhB,wBACE;IAAE;IAAc;IAAmB;IAAS;IAAU,EACtD,OACD;GAEH,gBAAgB,WAOd,sBACE;IAAE;IAAc;IAAmB;IAAS;IAAU;IAAsB,EAC5E,OACD;GAEH,WAAW,WAOT,iBACE;IAAE;IAAc;IAAmB;IAAS;IAAU,EACtD,OACD;GACJ;;;CAMH,yBAAyB;AACvB,SAAO,KAAK,QAAQ,wBAAwB;;;CAI9C,mBAAmB,MAAc;AAC/B,SAAO,KAAK,QAAQ,mBAAmB,KAAK;;;CAI9C,eAAe,MAAc;AAC3B,SAAO,KAAK,QAAQ,eAAe,KAAK;;;CAI1C,qBAAqB,QAAgB;AACnC,SAAO,KAAK,QAAQ,qBAAqB,OAAO;;;;;;;;;;;;;;;;;;CAqBlD,6BAEE,kBACA,QAMA,oBAA2B,EAAE,EAExB;EACL,MAAM,WAAW,KAAK,OAAO,aAAa;EAC1C,MAAM,EAAE,mBAAmB,cAAc,sBAAsB;EAE/D,MAAM,UAAU,CACd,GAAG,mBACH,kBAAkB;GAChB;GACA;GACA;GACD,CAAC,CACH;AAED,MAAI,kBACF,SAAQ,KACN,iBAAiB;GACf;GACA;GACA;GACD,CAAC,CACH;AAGH,MAAI,KAAK,OAAO,YACd,SAAQ,KACN,kBAAkB;GAChB;GACA;GACD,CAAC,CACH;EAGH,MAAM,aAAa,iBAAiB,mBAAmB,QAAQ;AAC/D,0BAAwB,YAAY,mBAAmB,UAAU,KAAK,OAAO,WAAW;AACxF,SAAO;;;;;;;;;;;;;;;CAiBT,2BAA2B,YAAiB,mBAAwC;EAClF,MAAM,WAAW,KAAK,OAAO,aAAa;AAC1C,0BAAwB,YAAY,mBAAmB,UAAU,KAAK,OAAO,WAAW;AACxF,SAAO;;;;;;;;;;;CAaT,sBAAsB,YAAiB,cAAmC;EACxE,MAAM,WAAW,KAAK,OAAO,aAAa;AAC1C,qBAAmB,YAAY,cAAc,KAAK,SAAS,SAAS;AACpE,SAAO;;;AAMX,SAAgB,uBAAuB,QAAkD;AACvF,QAAO,IAAI,iBAAiB,OAAO"}
@@ -0,0 +1,92 @@
1
+ //#region src/constants/journals.ts
2
+ const JOURNAL_TYPES = Object.freeze({
3
+ SALES: {
4
+ code: "SALES",
5
+ name: "Sales Journal",
6
+ description: "Sales transactions and revenue"
7
+ },
8
+ PURCHASES: {
9
+ code: "PURCHASES",
10
+ name: "Purchases Journal",
11
+ description: "Purchase transactions and expenses"
12
+ },
13
+ CASH_RECEIPTS: {
14
+ code: "CASH_RECEIPTS",
15
+ name: "Cash Receipts Journal",
16
+ description: "Cash and bank deposits received"
17
+ },
18
+ CASH_PAYMENTS: {
19
+ code: "CASH_PAYMENTS",
20
+ name: "Cash Payments Journal",
21
+ description: "Cash and bank payments made"
22
+ },
23
+ PAYROLL: {
24
+ code: "PAYROLL",
25
+ name: "Payroll Journal",
26
+ description: "Employee wages, salaries, and related expenses"
27
+ },
28
+ GENERAL: {
29
+ code: "GENERAL",
30
+ name: "General Journal",
31
+ description: "Adjusting entries, corrections, and misc transactions"
32
+ },
33
+ INVENTORY: {
34
+ code: "INVENTORY",
35
+ name: "Inventory Journal",
36
+ description: "Inventory adjustments and movements"
37
+ },
38
+ FIXED_ASSETS: {
39
+ code: "FIXED_ASSETS",
40
+ name: "Fixed Assets Journal",
41
+ description: "Asset purchases, disposals, and depreciation"
42
+ },
43
+ BANK_RECONCILIATION: {
44
+ code: "BANK_RECONCILIATION",
45
+ name: "Bank Reconciliation",
46
+ description: "Bank reconciliation adjustments"
47
+ },
48
+ DEPRECIATION: {
49
+ code: "DEPRECIATION",
50
+ name: "Depreciation Journal",
51
+ description: "Periodic depreciation expenses"
52
+ },
53
+ YEAR_END: {
54
+ code: "YEAR_END",
55
+ name: "Year-End Adjustments",
56
+ description: "Year-end closing and adjustment entries"
57
+ },
58
+ ACCOUNTS_RECEIVABLE: {
59
+ code: "ACCOUNTS_RECEIVABLE",
60
+ name: "Accounts Receivable",
61
+ description: "Customer invoices and receivable transactions"
62
+ },
63
+ ACCOUNTS_PAYABLE: {
64
+ code: "ACCOUNTS_PAYABLE",
65
+ name: "Accounts Payable",
66
+ description: "Vendor bills and payable transactions"
67
+ },
68
+ TAX: {
69
+ code: "TAX",
70
+ name: "Tax Journal",
71
+ description: "GST/HST/PST and other tax-related entries"
72
+ },
73
+ MISC: {
74
+ code: "MISC",
75
+ name: "Miscellaneous",
76
+ description: "Transactions that don't fit other categories"
77
+ }
78
+ });
79
+ const JOURNAL_CODES = Object.freeze(Object.fromEntries(Object.keys(JOURNAL_TYPES).map((k) => [k, k])));
80
+ function getJournalTypeCodes() {
81
+ return Object.keys(JOURNAL_TYPES);
82
+ }
83
+ function isValidJournalType(code) {
84
+ return code in JOURNAL_TYPES;
85
+ }
86
+ function getJournalType(code) {
87
+ return JOURNAL_TYPES[code] ?? null;
88
+ }
89
+
90
+ //#endregion
91
+ export { isValidJournalType as a, getJournalTypeCodes as i, JOURNAL_TYPES as n, getJournalType as r, JOURNAL_CODES as t };
92
+ //# sourceMappingURL=journals-CI3Wb4EF.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journals-CI3Wb4EF.mjs","names":[],"sources":["../src/constants/journals.ts"],"sourcesContent":["/**\r\n * Journal Types — Standard journal classifications.\r\n * Extensible: country packs can add custom journal types.\r\n */\r\n\r\nimport type { JournalType } from '../types/core.js';\r\n\r\nexport const JOURNAL_TYPES: Readonly<Record<string, JournalType>> = Object.freeze({\r\n SALES: { code: 'SALES', name: 'Sales Journal', description: 'Sales transactions and revenue' },\r\n PURCHASES: { code: 'PURCHASES', name: 'Purchases Journal', description: 'Purchase transactions and expenses' },\r\n CASH_RECEIPTS: { code: 'CASH_RECEIPTS', name: 'Cash Receipts Journal', description: 'Cash and bank deposits received' },\r\n CASH_PAYMENTS: { code: 'CASH_PAYMENTS', name: 'Cash Payments Journal', description: 'Cash and bank payments made' },\r\n PAYROLL: { code: 'PAYROLL', name: 'Payroll Journal', description: 'Employee wages, salaries, and related expenses' },\r\n GENERAL: { code: 'GENERAL', name: 'General Journal', description: 'Adjusting entries, corrections, and misc transactions' },\r\n INVENTORY: { code: 'INVENTORY', name: 'Inventory Journal', description: 'Inventory adjustments and movements' },\r\n FIXED_ASSETS: { code: 'FIXED_ASSETS', name: 'Fixed Assets Journal', description: 'Asset purchases, disposals, and depreciation' },\r\n BANK_RECONCILIATION:{ code: 'BANK_RECONCILIATION', name: 'Bank Reconciliation', description: 'Bank reconciliation adjustments' },\r\n DEPRECIATION: { code: 'DEPRECIATION', name: 'Depreciation Journal', description: 'Periodic depreciation expenses' },\r\n YEAR_END: { code: 'YEAR_END', name: 'Year-End Adjustments', description: 'Year-end closing and adjustment entries' },\r\n ACCOUNTS_RECEIVABLE:{ code: 'ACCOUNTS_RECEIVABLE', name: 'Accounts Receivable', description: 'Customer invoices and receivable transactions' },\r\n ACCOUNTS_PAYABLE: { code: 'ACCOUNTS_PAYABLE', name: 'Accounts Payable', description: 'Vendor bills and payable transactions' },\r\n TAX: { code: 'TAX', name: 'Tax Journal', description: 'GST/HST/PST and other tax-related entries' },\r\n MISC: { code: 'MISC', name: 'Miscellaneous', description: 'Transactions that don\\'t fit other categories' },\r\n});\r\n\r\nexport const JOURNAL_CODES = Object.freeze(\r\n Object.fromEntries(Object.keys(JOURNAL_TYPES).map(k => [k, k])) as Record<string, string>,\r\n);\r\n\r\nexport function getJournalTypeCodes(): string[] {\r\n return Object.keys(JOURNAL_TYPES);\r\n}\r\n\r\nexport function isValidJournalType(code: string): boolean {\r\n return code in JOURNAL_TYPES;\r\n}\r\n\r\nexport function getJournalType(code: string): JournalType | null {\r\n return JOURNAL_TYPES[code] ?? null;\r\n}\r\n"],"mappings":";AAOA,MAAa,gBAAuD,OAAO,OAAO;CAChF,OAAoB;EAAE,MAAM;EAAS,MAAM;EAAiB,aAAa;EAAkC;CAC3G,WAAoB;EAAE,MAAM;EAAa,MAAM;EAAqB,aAAa;EAAsC;CACvH,eAAoB;EAAE,MAAM;EAAiB,MAAM;EAAyB,aAAa;EAAmC;CAC5H,eAAoB;EAAE,MAAM;EAAiB,MAAM;EAAyB,aAAa;EAA+B;CACxH,SAAoB;EAAE,MAAM;EAAW,MAAM;EAAmB,aAAa;EAAkD;CAC/H,SAAoB;EAAE,MAAM;EAAW,MAAM;EAAmB,aAAa;EAAyD;CACtI,WAAoB;EAAE,MAAM;EAAa,MAAM;EAAqB,aAAa;EAAuC;CACxH,cAAoB;EAAE,MAAM;EAAgB,MAAM;EAAwB,aAAa;EAAgD;CACvI,qBAAoB;EAAE,MAAM;EAAuB,MAAM;EAAuB,aAAa;EAAmC;CAChI,cAAoB;EAAE,MAAM;EAAgB,MAAM;EAAwB,aAAa;EAAkC;CACzH,UAAoB;EAAE,MAAM;EAAY,MAAM;EAAwB,aAAa;EAA2C;CAC9H,qBAAoB;EAAE,MAAM;EAAuB,MAAM;EAAuB,aAAa;EAAiD;CAC9I,kBAAoB;EAAE,MAAM;EAAoB,MAAM;EAAoB,aAAa;EAAyC;CAChI,KAAoB;EAAE,MAAM;EAAO,MAAM;EAAe,aAAa;EAA6C;CAClH,MAAoB;EAAE,MAAM;EAAQ,MAAM;EAAiB,aAAa;EAAiD;CAC1H,CAAC;AAEF,MAAa,gBAAgB,OAAO,OAClC,OAAO,YAAY,OAAO,KAAK,cAAc,CAAC,KAAI,MAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAChE;AAED,SAAgB,sBAAgC;AAC9C,QAAO,OAAO,KAAK,cAAc;;AAGnC,SAAgB,mBAAmB,MAAuB;AACxD,QAAO,QAAQ;;AAGjB,SAAgB,eAAe,MAAkC;AAC/D,QAAO,cAAc,SAAS"}
@@ -0,0 +1,15 @@
1
+ //#region src/utils/logger.d.ts
2
+ /**
3
+ * Minimal logger interface for the accounting package.
4
+ * Defaults to console. App layer can inject a real logger (Winston/Pino).
5
+ */
6
+ interface Logger {
7
+ warn(message: string, meta?: Record<string, unknown>): void;
8
+ error(message: string, meta?: Record<string, unknown>): void;
9
+ info(message: string, meta?: Record<string, unknown>): void;
10
+ }
11
+ /** Default console-based implementation */
12
+ declare const defaultLogger: Logger;
13
+ //#endregion
14
+ export { defaultLogger as n, Logger as t };
15
+ //# sourceMappingURL=logger-Cv6VVc4r.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger-Cv6VVc4r.d.mts","names":[],"sources":["../src/utils/logger.ts"],"mappings":";;AAIA;;;UAAiB,MAAA;EACf,IAAA,CAAK,OAAA,UAAiB,IAAA,GAAO,MAAA;EAC7B,KAAA,CAAM,OAAA,UAAiB,IAAA,GAAO,MAAA;EAC9B,IAAA,CAAK,OAAA,UAAiB,IAAA,GAAO,MAAA;AAAA;;cAIlB,aAAA,EAAe,MAAA"}
@@ -0,0 +1,129 @@
1
+ //#region src/money.d.ts
2
+ /**
3
+ * Money — Integer-cents arithmetic helpers for safe financial computation.
4
+ *
5
+ * Provides utilities that operate on **integer minor-unit values** (cents) to
6
+ * avoid floating-point rounding errors in intermediate calculations.
7
+ *
8
+ * **DB storage contract:**
9
+ * Journal entry `debit` / `credit` / `totalDebit` / `totalCredit` fields are
10
+ * stored as **integer cents** (e.g. 10050 for $100.50). All report outputs,
11
+ * aggregation results, and repository methods return integer cents.
12
+ *
13
+ * Use `Money.fromDecimal()` to convert user-facing dollar inputs to cents
14
+ * at the HTTP/API boundary. Use `Money.toDecimal()` or `Money.formatPlain()`
15
+ * to convert cents back to dollars for display or CSV export.
16
+ *
17
+ * Example workflow:
18
+ * const cents = Money.fromDecimal(req.body.debit); // 100.50 → 10050
19
+ * const taxCents = Money.percentage(cents, 5); // 5% → 502 (rounded)
20
+ * const display = Money.formatPlain(taxCents); // 502 → "5.02"
21
+ *
22
+ * Inspired by Stripe's money handling — simple, correct, auditable.
23
+ *
24
+ * @module @classytic/ledger/money
25
+ */
26
+ /** Round a floating-point value to the nearest integer cent */
27
+ declare function round(amount: number): number;
28
+ /** Convert a decimal dollar amount to integer cents: 10.50 → 1050 */
29
+ declare function fromDecimal(dollars: number, minorUnit?: number): number;
30
+ /** Convert integer cents to a decimal dollar amount: 1050 → 10.50 */
31
+ declare function toDecimal(cents: number, minorUnit?: number): number;
32
+ /** Add two cent amounts */
33
+ declare function add(a: number, b: number): number;
34
+ /** Subtract b from a in cents */
35
+ declare function subtract(a: number, b: number): number;
36
+ /** Multiply cents by a factor, rounding to nearest cent */
37
+ declare function multiply(cents: number, factor: number): number;
38
+ /**
39
+ * Calculate a percentage of a cent amount.
40
+ * percentage(10000, 5) → 500 (5% of $100.00 = $5.00)
41
+ */
42
+ declare function percentage(cents: number, rate: number): number;
43
+ /**
44
+ * Calculate tax from a tax-inclusive amount.
45
+ * splitTaxInclusive(10500, 0.05) → { base: 10000, tax: 500 }
46
+ */
47
+ declare function splitTaxInclusive(inclusiveAmount: number, taxRate: number): {
48
+ base: number;
49
+ tax: number;
50
+ };
51
+ /**
52
+ * Calculate tax from a tax-exclusive amount.
53
+ * splitTaxExclusive(10000, 0.05) → { base: 10000, tax: 500, total: 10500 }
54
+ */
55
+ declare function splitTaxExclusive(exclusiveAmount: number, taxRate: number): {
56
+ base: number;
57
+ tax: number;
58
+ total: number;
59
+ };
60
+ /**
61
+ * Allocate cents across ratios with zero remainder error.
62
+ * Uses largest-remainder method (same as parliamentary seat allocation).
63
+ *
64
+ * allocate(1000, [1, 1, 1]) → [334, 333, 333] (sums to 1000 exactly)
65
+ * allocate(10000, [50, 30, 20]) → [5000, 3000, 2000]
66
+ */
67
+ declare function allocate(totalCents: number, ratios: number[]): number[];
68
+ /** Are two cent amounts equal? */
69
+ declare function equals(a: number, b: number): boolean;
70
+ /** Is the amount zero? */
71
+ declare function isZero(cents: number): boolean;
72
+ /** Is the amount positive (> 0)? */
73
+ declare function isPositive(cents: number): boolean;
74
+ /** Is the amount negative (< 0)? */
75
+ declare function isNegative(cents: number): boolean;
76
+ /** Absolute value */
77
+ declare function abs(cents: number): number;
78
+ /** Negate */
79
+ declare function negate(cents: number): number;
80
+ /** Min of two amounts */
81
+ declare function min(a: number, b: number): number;
82
+ /** Max of two amounts */
83
+ declare function max(a: number, b: number): number;
84
+ /**
85
+ * Format cents as a currency string.
86
+ * format(10550, 'CAD') → "$105.50"
87
+ * format(10550, 'CAD', 'en-CA') → "$105.50"
88
+ */
89
+ declare function format(cents: number, currencyCode?: string, locale?: string, minorUnit?: number): string;
90
+ /**
91
+ * Format cents as a plain decimal string (no currency symbol).
92
+ * formatPlain(10550) → "105.50"
93
+ */
94
+ declare function formatPlain(cents: number, minorUnit?: number): string;
95
+ /** Is the value a valid integer cent amount? */
96
+ declare function isValid(value: unknown): value is number;
97
+ /**
98
+ * Parse a string or number into cents.
99
+ * parseCents("105.50") → 10550
100
+ * parseCents(105.50) → 10550
101
+ */
102
+ declare function parseCents(input: string | number, minorUnit?: number): number;
103
+ declare const Money: {
104
+ readonly round: typeof round;
105
+ readonly fromDecimal: typeof fromDecimal;
106
+ readonly toDecimal: typeof toDecimal;
107
+ readonly add: typeof add;
108
+ readonly subtract: typeof subtract;
109
+ readonly multiply: typeof multiply;
110
+ readonly percentage: typeof percentage;
111
+ readonly splitTaxInclusive: typeof splitTaxInclusive;
112
+ readonly splitTaxExclusive: typeof splitTaxExclusive;
113
+ readonly allocate: typeof allocate;
114
+ readonly equals: typeof equals;
115
+ readonly isZero: typeof isZero;
116
+ readonly isPositive: typeof isPositive;
117
+ readonly isNegative: typeof isNegative;
118
+ readonly abs: typeof abs;
119
+ readonly negate: typeof negate;
120
+ readonly min: typeof min;
121
+ readonly max: typeof max;
122
+ readonly format: typeof format;
123
+ readonly formatPlain: typeof formatPlain;
124
+ readonly isValid: typeof isValid;
125
+ readonly parseCents: typeof parseCents;
126
+ };
127
+ //#endregion
128
+ export { Money, abs, add, allocate, equals, format, formatPlain, fromDecimal, isNegative, isPositive, isValid, isZero, max, min, multiply, negate, parseCents, percentage, round, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal };
129
+ //# sourceMappingURL=money.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.d.mts","names":[],"sources":["../src/money.ts"],"mappings":";;AA4BA;;;;;AAKA;;;;;AAMA;;;;;AAMA;;;;;AAKA;;;;iBAtBgB,KAAA,CAAM,MAAA;AA2BtB;AAAA,iBAtBgB,WAAA,CAAY,OAAA,UAAiB,SAAA;;iBAM7B,SAAA,CAAU,KAAA,UAAe,SAAA;;iBAMzB,GAAA,CAAI,CAAA,UAAW,CAAA;;iBAKf,QAAA,CAAS,CAAA,UAAW,CAAA;;iBAKpB,QAAA,CAAS,KAAA,UAAe,MAAA;AAgBxC;;;;AAAA,iBARgB,UAAA,CAAW,KAAA,UAAe,IAAA;;;;;iBAQ1B,iBAAA,CACd,eAAA,UACA,OAAA;EACG,IAAA;EAAc,GAAA;AAAA;;;;;iBAUH,iBAAA,CACd,eAAA,UACA,OAAA;EACG,IAAA;EAAc,GAAA;EAAa,KAAA;AAAA;;;;;AA0ChC;;;iBA5BgB,QAAA,CAAS,UAAA,UAAoB,MAAA;;iBA4B7B,MAAA,CAAO,CAAA,UAAW,CAAA;;iBAKlB,MAAA,CAAO,KAAA;;iBAKP,UAAA,CAAW,KAAA;AAA3B;AAAA,iBAKgB,UAAA,CAAW,KAAA;;iBAKX,GAAA,CAAI,KAAA;;iBAKJ,MAAA,CAAO,KAAA;;iBAKP,GAAA,CAAI,CAAA,UAAW,CAAA;;iBAKf,GAAA,CAAI,CAAA,UAAW,CAAA;AAf/B;;;;;AAAA,iBA0BgB,MAAA,CACd,KAAA,UACA,YAAA,WACA,MAAA,WACA,SAAA;;;;;iBAac,WAAA,CAAY,KAAA,UAAe,SAAA;;iBAO3B,OAAA,CAAQ,KAAA,YAAiB,KAAA;;;AAnCzC;;;iBA4CgB,UAAA,CAAW,KAAA,mBAAwB,SAAA;AAAA,cAWtC,KAAA;EAAA"}
package/dist/money.mjs ADDED
@@ -0,0 +1,197 @@
1
+ //#region src/money.ts
2
+ /**
3
+ * Money — Integer-cents arithmetic helpers for safe financial computation.
4
+ *
5
+ * Provides utilities that operate on **integer minor-unit values** (cents) to
6
+ * avoid floating-point rounding errors in intermediate calculations.
7
+ *
8
+ * **DB storage contract:**
9
+ * Journal entry `debit` / `credit` / `totalDebit` / `totalCredit` fields are
10
+ * stored as **integer cents** (e.g. 10050 for $100.50). All report outputs,
11
+ * aggregation results, and repository methods return integer cents.
12
+ *
13
+ * Use `Money.fromDecimal()` to convert user-facing dollar inputs to cents
14
+ * at the HTTP/API boundary. Use `Money.toDecimal()` or `Money.formatPlain()`
15
+ * to convert cents back to dollars for display or CSV export.
16
+ *
17
+ * Example workflow:
18
+ * const cents = Money.fromDecimal(req.body.debit); // 100.50 → 10050
19
+ * const taxCents = Money.percentage(cents, 5); // 5% → 502 (rounded)
20
+ * const display = Money.formatPlain(taxCents); // 502 → "5.02"
21
+ *
22
+ * Inspired by Stripe's money handling — simple, correct, auditable.
23
+ *
24
+ * @module @classytic/ledger/money
25
+ */
26
+ /** Round a floating-point value to the nearest integer cent */
27
+ function round(amount) {
28
+ return Math.round(amount);
29
+ }
30
+ /** Convert a decimal dollar amount to integer cents: 10.50 → 1050 */
31
+ function fromDecimal(dollars, minorUnit = 2) {
32
+ const factor = 10 ** minorUnit;
33
+ return Math.round(dollars * factor);
34
+ }
35
+ /** Convert integer cents to a decimal dollar amount: 1050 → 10.50 */
36
+ function toDecimal(cents, minorUnit = 2) {
37
+ return cents / 10 ** minorUnit;
38
+ }
39
+ /** Add two cent amounts */
40
+ function add(a, b) {
41
+ return a + b;
42
+ }
43
+ /** Subtract b from a in cents */
44
+ function subtract(a, b) {
45
+ return a - b;
46
+ }
47
+ /** Multiply cents by a factor, rounding to nearest cent */
48
+ function multiply(cents, factor) {
49
+ return Math.round(cents * factor);
50
+ }
51
+ /**
52
+ * Calculate a percentage of a cent amount.
53
+ * percentage(10000, 5) → 500 (5% of $100.00 = $5.00)
54
+ */
55
+ function percentage(cents, rate) {
56
+ return Math.round(cents * rate / 100);
57
+ }
58
+ /**
59
+ * Calculate tax from a tax-inclusive amount.
60
+ * splitTaxInclusive(10500, 0.05) → { base: 10000, tax: 500 }
61
+ */
62
+ function splitTaxInclusive(inclusiveAmount, taxRate) {
63
+ const base = Math.round(inclusiveAmount / (1 + taxRate));
64
+ return {
65
+ base,
66
+ tax: inclusiveAmount - base
67
+ };
68
+ }
69
+ /**
70
+ * Calculate tax from a tax-exclusive amount.
71
+ * splitTaxExclusive(10000, 0.05) → { base: 10000, tax: 500, total: 10500 }
72
+ */
73
+ function splitTaxExclusive(exclusiveAmount, taxRate) {
74
+ const tax = Math.round(exclusiveAmount * taxRate);
75
+ return {
76
+ base: exclusiveAmount,
77
+ tax,
78
+ total: exclusiveAmount + tax
79
+ };
80
+ }
81
+ /**
82
+ * Allocate cents across ratios with zero remainder error.
83
+ * Uses largest-remainder method (same as parliamentary seat allocation).
84
+ *
85
+ * allocate(1000, [1, 1, 1]) → [334, 333, 333] (sums to 1000 exactly)
86
+ * allocate(10000, [50, 30, 20]) → [5000, 3000, 2000]
87
+ */
88
+ function allocate(totalCents, ratios) {
89
+ if (ratios.length === 0) throw new Error("Ratios must be non-empty");
90
+ if (ratios.some((r) => r < 0)) throw new Error("Ratios must be non-negative");
91
+ const ratioSum = ratios.reduce((s, r) => s + r, 0);
92
+ if (ratioSum === 0) throw new Error("Sum of ratios must be > 0");
93
+ const allocations = ratios.map((r) => Math.floor(totalCents * r / ratioSum));
94
+ let remainder = totalCents - allocations.reduce((s, a) => s + a, 0);
95
+ const fractions = ratios.map((r, i) => ({
96
+ index: i,
97
+ frac: totalCents * r / ratioSum - allocations[i]
98
+ }));
99
+ fractions.sort((a, b) => b.frac - a.frac);
100
+ for (let i = 0; i < remainder; i++) allocations[fractions[i].index]++;
101
+ return allocations;
102
+ }
103
+ /** Are two cent amounts equal? */
104
+ function equals(a, b) {
105
+ return a === b;
106
+ }
107
+ /** Is the amount zero? */
108
+ function isZero(cents) {
109
+ return cents === 0;
110
+ }
111
+ /** Is the amount positive (> 0)? */
112
+ function isPositive(cents) {
113
+ return cents > 0;
114
+ }
115
+ /** Is the amount negative (< 0)? */
116
+ function isNegative(cents) {
117
+ return cents < 0;
118
+ }
119
+ /** Absolute value */
120
+ function abs(cents) {
121
+ return Math.abs(cents);
122
+ }
123
+ /** Negate */
124
+ function negate(cents) {
125
+ return -cents;
126
+ }
127
+ /** Min of two amounts */
128
+ function min(a, b) {
129
+ return Math.min(a, b);
130
+ }
131
+ /** Max of two amounts */
132
+ function max(a, b) {
133
+ return Math.max(a, b);
134
+ }
135
+ /**
136
+ * Format cents as a currency string.
137
+ * format(10550, 'CAD') → "$105.50"
138
+ * format(10550, 'CAD', 'en-CA') → "$105.50"
139
+ */
140
+ function format(cents, currencyCode = "CAD", locale = "en-CA", minorUnit = 2) {
141
+ const dollars = toDecimal(cents, minorUnit);
142
+ return new Intl.NumberFormat(locale, {
143
+ style: "currency",
144
+ currency: currencyCode
145
+ }).format(dollars);
146
+ }
147
+ /**
148
+ * Format cents as a plain decimal string (no currency symbol).
149
+ * formatPlain(10550) → "105.50"
150
+ */
151
+ function formatPlain(cents, minorUnit = 2) {
152
+ return toDecimal(cents, minorUnit).toFixed(minorUnit);
153
+ }
154
+ /** Is the value a valid integer cent amount? */
155
+ function isValid(value) {
156
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
157
+ }
158
+ /**
159
+ * Parse a string or number into cents.
160
+ * parseCents("105.50") → 10550
161
+ * parseCents(105.50) → 10550
162
+ */
163
+ function parseCents(input, minorUnit = 2) {
164
+ if (typeof input === "number") return fromDecimal(input, minorUnit);
165
+ const cleaned = input.replace(/[$,\s]/g, "");
166
+ const parsed = parseFloat(cleaned);
167
+ if (isNaN(parsed)) throw new Error(`Cannot parse "${input}" as money`);
168
+ return fromDecimal(parsed, minorUnit);
169
+ }
170
+ const Money = {
171
+ round,
172
+ fromDecimal,
173
+ toDecimal,
174
+ add,
175
+ subtract,
176
+ multiply,
177
+ percentage,
178
+ splitTaxInclusive,
179
+ splitTaxExclusive,
180
+ allocate,
181
+ equals,
182
+ isZero,
183
+ isPositive,
184
+ isNegative,
185
+ abs,
186
+ negate,
187
+ min,
188
+ max,
189
+ format,
190
+ formatPlain,
191
+ isValid,
192
+ parseCents
193
+ };
194
+
195
+ //#endregion
196
+ export { Money, abs, add, allocate, equals, format, formatPlain, fromDecimal, isNegative, isPositive, isValid, isZero, max, min, multiply, negate, parseCents, percentage, round, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal };
197
+ //# sourceMappingURL=money.mjs.map