@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
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"}
|
package/dist/money.d.mts
ADDED
|
@@ -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
|