@classytic/ledger 0.6.0 → 0.8.0

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/dist/index.mjs CHANGED
@@ -3,11 +3,12 @@ import { Money, add, allocate, format, formatPlain, fromDecimal, multiply, parse
3
3
  import { n as Errors, t as AccountingError } from "./errors-CSDQPNyt.mjs";
4
4
  import { C as isVirtualTaxAccount, E as requireOrgScope, S as computeEndingBalance, T as generateAgedBalance, _ as buildItemFilters, a as finalizeSession, b as buildAccountTypeMap, c as generateRevaluation, d as generateIncomeStatement, f as generateGeneralLedger, g as generateBalanceSheet, h as generateBudgetVsActual, i as acquireSession, l as buildRevaluationEntry, m as generateCashFlow, n as closeFiscalPeriod, o as defaultLogger, p as generateDimensionBreakdown, r as reopenFiscalPeriod, s as generateTrialBalance, t as generatePartnerLedger, u as computeRevaluation, v as getDateRange, w as DEFAULT_BUCKETS, x as calculateTotal, y as getFiscalYearStart } from "./partner-ledger-D9H5hegI.mjs";
5
5
  import { c as getNormalBalance, d as isValidCategory, l as isBalanceSheet, n as CATEGORY_KEYS, t as CATEGORIES, u as isIncomeStatement } from "./categories-BkKdv16V.mjs";
6
- import { a as taxLockPlugin, c as createLockPlugin, i as fiscalLockPlugin, l as idempotencyPlugin, n as creditLimitPlugin, o as watermarkResolver, r as dailyLockPlugin, s as periodResolver, t as fxRealizationPlugin, u as doubleEntryPlugin } from "./fx-realization.plugin-CgQFDGv2.mjs";
6
+ import { a as watermarkResolver, c as idempotencyPlugin, i as fiscalLockPlugin, l as doubleEntryPlugin, n as creditLimitPlugin, o as periodResolver, r as dailyLockPlugin, s as createLockPlugin, t as fxRealizationPlugin } from "./fx-realization.plugin-DDVK-oYO.mjs";
7
+ import { t as buildOpeningBalanceEntry } from "./opening-balance-DPXmAIzN.mjs";
7
8
  import { defineCountryPack } from "./country/index.mjs";
8
- import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-BP-0Ni5W.mjs";
9
+ import { a as exportToCsv, i as quickbooksFieldMap, r as universalFieldMap, t as flattenJournalEntries } from "./exports-B3whucXe.mjs";
10
+ import { QueryParser, Repository } from "@classytic/mongokit";
9
11
  import mongoose, { Schema } from "mongoose";
10
- import { Repository } from "@classytic/mongokit";
11
12
  //#region src/schemas/currency-field.ts
12
13
  /**
13
14
  * Build the Mongoose currency field definition.
@@ -919,11 +920,10 @@ function createModels(connection, config) {
919
920
  * onto an existing mongokit Repository.
920
921
  *
921
922
  * @param repository - A mongokit Repository instance (already created)
922
- * @param AccountModel - The Mongoose model for accounts
923
923
  * @param country - The CountryPack for account type lookups
924
924
  * @param orgField - The multi-tenant field name (e.g. 'business')
925
925
  */
926
- function wireAccountMethods(repository, AccountModel, country, orgField) {
926
+ function wireAccountMethods(repository, country, orgField) {
927
927
  repository.on("before:create", (ctx) => {
928
928
  const code = ctx.data?.accountTypeCode;
929
929
  if (code && !country.isPostingAccount(code)) throw Errors.validation(`Cannot create account with type "${code}" — it is a structural group or calculated total, not a posting account.`);
@@ -936,7 +936,10 @@ function wireAccountMethods(repository, AccountModel, country, orgField) {
936
936
  const postingTypes = country.getPostingAccountTypes();
937
937
  const filter = {};
938
938
  if (orgField && orgId != null) filter[orgField] = orgId;
939
- const existing = await AccountModel.find(filter).select("accountNumber").lean();
939
+ const existing = await repository.findAll(filter, {
940
+ select: { accountNumber: 1 },
941
+ lean: true
942
+ });
940
943
  const existingNumbers = new Set(existing.map((a) => a.accountNumber));
941
944
  const toCreate = postingTypes.filter((at) => !existingNumbers.has(at.code)).map((at) => {
942
945
  const doc = {
@@ -953,7 +956,7 @@ function wireAccountMethods(repository, AccountModel, country, orgField) {
953
956
  };
954
957
  try {
955
958
  return {
956
- created: (await AccountModel.insertMany(toCreate, {
959
+ created: (await repository.createMany(toCreate, {
957
960
  session: options.session ?? void 0,
958
961
  ordered: false
959
962
  })).length,
@@ -1034,7 +1037,10 @@ function wireAccountMethods(repository, AccountModel, country, orgField) {
1034
1037
  };
1035
1038
  const existsFilter = { accountNumber: { $in: validAccounts.map((a) => a.accountNumber) } };
1036
1039
  if (orgField && orgId != null) existsFilter[orgField] = orgId;
1037
- const existingDocs = await AccountModel.find(existsFilter).select("accountNumber").lean();
1040
+ const existingDocs = await repository.findAll(existsFilter, {
1041
+ select: { accountNumber: 1 },
1042
+ lean: true
1043
+ });
1038
1044
  const existingNumbers = new Set(existingDocs.map((d) => d.accountNumber));
1039
1045
  const toCreate = [];
1040
1046
  for (const item of validAccounts) if (existingNumbers.has(item.accountNumber)) results.skipped.push({
@@ -1056,7 +1062,7 @@ function wireAccountMethods(repository, AccountModel, country, orgField) {
1056
1062
  return doc;
1057
1063
  });
1058
1064
  try {
1059
- const inserted = await AccountModel.insertMany(docs, { ordered: false });
1065
+ const inserted = await repository.createMany(docs, { ordered: false });
1060
1066
  results.created = toCreate.map((item, idx) => ({
1061
1067
  accountTypeCode: item.accountTypeCode,
1062
1068
  active: item.active,
@@ -1541,6 +1547,7 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
1541
1547
  const create = repository.create.bind(repository);
1542
1548
  const deleteById = repository.delete.bind(repository);
1543
1549
  const repoInstance = repository;
1550
+ const emitHook = repoInstance.emitAsync.bind(repoInstance);
1544
1551
  repository.match = async (input) => {
1545
1552
  const { account, items, note, reconciledBy, organizationId, session = null } = input;
1546
1553
  let { matchingNumber } = input;
@@ -1585,6 +1592,17 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
1585
1592
  const isFullReconcile = difference === 0;
1586
1593
  const sharedCurrency = currencies.size === 1 ? Array.from(currencies)[0] : null;
1587
1594
  if (!matchingNumber) matchingNumber = await nextMatchingNumber(ReconciliationModel, orgField, organizationId, session);
1595
+ const hookCtx = {
1596
+ input,
1597
+ items: itemSnapshots,
1598
+ sharedCurrency,
1599
+ matchingNumber,
1600
+ debitTotal,
1601
+ creditTotal,
1602
+ isFullReconcile,
1603
+ session
1604
+ };
1605
+ await emitHook("before:match", hookCtx);
1588
1606
  const bulkOps = itemSnapshots.map((snap) => ({ updateOne: {
1589
1607
  filter: { _id: snap.entry },
1590
1608
  update: { $set: { [`journalItems.${snap.itemIndex}.matchingNumber`]: matchingNumber } }
@@ -1612,12 +1630,9 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
1612
1630
  };
1613
1631
  if (orgField && organizationId != null) reconciliationData[orgField] = organizationId;
1614
1632
  const record = await create(reconciliationData);
1615
- const emit = repoInstance._emitHook;
1616
- if (emit) await emit.call(repoInstance, "after:match", {
1617
- reconciliation: record,
1618
- items: itemSnapshots,
1619
- sharedCurrency,
1620
- session
1633
+ await emitHook("after:match", {
1634
+ ...hookCtx,
1635
+ reconciliation: record
1621
1636
  });
1622
1637
  return record;
1623
1638
  };
@@ -1628,13 +1643,23 @@ function wireReconciliationMethods(repository, ReconciliationModel, JournalEntry
1628
1643
  if (orgField && organizationId != null) query[orgField] = organizationId;
1629
1644
  const existing = await ReconciliationModel.findOne(query).session(session).lean();
1630
1645
  if (!existing) throw Errors.notFound(`Reconciliation ${matchingNumber} not found`);
1631
- const bulkOps = (existing.items ?? []).map((it) => ({ updateOne: {
1646
+ const items = existing.items ?? [];
1647
+ const unmatchCtx = {
1648
+ matchingNumber,
1649
+ reconciliation: existing,
1650
+ items,
1651
+ organizationId,
1652
+ session
1653
+ };
1654
+ await emitHook("before:unmatch", unmatchCtx);
1655
+ const bulkOps = items.map((it) => ({ updateOne: {
1632
1656
  filter: { _id: it.entry },
1633
1657
  update: { $set: { [`journalItems.${it.itemIndex}.matchingNumber`]: null } }
1634
1658
  } }));
1635
1659
  if (bulkOps.length > 0) await JournalEntryModel.bulkWrite(bulkOps, { session: session ?? void 0 });
1636
1660
  const result = await deleteById(String(existing._id));
1637
1661
  if (!result.success) throw Errors.notFound("Failed to delete reconciliation record");
1662
+ await emitHook("after:unmatch", unmatchCtx);
1638
1663
  return result;
1639
1664
  };
1640
1665
  repository.getOpenItems = async (params) => {
@@ -1727,7 +1752,7 @@ function createRepositories(models, config, plugins = {}, pagination = {}) {
1727
1752
  const fpPagination = pagination.fiscalPeriod ?? {};
1728
1753
  const budgetPagination = pagination.budget ?? {};
1729
1754
  const reconPagination = pagination.reconciliation ?? {};
1730
- const accounts = wireAccountMethods(new Repository(models.Account, plugins.account ?? [], accountPagination), models.Account, country, orgField);
1755
+ const accounts = wireAccountMethods(new Repository(models.Account, plugins.account ?? [], accountPagination), country, orgField);
1731
1756
  const jePlugins = [
1732
1757
  ...plugins.journalEntry ?? [],
1733
1758
  doubleEntryPlugin({
@@ -2055,10 +2080,6 @@ function buildIntrospectAPI({ models, country, config }) {
2055
2080
  return Object.freeze([...builtIn, ...custom]);
2056
2081
  };
2057
2082
  const reports = () => REPORT_CATALOG;
2058
- const taxCodes = (region) => {
2059
- if (region) return Object.freeze(country.getTaxCodesForRegion(region));
2060
- return Object.freeze(Object.values(country.taxCodes));
2061
- };
2062
2083
  const fiscalPeriods = async (organizationId, session = null) => {
2063
2084
  const filter = {};
2064
2085
  if (orgField && organizationId != null) filter[orgField] = organizationId;
@@ -2076,32 +2097,21 @@ function buildIntrospectAPI({ models, country, config }) {
2076
2097
  accounts: await accounts(organizationId),
2077
2098
  journalTypes: journalTypes(),
2078
2099
  reports: reports(),
2079
- taxCodes: taxCodes(),
2080
2100
  fiscalPeriods: await fiscalPeriods(organizationId)
2081
2101
  });
2082
2102
  return {
2083
2103
  accounts,
2084
2104
  journalTypes,
2085
2105
  reports,
2086
- taxCodes,
2087
2106
  fiscalPeriods,
2088
2107
  catalog
2089
2108
  };
2090
2109
  }
2091
2110
  //#endregion
2092
2111
  //#region src/semantic/record.ts
2093
- function buildRecordAPI({ models, repositories, country, config }) {
2112
+ function buildRecordAPI({ models, repositories, config }) {
2094
2113
  const AccountModel = models.Account;
2095
2114
  const orgField = config.multiTenant?.orgField;
2096
- const lookupTaxRate = (taxCode) => {
2097
- const tc = country.taxCodes[taxCode];
2098
- if (!tc) throw Errors.notFound(`Tax code '${taxCode}' not found in country pack.`, [{
2099
- path: "tax.code",
2100
- issue: "unknown tax code",
2101
- value: taxCode
2102
- }]);
2103
- return tc.rate;
2104
- };
2105
2115
  const resolveAccounts = async (organizationId, codes, path, session) => {
2106
2116
  const unique = Array.from(new Set(codes));
2107
2117
  const filter = { accountTypeCode: { $in: unique } };
@@ -2155,26 +2165,8 @@ function buildRecordAPI({ models, repositories, country, config }) {
2155
2165
  });
2156
2166
  const sale = async (organizationId, input, options) => {
2157
2167
  validateAmount(input.amount, "amount");
2158
- let baseAmount = input.amount;
2159
- let taxAmount = 0;
2160
- if (input.tax) {
2161
- const rate = lookupTaxRate(input.tax.code);
2162
- if (input.tax.inclusive) {
2163
- const split = splitTaxInclusive(input.amount, rate);
2164
- baseAmount = split.base;
2165
- taxAmount = split.tax;
2166
- } else {
2167
- const split = splitTaxExclusive(input.amount, rate);
2168
- baseAmount = split.base;
2169
- taxAmount = split.tax;
2170
- }
2171
- }
2172
- const totalCharge = baseAmount + taxAmount;
2173
- const codes = [input.receivableAccount, input.revenueAccount];
2174
- if (input.tax) codes.push(input.tax.account);
2175
- const acctMap = await resolveAccounts(organizationId, codes, "receivableAccount", options?.session ?? null);
2176
- const items = [buildItem(acctMap.get(input.receivableAccount), totalCharge, 0, input.label, input.dimensions), buildItem(acctMap.get(input.revenueAccount), 0, baseAmount, input.label, input.dimensions)];
2177
- if (input.tax) items.push(buildItem(acctMap.get(input.tax.account), 0, taxAmount, `${input.label ?? "Sale"} — ${input.tax.code}`, input.dimensions));
2168
+ const acctMap = await resolveAccounts(organizationId, [input.receivableAccount, input.revenueAccount], "receivableAccount", options?.session ?? null);
2169
+ const items = [buildItem(acctMap.get(input.receivableAccount), input.amount, 0, input.label, input.dimensions), buildItem(acctMap.get(input.revenueAccount), 0, input.amount, input.label, input.dimensions)];
2178
2170
  return postEntry(organizationId, {
2179
2171
  journalType: input.journalType ?? "SALES",
2180
2172
  date: input.date,
@@ -2185,27 +2177,8 @@ function buildRecordAPI({ models, repositories, country, config }) {
2185
2177
  };
2186
2178
  const expense = async (organizationId, input, options) => {
2187
2179
  validateAmount(input.amount, "amount");
2188
- let baseAmount = input.amount;
2189
- let taxAmount = 0;
2190
- if (input.tax) {
2191
- const rate = lookupTaxRate(input.tax.code);
2192
- if (input.tax.inclusive) {
2193
- const split = splitTaxInclusive(input.amount, rate);
2194
- baseAmount = split.base;
2195
- taxAmount = split.tax;
2196
- } else {
2197
- const split = splitTaxExclusive(input.amount, rate);
2198
- baseAmount = split.base;
2199
- taxAmount = split.tax;
2200
- }
2201
- }
2202
- const totalPaid = baseAmount + taxAmount;
2203
- const codes = [input.expenseAccount, input.paidFromAccount];
2204
- if (input.tax) codes.push(input.tax.account);
2205
- const acctMap = await resolveAccounts(organizationId, codes, "expenseAccount", options?.session ?? null);
2206
- const items = [buildItem(acctMap.get(input.expenseAccount), baseAmount, 0, input.label, input.dimensions)];
2207
- if (input.tax) items.push(buildItem(acctMap.get(input.tax.account), taxAmount, 0, `${input.label ?? "Expense"} — ${input.tax.code} ITC`, input.dimensions));
2208
- items.push(buildItem(acctMap.get(input.paidFromAccount), 0, totalPaid, input.label, input.dimensions));
2180
+ const acctMap = await resolveAccounts(organizationId, [input.expenseAccount, input.paidFromAccount], "expenseAccount", options?.session ?? null);
2181
+ const items = [buildItem(acctMap.get(input.expenseAccount), input.amount, 0, input.label, input.dimensions), buildItem(acctMap.get(input.paidFromAccount), 0, input.amount, input.label, input.dimensions)];
2209
2182
  return postEntry(organizationId, {
2210
2183
  journalType: input.journalType ?? "PURCHASES",
2211
2184
  date: input.date,
@@ -2307,16 +2280,80 @@ function buildRecordAPI({ models, repositories, country, config }) {
2307
2280
  journalItems: items
2308
2281
  }, options);
2309
2282
  };
2283
+ const openingBalance = async (organizationId, input, options) => {
2284
+ if (!input.balances || input.balances.length === 0) throw Errors.validation("Opening balance requires at least one account balance.", [{
2285
+ path: "balances",
2286
+ issue: "must contain at least 1 entry",
2287
+ value: 0
2288
+ }]);
2289
+ const equityCode = input.equityAccount ?? config.country?.retainedEarningsAccountCode;
2290
+ if (!equityCode) throw Errors.validation("Equity contra account code is required. Pass equityAccount or configure retainedEarningsAccountCode in the country pack.", [{
2291
+ path: "equityAccount",
2292
+ issue: "required",
2293
+ value: void 0
2294
+ }]);
2295
+ const result = buildOpeningBalanceEntry({
2296
+ cutoverDate: input.cutoverDate,
2297
+ balances: input.balances.map((b) => ({
2298
+ accountCode: b.account,
2299
+ balance: b.balance
2300
+ })),
2301
+ equityAccountCode: equityCode,
2302
+ label: input.label
2303
+ });
2304
+ const acctMap = await resolveAccounts(organizationId, result.entry.journalItems.map((item) => item.account), "balances", options?.session ?? null);
2305
+ const items = result.entry.journalItems.map((item) => buildItem(acctMap.get(item.account), item.debit, item.credit, item.label));
2306
+ return postEntry(organizationId, {
2307
+ journalType: result.entry.journalType ?? "GENERAL",
2308
+ date: result.entry.date,
2309
+ label: result.entry.label,
2310
+ journalItems: items,
2311
+ ...result.entry.extra
2312
+ }, options);
2313
+ };
2310
2314
  return {
2311
2315
  sale,
2312
2316
  expense,
2313
2317
  transfer,
2314
2318
  payment,
2315
- adjustment
2319
+ adjustment,
2320
+ openingBalance
2316
2321
  };
2317
2322
  }
2318
2323
  //#endregion
2319
2324
  //#region src/engine.ts
2325
+ /**
2326
+ * AccountingEngine — The main entry point for @classytic/ledger.
2327
+ *
2328
+ * The engine owns all models, repositories, and reports. Matches the
2329
+ * @classytic/flow and @classytic/promo pattern: pass a mongoose connection
2330
+ * in config, and everything is auto-wired.
2331
+ *
2332
+ * @example
2333
+ * ```typescript
2334
+ * import mongoose from 'mongoose';
2335
+ * import { createAccountingEngine } from '@classytic/ledger';
2336
+ * import { canadaPack } from '@classytic/ledger-ca';
2337
+ *
2338
+ * const engine = createAccountingEngine({
2339
+ * mongoose: mongoose.connection,
2340
+ * country: canadaPack,
2341
+ * currency: 'CAD',
2342
+ * multiTenant: { orgField: 'organizationId', orgRef: 'Organization' },
2343
+ * });
2344
+ *
2345
+ * // Models — auto-created Mongoose models
2346
+ * engine.models.Account
2347
+ * engine.models.JournalEntry
2348
+ *
2349
+ * // Repositories — plugins + domain methods pre-wired
2350
+ * await engine.repositories.accounts.seedAccounts(orgId);
2351
+ * await engine.repositories.journalEntries.post(entryId, orgId);
2352
+ *
2353
+ * // Reports — bound to owned models
2354
+ * const bs = await engine.reports.balanceSheet({ organizationId: orgId, dateOption: 'year', dateValue: 2025 });
2355
+ * ```
2356
+ */
2320
2357
  var AccountingEngine = class {
2321
2358
  config;
2322
2359
  country;
@@ -2337,7 +2374,6 @@ var AccountingEngine = class {
2337
2374
  this.record = buildRecordAPI({
2338
2375
  models: this.models,
2339
2376
  repositories: this.repositories,
2340
- country: this.country,
2341
2377
  config: this.config
2342
2378
  });
2343
2379
  this.introspect = buildIntrospectAPI({
@@ -2366,9 +2402,59 @@ var AccountingEngine = class {
2366
2402
  getAccountType(code) {
2367
2403
  return this.country.getAccountType(code);
2368
2404
  }
2369
- /** Get tax codes for a region */
2370
- getTaxCodesForRegion(region) {
2371
- return this.country.getTaxCodesForRegion(region);
2405
+ /**
2406
+ * Create a pre-configured QueryParser for URL-driven queries against
2407
+ * ledger repositories. Returns a mongokit QueryParser with the correct
2408
+ * schema and pagination limits for the specified model.
2409
+ *
2410
+ * @param model - Which ledger model to parse queries for
2411
+ * @param overrides - Additional QueryParserOptions to merge
2412
+ *
2413
+ * @example
2414
+ * ```typescript
2415
+ * const parser = engine.createQueryParser('journalEntry');
2416
+ * const parsed = parser.parse(req.query);
2417
+ * const result = await engine.repositories.journalEntries.getAll({
2418
+ * ...parsed,
2419
+ * filters: { ...parsed.filters, organizationId },
2420
+ * });
2421
+ * ```
2422
+ */
2423
+ createQueryParser(model, overrides) {
2424
+ const paginationConfig = this.config.pagination ?? {};
2425
+ const entry = {
2426
+ account: {
2427
+ model: this.models.Account,
2428
+ pagination: paginationConfig.account
2429
+ },
2430
+ journalEntry: {
2431
+ model: this.models.JournalEntry,
2432
+ pagination: paginationConfig.journalEntry
2433
+ },
2434
+ fiscalPeriod: {
2435
+ model: this.models.FiscalPeriod,
2436
+ pagination: paginationConfig.fiscalPeriod
2437
+ },
2438
+ budget: {
2439
+ model: this.models.Budget,
2440
+ pagination: paginationConfig.budget
2441
+ },
2442
+ reconciliation: {
2443
+ model: this.models.Reconciliation,
2444
+ pagination: paginationConfig.reconciliation
2445
+ },
2446
+ journal: {
2447
+ model: this.models.Journal,
2448
+ pagination: paginationConfig.journal
2449
+ }
2450
+ }[model];
2451
+ if (!entry) throw new Error(`createQueryParser: unknown model "${model}"`);
2452
+ return new QueryParser({
2453
+ schema: entry.model.schema,
2454
+ maxLimit: entry.pagination?.maxLimit ?? 100,
2455
+ searchMode: "regex",
2456
+ ...overrides
2457
+ });
2372
2458
  }
2373
2459
  _buildReports() {
2374
2460
  const AccountModel = this.models.Account;
@@ -2450,68 +2536,6 @@ function createAccountingEngine(config) {
2450
2536
  return new AccountingEngine(config);
2451
2537
  }
2452
2538
  //#endregion
2453
- //#region src/utils/repartition-tax.ts
2454
- /**
2455
- * Default role resolver used when the country pack doesn't override.
2456
- * Walks `taxCodes` to find a code whose `direction` matches the role.
2457
- */
2458
- function defaultResolveRoleCode(role, _tax, country) {
2459
- const direction = role === "collected" ? "collected" : role === "recoverable" ? "recoverable" : null;
2460
- if (!direction) return void 0;
2461
- for (const tc of Object.values(country.taxCodes)) if (tc.direction === direction) return tc.code;
2462
- }
2463
- /**
2464
- * Build a `TaxLineGenerator` that expands each hit `taxCode` into one
2465
- * journal item per repartition line. Taxes without `repartition` fall
2466
- * back to a single-line generator using the direction-implied account.
2467
- */
2468
- function createRepartitionTaxGenerator(options) {
2469
- const { country, resolveAccount, documentType = "invoice" } = options;
2470
- return { generateTaxLines(input) {
2471
- const code = input.taxCode;
2472
- if (!code) return [];
2473
- const tax = country.taxCodes[code];
2474
- if (!tax) return [];
2475
- const baseTax = Math.round(input.amount * tax.rate / 100);
2476
- if (baseTax === 0) return [];
2477
- const lines = tax.repartition && tax.repartition.length > 0 ? tax.repartition.filter((line) => !line.documentTypes || line.documentTypes.includes(documentType)) : [{
2478
- factor: 1,
2479
- accountRole: tax.direction === "recoverable" ? "recoverable" : "collected",
2480
- gridCode: tax.reportLines?.[0]
2481
- }];
2482
- const generated = [];
2483
- for (const rep of lines) {
2484
- const signed = Math.round(baseTax * rep.factor);
2485
- if (signed === 0) continue;
2486
- const account = resolveAccount(rep.accountRole, tax, input);
2487
- if (!account) throw new Error(`repartitionTax: cannot resolve account for role "${rep.accountRole}" on tax "${tax.code}"`);
2488
- const absAmount = Math.abs(signed);
2489
- let onCredit = rep.accountRole === "collected" || rep.accountRole === "transition";
2490
- if (signed < 0) onCredit = !onCredit;
2491
- generated.push({
2492
- account,
2493
- debit: onCredit ? 0 : absAmount,
2494
- credit: onCredit ? absAmount : 0,
2495
- label: rep.label ?? `${tax.name} ${rep.accountRole}`,
2496
- taxDetails: [{
2497
- taxCode: tax.code,
2498
- taxName: tax.name,
2499
- ...rep.gridCode != null ? { gridCode: String(rep.gridCode) } : {}
2500
- }]
2501
- });
2502
- }
2503
- return generated;
2504
- } };
2505
- }
2506
- /**
2507
- * Helper for packs that want the standard "role → account-type code"
2508
- * mapping without writing their own resolver. Returns the function you
2509
- * stuff into `CountryPackInput.resolveTaxRepartitionAccountCode`.
2510
- */
2511
- function defaultResolveTaxRepartitionAccountCode(country) {
2512
- return (role, tax) => defaultResolveRoleCode(role, tax, country);
2513
- }
2514
- //#endregion
2515
2539
  //#region src/utils/dimensions.ts
2516
2540
  /**
2517
2541
  * Analytic Dimensions — Helpers for defining analytic dimensions
@@ -2571,4 +2595,4 @@ function buildDimensionIndexes(dimensions, orgField) {
2571
2595
  });
2572
2596
  }
2573
2597
  //#endregion
2574
- export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, DEFAULT_BUCKETS, Errors, JOURNAL_CODES, JOURNAL_TYPES, Money, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createLockPlugin, createModels, createRepartitionTaxGenerator, createRepositories, creditLimitPlugin, dailyLockPlugin, defaultLogger, defaultResolveTaxRepartitionAccountCode, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, fxRealizationPlugin, generateAgedBalance, generateBalanceSheet, generateBudgetVsActual, generateCashFlow, generateDimensionBreakdown, generateGeneralLedger, generateIncomeStatement, generatePartnerLedger, generateRevaluation, generateTrialBalance, getCurrency, getCustomJournalTypes, getDateRange, getFiscalYearStart, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, periodResolver, quickbooksFieldMap, registerJournalType, reopenFiscalPeriod, resolveModelNames, splitTaxExclusive, splitTaxInclusive, subtract, taxLockPlugin, toDecimal, universalFieldMap, watermarkResolver };
2598
+ export { AccountingEngine, AccountingError, CATEGORIES, CATEGORY_KEYS, CURRENCIES, DEFAULT_BUCKETS, Errors, JOURNAL_CODES, JOURNAL_TYPES, Money, acquireSession, add, allocate, buildAccountTypeMap, buildDimensionFields, buildDimensionIndexes, buildItemFilters, buildRevaluationEntry, calculateTotal, closeFiscalPeriod, computeEndingBalance, computeRevaluation, createAccountingEngine, createLockPlugin, createModels, createRepositories, creditLimitPlugin, dailyLockPlugin, defaultLogger, defineCountryPack, doubleEntryPlugin, exportToCsv, finalizeSession, fiscalLockPlugin, flattenJournalEntries, format, formatPlain, fromDecimal, fxRealizationPlugin, generateAgedBalance, generateBalanceSheet, generateBudgetVsActual, generateCashFlow, generateDimensionBreakdown, generateGeneralLedger, generateIncomeStatement, generatePartnerLedger, generateRevaluation, generateTrialBalance, getCurrency, getCustomJournalTypes, getDateRange, getFiscalYearStart, getJournalType, getJournalTypeCodes, getMinorUnit, getNormalBalance, idempotencyPlugin, isBalanceSheet, isIncomeStatement, isValidCategory, isValidCurrency, isValidJournalType, isVirtualTaxAccount, multiply, parseCents, percentage, periodResolver, quickbooksFieldMap, registerJournalType, reopenFiscalPeriod, resolveModelNames, splitTaxExclusive, splitTaxInclusive, subtract, toDecimal, universalFieldMap, watermarkResolver };
@@ -1,4 +1,4 @@
1
- import { d as JournalType, f as MainType, h as StatementType, i as CategoryKey, o as Currency, r as Category } from "./core-BkGjuVZj.mjs";
1
+ import { d as JournalType, f as MainType, h as StatementType, i as CategoryKey, o as Currency, r as Category } from "./core-MpgjCqK0.mjs";
2
2
 
3
3
  //#region src/constants/categories.d.ts
4
4
  /** All valid categories */
@@ -0,0 +1,60 @@
1
+ //#region src/sync/builders/opening-balance.ts
2
+ function buildOpeningBalanceEntry(input) {
3
+ const { cutoverDate, balances, equityAccountCode } = input;
4
+ const dateStr = cutoverDate.toISOString().split("T")[0];
5
+ const label = input.label ?? `Opening Balance — Cutover ${dateStr}`;
6
+ const items = [];
7
+ let totalDebit = 0;
8
+ let totalCredit = 0;
9
+ for (const { accountCode, balance } of balances) {
10
+ if (balance === 0) continue;
11
+ if (balance > 0) {
12
+ items.push({
13
+ account: accountCode,
14
+ debit: balance,
15
+ credit: 0,
16
+ label: "Opening balance"
17
+ });
18
+ totalDebit += balance;
19
+ } else {
20
+ const absBalance = Math.abs(balance);
21
+ items.push({
22
+ account: accountCode,
23
+ debit: 0,
24
+ credit: absBalance,
25
+ label: "Opening balance"
26
+ });
27
+ totalCredit += absBalance;
28
+ }
29
+ }
30
+ const residual = totalDebit - totalCredit;
31
+ const lineCount = items.length;
32
+ if (residual > 0) items.push({
33
+ account: equityAccountCode,
34
+ debit: 0,
35
+ credit: residual,
36
+ label: "Opening balance equity (contra)"
37
+ });
38
+ else if (residual < 0) items.push({
39
+ account: equityAccountCode,
40
+ debit: Math.abs(residual),
41
+ credit: 0,
42
+ label: "Opening balance equity (contra)"
43
+ });
44
+ return {
45
+ entry: {
46
+ date: cutoverDate,
47
+ label,
48
+ journalType: "GENERAL",
49
+ journalItems: items,
50
+ extra: {
51
+ _externalId: `opening-balance:${dateStr}`,
52
+ _importSource: "opening-balance"
53
+ }
54
+ },
55
+ residual,
56
+ lineCount
57
+ };
58
+ }
59
+ //#endregion
60
+ export { buildOpeningBalanceEntry as t };
@@ -1,16 +1,2 @@
1
- import { C as DoubleEntryPluginOptions, E as creditLimitPlugin, S as fxRealizationPlugin, T as CreditLimitPluginOptions, _ as LockResolver, a as DailyLockPluginOptions, b as idempotencyPlugin, c as dailyLockPlugin, d as PeriodResolverOptions, f as periodResolver, g as LockHit, h as LockAccountSelector, i as watermarkResolver, l as fiscalLockPlugin, m as CreateLockPluginOptions, o as FiscalLockPluginOptions, p as createLockPlugin, r as WatermarkResolverOptions, s as TaxLockPluginOptions, t as TaxLineGenerator, u as taxLockPlugin, v as LockResolverContext, w as doubleEntryPlugin, x as FxRealizationPluginOptions, y as IdempotencyPluginOptions } from "../tax-hooks-BnVenul5.mjs";
2
- import { RepositoryInstance } from "@classytic/mongokit";
3
-
4
- //#region src/plugins/tax-hook.plugin.d.ts
5
- interface TaxHookPluginOptions {
6
- /** Tax line generator — implements the tax calculation logic */
7
- generator: TaxLineGenerator;
8
- /** Only apply tax hooks on posted entries (default: true) */
9
- onlyOnPost?: boolean;
10
- }
11
- declare function taxHookPlugin(options: TaxHookPluginOptions): {
12
- name: string;
13
- apply(repo: RepositoryInstance): void;
14
- };
15
- //#endregion
16
- export { type CreateLockPluginOptions, type CreditLimitPluginOptions, type DailyLockPluginOptions, type DoubleEntryPluginOptions, type FiscalLockPluginOptions, type FxRealizationPluginOptions, type IdempotencyPluginOptions, type LockAccountSelector, type LockHit, type LockResolver, type LockResolverContext, type PeriodResolverOptions, type TaxHookPluginOptions, type TaxLockPluginOptions, type WatermarkResolverOptions, createLockPlugin, creditLimitPlugin, dailyLockPlugin, doubleEntryPlugin, fiscalLockPlugin, fxRealizationPlugin, idempotencyPlugin, periodResolver, taxHookPlugin, taxLockPlugin, watermarkResolver };
1
+ import { S as creditLimitPlugin, _ as FxRealizationPluginOptions, a as dailyLockPlugin, b as doubleEntryPlugin, c as periodResolver, d as LockAccountSelector, f as LockHit, g as idempotencyPlugin, h as IdempotencyPluginOptions, i as FiscalLockPluginOptions, l as createLockPlugin, m as LockResolverContext, n as watermarkResolver, o as fiscalLockPlugin, p as LockResolver, r as DailyLockPluginOptions, s as PeriodResolverOptions, t as WatermarkResolverOptions, u as CreateLockPluginOptions, v as fxRealizationPlugin, x as CreditLimitPluginOptions, y as DoubleEntryPluginOptions } from "../index-BSsvrf3m.mjs";
2
+ export { type CreateLockPluginOptions, type CreditLimitPluginOptions, type DailyLockPluginOptions, type DoubleEntryPluginOptions, type FiscalLockPluginOptions, type FxRealizationPluginOptions, type IdempotencyPluginOptions, type LockAccountSelector, type LockHit, type LockResolver, type LockResolverContext, type PeriodResolverOptions, type WatermarkResolverOptions, createLockPlugin, creditLimitPlugin, dailyLockPlugin, doubleEntryPlugin, fiscalLockPlugin, fxRealizationPlugin, idempotencyPlugin, periodResolver, watermarkResolver };
@@ -1,57 +1,2 @@
1
- import { a as taxLockPlugin, c as createLockPlugin, i as fiscalLockPlugin, l as idempotencyPlugin, n as creditLimitPlugin, o as watermarkResolver, r as dailyLockPlugin, s as periodResolver, t as fxRealizationPlugin, u as doubleEntryPlugin } from "../fx-realization.plugin-CgQFDGv2.mjs";
2
- //#region src/utils/tax-hooks.ts
3
- /**
4
- * Apply a tax hook to journal items.
5
- *
6
- * Iterates each item that has a taxCode in taxDetails, calls
7
- * `generator.generateTaxLines` for each, and appends the generated
8
- * tax lines as new journal items.
9
- *
10
- * @returns The original items + generated tax items
11
- */
12
- function applyTaxHook(items, generator) {
13
- const taxLines = [];
14
- for (const item of items) {
15
- const taxDetails = item.taxDetails;
16
- if (!taxDetails || taxDetails.length === 0) continue;
17
- const taxCode = taxDetails.find((td) => td.taxCode != null)?.taxCode;
18
- if (!taxCode) continue;
19
- const side = item.debit > 0 ? "debit" : "credit";
20
- const amount = item.debit > 0 ? item.debit : item.credit;
21
- const input = {
22
- account: item.account,
23
- amount,
24
- side,
25
- taxCode
26
- };
27
- const generated = generator.generateTaxLines(input);
28
- for (const line of generated) taxLines.push({
29
- account: line.account,
30
- debit: line.debit,
31
- credit: line.credit,
32
- label: line.label,
33
- taxDetails: line.taxDetails
34
- });
35
- }
36
- return [...items, ...taxLines];
37
- }
38
- //#endregion
39
- //#region src/plugins/tax-hook.plugin.ts
40
- function taxHookPlugin(options) {
41
- const { generator, onlyOnPost = true } = options;
42
- return {
43
- name: "accounting:tax-hook",
44
- apply(repo) {
45
- repo.on("before:create", (context) => {
46
- const data = context.data;
47
- if (!data) return;
48
- if (onlyOnPost && data.state !== "posted") return;
49
- const items = data.journalItems;
50
- if (!items || items.length === 0) return;
51
- data.journalItems = applyTaxHook(items, generator);
52
- });
53
- }
54
- };
55
- }
56
- //#endregion
57
- export { createLockPlugin, creditLimitPlugin, dailyLockPlugin, doubleEntryPlugin, fiscalLockPlugin, fxRealizationPlugin, idempotencyPlugin, periodResolver, taxHookPlugin, taxLockPlugin, watermarkResolver };
1
+ import { a as watermarkResolver, c as idempotencyPlugin, i as fiscalLockPlugin, l as doubleEntryPlugin, n as creditLimitPlugin, o as periodResolver, r as dailyLockPlugin, s as createLockPlugin, t as fxRealizationPlugin } from "../fx-realization.plugin-DDVK-oYO.mjs";
2
+ export { createLockPlugin, creditLimitPlugin, dailyLockPlugin, doubleEntryPlugin, fiscalLockPlugin, fxRealizationPlugin, idempotencyPlugin, periodResolver, watermarkResolver };
@@ -1,2 +1,2 @@
1
- import { A as generateDimensionBreakdown, C as FiscalReopenResult, D as DimensionBreakdownParams, E as DimensionBreakdownOptions, F as BudgetVsActualReport, I as BudgetVsActualRow, L as generateBudgetVsActual, M as generateCashFlow, N as BudgetVsActualOptions, O as DimensionBreakdownReport, P as BudgetVsActualParams, R as BalanceSheetOptions, S as FiscalCloseResult, T as reopenFiscalPeriod, _ as IncomeStatementOptions, a as RevaluationReport, at as DEFAULT_BUCKETS, b as generateGeneralLedger, et as AgedBalanceOptions, f as PartnerLedgerLine, g as generatePartnerLedger, h as PartnerLedgerReport, i as RevaluationParams, it as AgedBucketConfig, j as CashFlowOptions, k as DimensionBreakdownRow, m as PartnerLedgerParams, n as generateTrialBalance, nt as AgedBalanceReport, o as generateRevaluation, ot as generateAgedBalance, p as PartnerLedgerOptions, r as RevaluationOptions, rt as AgedBalanceRow, t as TrialBalanceOptions, tt as AgedBalanceParams, v as generateIncomeStatement, w as closeFiscalPeriod, x as FiscalCloseOptions, y as GeneralLedgerOptions, z as generateBalanceSheet } from "../trial-balance-s92GEvRR.mjs";
1
+ import { $ as AgedBalanceParams, A as generateDimensionBreakdown, C as FiscalReopenResult, D as DimensionBreakdownParams, E as DimensionBreakdownOptions, F as BudgetVsActualReport, I as BudgetVsActualRow, L as generateBudgetVsActual, M as generateCashFlow, N as BudgetVsActualOptions, O as DimensionBreakdownReport, P as BudgetVsActualParams, Q as AgedBalanceOptions, R as BalanceSheetOptions, S as FiscalCloseResult, T as reopenFiscalPeriod, _ as IncomeStatementOptions, a as RevaluationReport, b as generateGeneralLedger, et as AgedBalanceReport, f as PartnerLedgerLine, g as generatePartnerLedger, h as PartnerLedgerReport, i as RevaluationParams, it as generateAgedBalance, j as CashFlowOptions, k as DimensionBreakdownRow, m as PartnerLedgerParams, n as generateTrialBalance, nt as AgedBucketConfig, o as generateRevaluation, p as PartnerLedgerOptions, r as RevaluationOptions, rt as DEFAULT_BUCKETS, t as TrialBalanceOptions, tt as AgedBalanceRow, v as generateIncomeStatement, w as closeFiscalPeriod, x as FiscalCloseOptions, y as GeneralLedgerOptions, z as generateBalanceSheet } from "../trial-balance-DTj-c21f.mjs";
2
2
  export { type AgedBalanceOptions, type AgedBalanceParams, type AgedBalanceReport, type AgedBalanceRow, type AgedBucketConfig, type BalanceSheetOptions, type BudgetVsActualOptions, type BudgetVsActualParams, type BudgetVsActualReport, type BudgetVsActualRow, type CashFlowOptions, DEFAULT_BUCKETS, type DimensionBreakdownOptions, type DimensionBreakdownParams, type DimensionBreakdownReport, type DimensionBreakdownRow, type FiscalCloseOptions, type FiscalCloseResult, type FiscalReopenResult, type GeneralLedgerOptions, type IncomeStatementOptions, type PartnerLedgerLine, type PartnerLedgerOptions, type PartnerLedgerParams, type PartnerLedgerReport, type RevaluationOptions, type RevaluationParams, type RevaluationReport, type TrialBalanceOptions, closeFiscalPeriod, generateAgedBalance, generateBalanceSheet, generateBudgetVsActual, generateCashFlow, generateDimensionBreakdown, generateGeneralLedger, generateIncomeStatement, generatePartnerLedger, generateRevaluation, generateTrialBalance, reopenFiscalPeriod };