@classytic/ledger 0.1.3 → 0.1.5

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.
@@ -402,8 +402,12 @@ async function generateBalanceSheet(opts, params) {
402
402
  groupsMap.Equity[reGroup.name].accounts.push(...reGroup.accounts);
403
403
  groupsMap.Equity[reGroup.name].total += reGroup.total;
404
404
  }
405
- assets.groups = Object.values(groupsMap.Asset);
406
- liabilities.groups = Object.values(groupsMap.Liability);
405
+ const pruneGroups = (groups) => Object.values(groups).map((g) => ({
406
+ ...g,
407
+ accounts: g.accounts.filter((a) => a.balance !== 0 || a.isTotal || a.isCalculated)
408
+ })).filter((g) => g.accounts.length > 0 || g.total !== 0);
409
+ assets.groups = pruneGroups(groupsMap.Asset);
410
+ liabilities.groups = pruneGroups(groupsMap.Liability);
407
411
  equity.groups = Object.values(groupsMap.Equity);
408
412
  assets.total = assets.groups.reduce((s, g) => s + g.total, 0);
409
413
  liabilities.total = liabilities.groups.reduce((s, g) => s + g.total, 0);
@@ -472,6 +476,14 @@ async function generateIncomeStatement(opts, params) {
472
476
  const accountMap = new Map(allAccounts.map((a) => [String(a._id), a]));
473
477
  const revenueGroups = {};
474
478
  const expenseGroups = {};
479
+ const resolveGroupName = (at) => {
480
+ let current = at.parentCode ? country.getAccountType(at.parentCode) : void 0;
481
+ while (current) {
482
+ if (current.isGroup) return current.name;
483
+ current = current.parentCode ? country.getAccountType(current.parentCode) : void 0;
484
+ }
485
+ return at.name;
486
+ };
475
487
  for (const r of results) {
476
488
  const acc = accountMap.get(String(r._id));
477
489
  if (!acc) continue;
@@ -480,7 +492,7 @@ async function generateIncomeStatement(opts, params) {
480
492
  const mainType = extractMainType(at.category);
481
493
  const netAmount = mainType === "Income" ? r.c - r.d : r.d - r.c;
482
494
  if (netAmount === 0) continue;
483
- const groupName = (at.parentCode ? country.getAccountType(at.parentCode) : void 0)?.name ?? at.name;
495
+ const groupName = resolveGroupName(at);
484
496
  const groups = mainType === "Income" ? revenueGroups : expenseGroups;
485
497
  if (!(groupName in groups)) groups[groupName] = {
486
498
  name: groupName,
@@ -931,4 +943,4 @@ async function reopenFiscalPeriod(opts, params) {
931
943
 
932
944
  //#endregion
933
945
  export { generateIncomeStatement as a, calculateTotal as c, generateTrialBalance as d, buildItemFilters as f, generateGeneralLedger as i, computeEndingBalance as l, getFiscalYearStart as m, reopenFiscalPeriod as n, generateBalanceSheet as o, getDateRange as p, generateCashFlow as r, buildAccountTypeMap as s, closeFiscalPeriod as t, isVirtualTaxAccount as u };
934
- //# sourceMappingURL=fiscal-close-CNOwv_ud.mjs.map
946
+ //# sourceMappingURL=fiscal-close-DuXDgVvb.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fiscal-close-DuXDgVvb.mjs","names":[],"sources":["../src/utils/date-range.ts","../src/utils/filter-builder.ts","../src/reports/trial-balance.ts","../src/utils/account-helpers.ts","../src/reports/balance-sheet.ts","../src/reports/income-statement.ts","../src/reports/general-ledger.ts","../src/reports/cash-flow.ts","../src/reports/fiscal-close.ts"],"sourcesContent":["/**\r\n * Date Range Utility — Compute period boundaries for reports.\r\n */\r\n\r\nimport type { DateOption, DateRange, QuarterValue, CustomDateRange } from '../types/core.js';\r\n\r\n/**\r\n * Compute start/end dates from a date option + value.\r\n *\r\n * Examples:\r\n * getDateRange('month', '2025-03') → Mar 1 – Mar 31\r\n * getDateRange('quarter', { quarter: 2, year: 2025 }) → Apr 1 – Jun 30\r\n * getDateRange('year', 2025) → Jan 1 – Dec 31\r\n * getDateRange('custom', { startDate, endDate })\r\n */\r\nexport function getDateRange(option: DateOption, value: unknown): DateRange {\r\n switch (option) {\r\n case 'month': {\r\n // Parse 'YYYY-MM' strings explicitly to avoid UTC-vs-local timezone shift\r\n let year: number;\r\n let month: number;\r\n const strVal = String(value);\r\n const match = strVal.match(/^(\\d{4})-(\\d{1,2})$/);\r\n if (match) {\r\n year = parseInt(match[1], 10);\r\n month = parseInt(match[2], 10) - 1; // 0-indexed\r\n } else {\r\n const date = new Date(value as string | number | Date);\r\n year = date.getFullYear();\r\n month = date.getMonth();\r\n }\r\n const startDate = new Date(year, month, 1);\r\n const endDate = new Date(year, month + 1, 0, 23, 59, 59, 999);\r\n return { startDate, endDate };\r\n }\r\n\r\n case 'quarter': {\r\n const { quarter, year } = value as QuarterValue;\r\n const startMonth = (quarter - 1) * 3;\r\n const startDate = new Date(year, startMonth, 1);\r\n const endDate = new Date(year, startMonth + 3, 0, 23, 59, 59, 999);\r\n return { startDate, endDate };\r\n }\r\n\r\n case 'year': {\r\n const year = typeof value === 'number' ? value : parseInt(String(value), 10);\r\n const startDate = new Date(year, 0, 1);\r\n const endDate = new Date(year, 11, 31, 23, 59, 59, 999);\r\n return { startDate, endDate };\r\n }\r\n\r\n case 'custom': {\r\n const { startDate, endDate } = value as CustomDateRange;\r\n const end = new Date(endDate);\r\n // Normalize end date to end-of-day if time is midnight (00:00:00)\r\n if (end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0 && end.getMilliseconds() === 0) {\r\n end.setHours(23, 59, 59, 999);\r\n }\r\n return {\r\n startDate: new Date(startDate),\r\n endDate: end,\r\n };\r\n }\r\n\r\n default: {\r\n // Default: current month\r\n const now = new Date();\r\n return {\r\n startDate: new Date(now.getFullYear(), now.getMonth(), 1),\r\n endDate: new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999),\r\n };\r\n }\r\n }\r\n}\r\n\r\n/** Get fiscal year start date for a given date and fiscal start month */\r\nexport function getFiscalYearStart(date: Date, fiscalStartMonth = 1): Date {\r\n const month = fiscalStartMonth - 1; // 0-indexed\r\n const year = date.getMonth() < month ? date.getFullYear() - 1 : date.getFullYear();\r\n return new Date(year, month, 1);\r\n}\r\n","/**\r\n * Filter Builder — Sanitizes user-supplied dimension filters for aggregation pipelines.\r\n *\r\n * Prevents injection of dangerous MongoDB operators while allowing\r\n * standard equality and comparison filters on custom dimension fields.\r\n */\r\n\r\nconst BLOCKED_OPERATORS = new Set([\r\n '$where', '$expr', '$function', '$accumulator',\r\n '$merge', '$out', '$unionWith',\r\n]);\r\n\r\n/**\r\n * Build a sanitized filter object from user-supplied dimension filters.\r\n * Blocks dangerous operators ($where, $expr, $function, etc.).\r\n *\r\n * @param filters - Key-value filters (e.g. { 'journalItems.departmentId': 'dept-1' })\r\n * @returns Sanitized filter object safe for $match stages\r\n * @throws Error if a blocked operator is used\r\n */\r\nexport function buildItemFilters(filters?: Record<string, unknown>): Record<string, unknown> {\r\n if (!filters || Object.keys(filters).length === 0) return {};\r\n\r\n const result: Record<string, unknown> = {};\r\n\r\n for (const [key, value] of Object.entries(filters)) {\r\n // Block operators at top level\r\n if (key.startsWith('$')) {\r\n throw new Error(`Filter key \"${key}\" is not allowed. Use field names, not operators.`);\r\n }\r\n\r\n // Check nested values for blocked operators\r\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\r\n for (const opKey of Object.keys(value as Record<string, unknown>)) {\r\n if (BLOCKED_OPERATORS.has(opKey)) {\r\n throw new Error(`Filter operator \"${opKey}\" is not allowed.`);\r\n }\r\n }\r\n }\r\n\r\n result[key] = value;\r\n }\r\n\r\n return result;\r\n}\r\n","/**\r\n * Trial Balance Report\r\n *\r\n * Three-column trial balance: Initial + Current Period + Ending Balance.\r\n * Pure aggregation pipeline — no cached balances.\r\n */\r\n\r\nimport type { Model, PipelineStage } from 'mongoose';\r\nimport type { AccountType, CategoryKey } from '../types/core.js';\r\nimport type { TrialBalanceRow, TrialBalanceReport } from '../types/report.js';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport { getDateRange, getFiscalYearStart } from '../utils/date-range.js';\r\nimport { computeEndingBalance } from '../utils/account-helpers.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface TrialBalanceOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n fiscalYearStartMonth?: number;\r\n}\r\n\r\nexport async function generateTrialBalance(\r\n opts: TrialBalanceOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n accountId?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<TrialBalanceReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1 } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const fiscalYearStart = getFiscalYearStart(startDate, fiscalYearStartMonth);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch all active accounts\r\n const accountQuery: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;\r\n\r\n const allAccounts = await AccountModel.find(accountQuery).lean() as Array<Record<string, unknown>>;\r\n\r\n // Split by statement type\r\n const bsIds: unknown[] = [];\r\n const isIds: unknown[] = [];\r\n\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at || at.isGroup) continue;\r\n\r\n if (at.category.startsWith('Balance Sheet')) bsIds.push(acc._id);\r\n else if (at.category.startsWith('Income Statement')) isIds.push(acc._id);\r\n }\r\n\r\n const baseMatch: Record<string, unknown> = { state: 'posted' };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n const accountFilter = params.accountId ? { 'journalItems.account': params.accountId } : {};\r\n\r\n // Build pipelines\r\n const buildPipeline = (ids: unknown[], dateFrom: Date, dateTo: Date): PipelineStage[] => [\r\n { $match: { ...baseMatch, date: { $gte: dateFrom, $lt: dateTo } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: ids }, ...accountFilter, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ];\r\n\r\n // BS initial: all history before startDate\r\n // IS initial: fiscal year start → startDate\r\n // Current: startDate → endDate\r\n const [bsInitial, isInitial, current] = await Promise.all([\r\n bsIds.length ? JournalEntryModel.aggregate(buildPipeline(bsIds, new Date(0), startDate)) : [],\r\n isIds.length ? JournalEntryModel.aggregate(buildPipeline(isIds, fiscalYearStart, startDate)) : [],\r\n JournalEntryModel.aggregate(buildPipeline([...bsIds, ...isIds], startDate, new Date(endDate.getTime() + 1))),\r\n ]);\r\n\r\n // Merge\r\n const map = new Map<string, { iD: number; iC: number; cD: number; cC: number }>();\r\n\r\n for (const r of [...bsInitial, ...isInitial]) {\r\n const key = String(r._id);\r\n map.set(key, { iD: r.d, iC: r.c, cD: 0, cC: 0 });\r\n }\r\n for (const r of current) {\r\n const key = String(r._id);\r\n const existing = map.get(key) ?? { iD: 0, iC: 0, cD: 0, cC: 0 };\r\n existing.cD = r.d;\r\n existing.cC = r.c;\r\n map.set(key, existing);\r\n }\r\n\r\n // Build rows\r\n const accountLookup = new Map(allAccounts.map(a => [String(a._id), a]));\r\n\r\n const rows: TrialBalanceRow[] = [];\r\n for (const [id, bal] of map) {\r\n const acc = accountLookup.get(id);\r\n const totalD = bal.iD + bal.cD;\r\n const totalC = bal.iC + bal.cC;\r\n const net = totalD - totalC;\r\n\r\n rows.push({\r\n account: acc ?? id,\r\n initial: { debit: bal.iD, credit: bal.iC },\r\n current: { debit: bal.cD, credit: bal.cC },\r\n ending: net >= 0 ? { debit: net, credit: 0 } : { debit: 0, credit: Math.abs(net) },\r\n });\r\n }\r\n\r\n return { rows, period: { startDate, endDate } };\r\n}\r\n","/**\r\n * Account Helper Utilities\r\n */\r\n\r\nimport type { AccountType, TotalAccountOp, CategoryKey } from '../types/core.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\n\r\n/**\r\n * Check if an account type is a virtual tax sub-account.\r\n * Returns true if the account's parent has `isVirtualTotal: true`.\r\n * Works for any country pack — no code format assumptions.\r\n */\r\nexport function isVirtualTaxAccount(accountType: AccountType, accountMap: Map<string, AccountType>): boolean {\r\n if (!accountType.parentCode) return false;\r\n const parent = accountMap.get(accountType.parentCode);\r\n return parent?.isVirtualTotal === true;\r\n}\r\n\r\n/** Check if an account type is a balance sheet account */\r\nexport function isBalanceSheetAccountType(accountType: AccountType): boolean {\r\n const { category } = accountType;\r\n return category.endsWith('-Asset') || category.endsWith('-Liability') || category.endsWith('-Equity');\r\n}\r\n\r\n/** Check if an account type is an income statement account */\r\nexport function isIncomeStatementAccountType(accountType: AccountType): boolean {\r\n const { category } = accountType;\r\n return category.endsWith('-Income') || category.endsWith('-Expense');\r\n}\r\n\r\n/**\r\n * Calculate a total from sub-accounts using the totalAccountTypes formula.\r\n * @param formula - Array of { account, operation } instructions\r\n * @param balanceMap - Map of account code → balance\r\n */\r\nexport function calculateTotal(\r\n formula: readonly TotalAccountOp[],\r\n balanceMap: Map<string, number>,\r\n): number {\r\n let total = 0;\r\n for (const item of formula) {\r\n const balance = balanceMap.get(item.account) ?? 0;\r\n total += item.operation === '+' ? balance : -balance;\r\n }\r\n return total;\r\n}\r\n\r\n/**\r\n * Compute the ending balance for an account given its debits and credits.\r\n * Uses the account's main type to determine normal balance direction.\r\n *\r\n * Assets & Expenses: debit - credit\r\n * Liabilities, Equity & Income: credit - debit\r\n */\r\nexport function computeEndingBalance(\r\n category: CategoryKey,\r\n totalDebit: number,\r\n totalCredit: number,\r\n): number {\r\n const mainType = extractMainType(category);\r\n if (mainType === 'Asset' || mainType === 'Expense') {\r\n return totalDebit - totalCredit;\r\n }\r\n return totalCredit - totalDebit;\r\n}\r\n\r\n/**\r\n * Build a lookup map from an array of account types.\r\n */\r\nexport function buildAccountTypeMap(accountTypes: readonly AccountType[]): Map<string, AccountType> {\r\n const map = new Map<string, AccountType>();\r\n for (const at of accountTypes) {\r\n map.set(at.code, at);\r\n }\r\n return map;\r\n}\r\n","/**\r\n * Balance Sheet Report\r\n *\r\n * Assets = Liabilities + Equity\r\n * Net income injected into retained earnings for the current fiscal year.\r\n */\r\n\r\nimport type { Model, PipelineStage } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { BalanceSheetReport, ReportCategory, ReportGroup, ReportAccount } from '../types/report.js';\r\nimport { getDateRange, getFiscalYearStart } from '../utils/date-range.js';\r\nimport { computeEndingBalance, calculateTotal, isVirtualTaxAccount, buildAccountTypeMap } from '../utils/account-helpers.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\nimport type { CategoryKey } from '../types/core.js';\r\n\r\nexport interface BalanceSheetOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n fiscalYearStartMonth?: number;\r\n /** Display code for prior retained earnings (default: '3660') */\r\n retainedEarningsCode?: string;\r\n /** Display code for current year net income (default: '3680') */\r\n currentYearEarningsCode?: string;\r\n}\r\n\r\nexport async function generateBalanceSheet(\r\n opts: BalanceSheetOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n businessName?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<BalanceSheetReport> {\r\n const {\r\n AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1,\r\n retainedEarningsCode = country.retainedEarningsCode ?? '3660',\r\n currentYearEarningsCode = country.currentYearEarningsCode ?? '3680',\r\n } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const fiscalYearStart = getFiscalYearStart(endDate, fiscalYearStartMonth);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch accounts\r\n const q: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) q[orgField] = params.organizationId;\r\n const allAccounts = await AccountModel.find(q).lean() as Array<Record<string, unknown>>;\r\n\r\n // Balance sheet account IDs\r\n const bsIds = allAccounts\r\n .filter(a => {\r\n const at = country.getAccountType(a.accountTypeCode as string);\r\n return at && !at.isGroup && at.category.startsWith('Balance Sheet');\r\n })\r\n .map(a => a._id);\r\n\r\n // Income statement account IDs (for net income calculation)\r\n const isIds = allAccounts\r\n .filter(a => {\r\n const at = country.getAccountType(a.accountTypeCode as string);\r\n return at && !at.isGroup && !at.isTotal && at.category.startsWith('Income Statement');\r\n })\r\n .map(a => a._id);\r\n\r\n const baseMatch: Record<string, unknown> = { state: 'posted' };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n // Run pipelines in parallel\r\n const [bsResults, netIncomeResults, priorRetainedResults] = await Promise.all([\r\n // Balance sheet balances (all time up to endDate)\r\n JournalEntryModel.aggregate([\r\n { $match: { ...baseMatch, date: { $lte: endDate } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: bsIds }, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Promise<Array<{ _id: unknown; d: number; c: number }>>,\r\n\r\n // Net income (fiscal year start → endDate)\r\n JournalEntryModel.aggregate([\r\n { $match: { ...baseMatch, date: { $gte: fiscalYearStart, $lte: endDate } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds }, ...itemFilters } },\r\n { $group: { _id: null, d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Promise<Array<{ _id: unknown; d: number; c: number }>>,\r\n\r\n // Prior retained earnings (all income statement before fiscal year)\r\n JournalEntryModel.aggregate([\r\n { $match: { ...baseMatch, date: { $lt: fiscalYearStart } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds }, ...itemFilters } },\r\n { $group: { _id: null, d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Promise<Array<{ _id: unknown; d: number; c: number }>>,\r\n ]);\r\n\r\n const netIncome = netIncomeResults.length > 0 ? netIncomeResults[0].c - netIncomeResults[0].d : 0;\r\n const priorRetained = priorRetainedResults.length > 0 ? priorRetainedResults[0].c - priorRetainedResults[0].d : 0;\r\n\r\n // Build categories\r\n const accountMap = new Map(allAccounts.map(a => [String(a._id), a]));\r\n const accountTypeMap = buildAccountTypeMap(country.accountTypes);\r\n const balanceMap = new Map<string, number>();\r\n\r\n const labels = country.reportLabels ?? {};\r\n const assets: ReportCategory = { name: labels.assets ?? 'Assets', total: 0, groups: [] };\r\n const liabilities: ReportCategory = { name: labels.liabilities ?? 'Liabilities', total: 0, groups: [] };\r\n const equity: ReportCategory = { name: labels.equity ?? 'Equity', total: 0, groups: [] };\r\n\r\n const groupsMap: Record<string, Record<string, ReportGroup>> = {\r\n Asset: {}, Liability: {}, Equity: {},\r\n };\r\n\r\n for (const r of bsResults) {\r\n const acc = accountMap.get(String(r._id));\r\n if (!acc) continue;\r\n\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at) continue;\r\n\r\n const mainType = extractMainType(at.category) ?? 'Asset';\r\n const balance = computeEndingBalance(at.category as CategoryKey, r.d, r.c);\r\n balanceMap.set(at.code, balance);\r\n\r\n const parentAt = at.parentCode ? country.getAccountType(at.parentCode) : undefined;\r\n const groupName = parentAt?.name ?? at.name;\r\n\r\n if (!(groupName in groupsMap[mainType])) {\r\n groupsMap[mainType][groupName] = { name: groupName, total: 0, accounts: [] };\r\n }\r\n\r\n const group = groupsMap[mainType][groupName];\r\n\r\n // Skip virtual tax sub-accounts from display but include in calculation\r\n if (!isVirtualTaxAccount(at, accountTypeMap)) {\r\n group.accounts.push({\r\n id: acc._id,\r\n name: (acc.name as string) ?? at.name,\r\n code: (acc.accountNumber as string) ?? at.code,\r\n balance,\r\n isTotal: at.isTotal,\r\n isVirtualTotal: at.isVirtualTotal,\r\n });\r\n }\r\n\r\n if (!at.isTotal) {\r\n group.total += balance;\r\n }\r\n }\r\n\r\n // Add retained earnings to equity\r\n const reGroup: ReportGroup = {\r\n name: 'Retained Earnings',\r\n total: priorRetained + netIncome,\r\n accounts: [\r\n { id: 'prior-retained', name: 'Previous Years Retained Earnings', code: retainedEarningsCode, balance: priorRetained },\r\n { id: 'current-year', name: `Current Year Net Income (${endDate.getFullYear()})`, code: currentYearEarningsCode, balance: netIncome, isCalculated: true },\r\n ],\r\n };\r\n\r\n if (!(reGroup.name in groupsMap.Equity)) {\r\n groupsMap.Equity[reGroup.name] = reGroup;\r\n } else {\r\n groupsMap.Equity[reGroup.name].accounts.push(...reGroup.accounts);\r\n groupsMap.Equity[reGroup.name].total += reGroup.total;\r\n }\r\n\r\n // Convert groups maps to arrays, filtering out zero-balance accounts and empty groups\r\n const pruneGroups = (groups: Record<string, ReportGroup>) =>\r\n Object.values(groups)\r\n .map(g => ({\r\n ...g,\r\n accounts: g.accounts.filter(a => a.balance !== 0 || a.isTotal || a.isCalculated),\r\n }))\r\n .filter(g => g.accounts.length > 0 || g.total !== 0);\r\n\r\n assets.groups = pruneGroups(groupsMap.Asset);\r\n liabilities.groups = pruneGroups(groupsMap.Liability);\r\n equity.groups = Object.values(groupsMap.Equity); // Keep equity as-is (retained earnings always shown)\r\n\r\n // Sum totals\r\n assets.total = assets.groups.reduce((s, g) => s + g.total, 0);\r\n liabilities.total = liabilities.groups.reduce((s, g) => s + g.total, 0);\r\n equity.total = equity.groups.reduce((s, g) => s + g.total, 0);\r\n\r\n const liabilitiesAndEquity = liabilities.total + equity.total;\r\n\r\n return {\r\n metadata: {\r\n businessName: params.businessName,\r\n generatedAt: new Date().toISOString(),\r\n asOfDate: endDate.toISOString().split('T')[0],\r\n displayDate: `As of ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`,\r\n },\r\n assets,\r\n liabilities,\r\n equity,\r\n summary: {\r\n totalAssets: assets.total,\r\n totalLiabilities: liabilities.total,\r\n totalEquity: equity.total,\r\n liabilitiesAndEquity,\r\n difference: assets.total - liabilitiesAndEquity,\r\n isBalanced: assets.total === liabilitiesAndEquity,\r\n },\r\n };\r\n}\r\n","/**\r\n * Income Statement (Profit & Loss) Report\r\n *\r\n * Revenue - COGS = Gross Profit\r\n * Gross Profit - Operating Expenses = Operating Income\r\n * Operating Income ± Other = Net Income\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { IncomeStatementReport, ReportCategory, ReportGroup } from '../types/report.js';\r\nimport { getDateRange } from '../utils/date-range.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface IncomeStatementOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n}\r\n\r\nexport async function generateIncomeStatement(\r\n opts: IncomeStatementOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n businessName?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<IncomeStatementReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch accounts\r\n const q: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) q[orgField] = params.organizationId;\r\n const allAccounts = await AccountModel.find(q).lean() as Array<Record<string, unknown>>;\r\n\r\n // Income statement posting accounts only\r\n const isAccounts = allAccounts.filter(a => {\r\n const at = country.getAccountType(a.accountTypeCode as string);\r\n return at && !at.isGroup && !at.isTotal && at.category.startsWith('Income Statement');\r\n });\r\n const isIds = isAccounts.map(a => a._id);\r\n\r\n const baseMatch: Record<string, unknown> = {\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n const results = await JournalEntryModel.aggregate([\r\n { $match: baseMatch },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds }, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Array<{ _id: unknown; d: number; c: number }>;\r\n\r\n const accountMap = new Map(allAccounts.map(a => [String(a._id), a]));\r\n\r\n // Organize into revenue and expenses\r\n const revenueGroups: Record<string, ReportGroup> = {};\r\n const expenseGroups: Record<string, ReportGroup> = {};\r\n\r\n // Resolve the top-level IS group (Revenue, Cost of Sales, Operating Expenses)\r\n // by walking up the parent chain until hitting a group-label account type.\r\n const resolveGroupName = (at: { parentCode: string | null; name: string }) => {\r\n let current = at.parentCode ? country.getAccountType(at.parentCode) : undefined;\r\n while (current) {\r\n if (current.isGroup) return current.name;\r\n current = current.parentCode ? country.getAccountType(current.parentCode) : undefined;\r\n }\r\n return at.name;\r\n };\r\n\r\n for (const r of results) {\r\n const acc = accountMap.get(String(r._id));\r\n if (!acc) continue;\r\n\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at) continue;\r\n\r\n const mainType = extractMainType(at.category);\r\n const netAmount = mainType === 'Income' ? r.c - r.d : r.d - r.c;\r\n if (netAmount === 0) continue;\r\n\r\n const groupName = resolveGroupName(at);\r\n\r\n const groups = mainType === 'Income' ? revenueGroups : expenseGroups;\r\n\r\n if (!(groupName in groups)) {\r\n groups[groupName] = { name: groupName, total: 0, accounts: [] };\r\n }\r\n\r\n groups[groupName].accounts.push({\r\n id: acc._id,\r\n name: (acc.name as string) ?? at.name,\r\n code: (acc.accountNumber as string) ?? at.code,\r\n balance: netAmount,\r\n });\r\n groups[groupName].total += netAmount;\r\n }\r\n\r\n const labels = country.reportLabels ?? {};\r\n const revenue: ReportCategory = {\r\n name: labels.revenue ?? 'Revenue',\r\n total: Object.values(revenueGroups).reduce((s, g) => s + g.total, 0),\r\n groups: Object.values(revenueGroups),\r\n };\r\n\r\n const expenses: ReportCategory = {\r\n name: labels.expenses ?? 'Expenses',\r\n total: Object.values(expenseGroups).reduce((s, g) => s + g.total, 0),\r\n groups: Object.values(expenseGroups),\r\n };\r\n\r\n // Calculate COGS — use pack-declared group code, fall back to common names\r\n const cogsCode = country.cogsGroupCode;\r\n const isCogs = (name: string) =>\r\n cogsCode\r\n ? name === cogsCode\r\n : name === 'Cost of Sales' || name === 'Cost of Goods Sold';\r\n\r\n const cogsGroup = expenses.groups.find(g => isCogs(g.name));\r\n const costOfSales = cogsGroup?.total ?? 0;\r\n const grossProfit = revenue.total - costOfSales;\r\n const operatingExpenses = expenses.groups\r\n .filter(g => !isCogs(g.name))\r\n .reduce((s, g) => s + g.total, 0);\r\n const operatingIncome = grossProfit - operatingExpenses;\r\n const netIncome = revenue.total - expenses.total;\r\n\r\n const periodDisplay =\r\n params.dateOption === 'year'\r\n ? `For the year ended ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`\r\n : `${startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}`;\r\n\r\n return {\r\n metadata: {\r\n businessName: params.businessName,\r\n generatedAt: new Date().toISOString(),\r\n periodStart: startDate.toISOString().split('T')[0],\r\n periodEnd: endDate.toISOString().split('T')[0],\r\n displayPeriod: periodDisplay,\r\n },\r\n revenue,\r\n costOfSales,\r\n grossProfit,\r\n expenses,\r\n operatingIncome,\r\n netIncome,\r\n };\r\n}\r\n","/**\r\n * General Ledger Report\r\n *\r\n * Shows every posted entry for selected accounts with running balances.\r\n * Uses batched queries (3 max) instead of per-account loops.\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { GeneralLedgerReport, GeneralLedgerAccount, LedgerEntry } from '../types/report.js';\r\nimport type { AccountType } from '../types/core.js';\r\nimport type { CategoryKey } from '../types/core.js';\r\nimport { getDateRange, getFiscalYearStart } from '../utils/date-range.js';\r\nimport { computeEndingBalance } from '../utils/account-helpers.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface GeneralLedgerOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n fiscalYearStartMonth?: number;\r\n}\r\n\r\nexport async function generateGeneralLedger(\r\n opts: GeneralLedgerOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n accountId?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<GeneralLedgerReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1 } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const fiscalYearStart = getFiscalYearStart(startDate, fiscalYearStartMonth);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Get target accounts\r\n const acctQuery: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) acctQuery[orgField] = params.organizationId;\r\n if (params.accountId) acctQuery._id = params.accountId;\r\n\r\n const allAccounts = await AccountModel.find(acctQuery).lean() as Array<Record<string, unknown>>;\r\n\r\n // Filter to postable accounts (no groups, no totals)\r\n const filtered: Array<{ acc: Record<string, unknown>; at: AccountType }> = [];\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at || at.isGroup || at.isTotal) continue;\r\n filtered.push({ acc, at });\r\n }\r\n\r\n if (filtered.length === 0) {\r\n return { accounts: [], period: { startDate, endDate } };\r\n }\r\n\r\n // Separate BS vs IS account IDs (different opening-balance date ranges)\r\n const bsAccountIds: unknown[] = [];\r\n const isAccountIds: unknown[] = [];\r\n const allAccountIds: unknown[] = [];\r\n\r\n for (const { acc, at } of filtered) {\r\n allAccountIds.push(acc._id);\r\n if (at.category.startsWith('Balance Sheet')) {\r\n bsAccountIds.push(acc._id);\r\n } else {\r\n isAccountIds.push(acc._id);\r\n }\r\n }\r\n\r\n // Org scope helper\r\n const orgScope: Record<string, unknown> = {};\r\n if (orgField && params.organizationId) orgScope[orgField] = params.organizationId;\r\n\r\n // ── Batch queries (3 max, run in parallel) ──────────────────────────────────\r\n\r\n const openingBalancePipeline = (\r\n accountIds: unknown[],\r\n dateFilter: Record<string, unknown>,\r\n ) =>\r\n accountIds.length > 0\r\n ? JournalEntryModel.aggregate([\r\n { $match: { state: 'posted', date: dateFilter, ...orgScope } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: accountIds }, ...itemFilters } },\r\n {\r\n $group: {\r\n _id: '$journalItems.account',\r\n d: { $sum: '$journalItems.debit' },\r\n c: { $sum: '$journalItems.credit' },\r\n },\r\n },\r\n ])\r\n : Promise.resolve([]);\r\n\r\n const [bsOpenResults, isOpenResults, periodEntries] = await Promise.all([\r\n // BS opening: all posted entries before startDate\r\n openingBalancePipeline(bsAccountIds, { $lt: startDate }),\r\n // IS opening: posted entries from fiscal year start to before startDate\r\n openingBalancePipeline(isAccountIds, { $gte: fiscalYearStart, $lt: startDate }),\r\n // Period entries: all posted entries for any target account in the period\r\n JournalEntryModel.find({\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n 'journalItems.account': { $in: allAccountIds },\r\n ...orgScope,\r\n ...itemFilters,\r\n })\r\n .select('date referenceNumber label journalItems')\r\n .sort({ date: 1 })\r\n .lean() as Promise<Array<Record<string, unknown>>>,\r\n ]);\r\n\r\n // ── Build lookup maps ───────────────────────────────────────────────────────\r\n\r\n // Opening balance by account ID\r\n const openBalMap = new Map<string, { d: number; c: number }>();\r\n for (const r of [...(bsOpenResults as Array<{ _id: unknown; d: number; c: number }>),\r\n ...(isOpenResults as Array<{ _id: unknown; d: number; c: number }>)]) {\r\n openBalMap.set(String(r._id), { d: r.d, c: r.c });\r\n }\r\n\r\n // ── Pre-index period entries by account ID (O(entries × items) once) ────────\r\n\r\n const entryItemsByAccount = new Map<string, Array<{\r\n date: Date; referenceNumber: string; label: string; debit: number; credit: number;\r\n }>>();\r\n\r\n for (const entry of periodEntries) {\r\n const items = (entry.journalItems as Array<Record<string, unknown>>) ?? [];\r\n for (const item of items) {\r\n const accId = String(item.account);\r\n const debit = (item.debit as number) ?? 0;\r\n const credit = (item.credit as number) ?? 0;\r\n\r\n let list = entryItemsByAccount.get(accId);\r\n if (!list) {\r\n list = [];\r\n entryItemsByAccount.set(accId, list);\r\n }\r\n list.push({\r\n date: entry.date as Date,\r\n referenceNumber: (entry.referenceNumber as string) ?? '',\r\n label: (entry.label as string) ?? '',\r\n debit,\r\n credit,\r\n });\r\n }\r\n }\r\n\r\n // ── Assemble per-account results (O(accounts + total items)) ──────────────\r\n\r\n const glAccounts: GeneralLedgerAccount[] = [];\r\n\r\n for (const { acc, at } of filtered) {\r\n const accIdStr = String(acc._id);\r\n const openData = openBalMap.get(accIdStr);\r\n const openingBalance = openData\r\n ? computeEndingBalance(at.category as CategoryKey, openData.d, openData.c)\r\n : 0;\r\n\r\n let runningBalance = openingBalance;\r\n const entries: LedgerEntry[] = [];\r\n const mainType = extractMainType(at.category as CategoryKey);\r\n\r\n const accountItems = entryItemsByAccount.get(accIdStr) ?? [];\r\n for (const item of accountItems) {\r\n const delta =\r\n mainType === 'Asset' || mainType === 'Expense'\r\n ? item.debit - item.credit\r\n : item.credit - item.debit;\r\n\r\n runningBalance += delta;\r\n\r\n entries.push({\r\n date: item.date,\r\n referenceNumber: item.referenceNumber,\r\n label: item.label,\r\n debit: item.debit,\r\n credit: item.credit,\r\n runningBalance,\r\n });\r\n }\r\n\r\n glAccounts.push({\r\n account: acc,\r\n openingBalance,\r\n entries,\r\n closingBalance: runningBalance,\r\n });\r\n }\r\n\r\n return { accounts: glAccounts, period: { startDate, endDate } };\r\n}\r\n","/**\r\n * Cash Flow Statement\r\n *\r\n * Groups transactions by cashFlowCategory from account type definitions:\r\n * Operating, Investing, Financing.\r\n * Uses aggregation pipeline — no in-memory processing.\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { CashFlowCategory, CategoryKey } from '../types/core.js';\r\nimport type { CashFlowReport } from '../types/report.js';\r\nimport { getDateRange } from '../utils/date-range.js';\r\nimport { computeEndingBalance } from '../utils/account-helpers.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface CashFlowOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n}\r\n\r\nexport async function generateCashFlow(\r\n opts: CashFlowOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n businessName?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<CashFlowReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch accounts\r\n const q: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) q[orgField] = params.organizationId;\r\n const allAccounts = await AccountModel.find(q).lean() as Array<Record<string, unknown>>;\r\n\r\n // Build maps: accountId -> metadata, accountId -> raw account doc\r\n const accountCfMap = new Map<string, { category: CategoryKey; cfCategory: CashFlowCategory }>();\r\n const accountMap = new Map(allAccounts.map(a => [String(a._id), a]));\r\n const cfAccountIds: unknown[] = [];\r\n\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at || at.isGroup || at.isTotal) continue;\r\n\r\n const cf = at.cashFlowCategory;\r\n if (!cf) continue;\r\n\r\n // Normalize case: 'operating' → 'Operating'\r\n const normalized = (cf.charAt(0).toUpperCase() + cf.slice(1)) as CashFlowCategory;\r\n accountCfMap.set(String(acc._id), { category: at.category, cfCategory: normalized });\r\n cfAccountIds.push(acc._id);\r\n }\r\n\r\n // Aggregate journal items for accounts with cashFlowCategory\r\n const baseMatch: Record<string, unknown> = {\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n const results = cfAccountIds.length > 0\r\n ? await JournalEntryModel.aggregate([\r\n { $match: baseMatch },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: cfAccountIds }, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Array<{ _id: unknown; d: number; c: number }>\r\n : [];\r\n\r\n // Accumulate by category\r\n const flows: Record<CashFlowCategory, { total: number; accounts: Array<{ name: string; code: string; amount: number }> }> = {\r\n Operating: { total: 0, accounts: [] },\r\n Investing: { total: 0, accounts: [] },\r\n Financing: { total: 0, accounts: [] },\r\n };\r\n\r\n for (const r of results) {\r\n const accIdStr = String(r._id);\r\n const meta = accountCfMap.get(accIdStr);\r\n if (!meta) continue;\r\n\r\n // Net cash flow: for assets/expenses, debit increases (cash out), credit decreases (cash in)\r\n // For liabilities/equity/income, credit increases, debit decreases\r\n const amount = computeEndingBalance(meta.category, r.d, r.c);\r\n const acc = accountMap.get(accIdStr);\r\n const at = country.getAccountType(acc?.accountTypeCode as string);\r\n\r\n flows[meta.cfCategory].accounts.push({\r\n name: (acc?.name as string) ?? at?.name ?? '',\r\n code: (acc?.accountNumber as string) ?? at?.code ?? '',\r\n amount,\r\n });\r\n flows[meta.cfCategory].total += amount;\r\n }\r\n\r\n const netCashFlow = flows.Operating.total + flows.Investing.total + flows.Financing.total;\r\n\r\n const periodDisplay = `${startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}`;\r\n\r\n return {\r\n metadata: {\r\n businessName: params.businessName,\r\n generatedAt: new Date().toISOString(),\r\n periodStart: startDate.toISOString().split('T')[0],\r\n periodEnd: endDate.toISOString().split('T')[0],\r\n displayPeriod: periodDisplay,\r\n },\r\n operating: flows.Operating,\r\n investing: flows.Investing,\r\n financing: flows.Financing,\r\n netCashFlow,\r\n };\r\n}\r\n","/**\r\n * Fiscal Year Closing & Reopening\r\n *\r\n * Close: zeroes income/expense accounts, transfers net income to retained\r\n * earnings via a YEAR_END journal entry, marks the period closed.\r\n *\r\n * Reopen: validates no later period is closed, deletes the closing entry,\r\n * marks the period open again with an audit trail.\r\n *\r\n * Session management: creates an internal transaction by default.\r\n * Pass an external session to join a caller-managed transaction instead.\r\n */\r\n\r\nimport type { Model, ClientSession } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { Errors } from '../utils/errors.js';\r\nimport type { Logger } from '../utils/logger.js';\r\nimport { defaultLogger } from '../utils/logger.js';\r\nimport { acquireSession, finalizeSession } from '../utils/session.js';\r\n\r\nexport interface FiscalCloseOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n FiscalPeriodModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n retainedEarningsCode?: string;\r\n logger?: Logger;\r\n}\r\n\r\nexport interface FiscalCloseResult {\r\n periodId: unknown;\r\n netIncome: number;\r\n closingEntryId: unknown | null;\r\n accountsClosed: number;\r\n closedAt: Date;\r\n}\r\n\r\nexport async function closeFiscalPeriod(\r\n opts: FiscalCloseOptions,\r\n params: {\r\n periodId: unknown;\r\n organizationId?: unknown;\r\n closedBy?: string;\r\n session?: ClientSession;\r\n },\r\n): Promise<FiscalCloseResult> {\r\n const {\r\n AccountModel,\r\n JournalEntryModel,\r\n FiscalPeriodModel,\r\n country,\r\n orgField,\r\n retainedEarningsCode = country.retainedEarningsCode ?? '3660',\r\n logger = defaultLogger,\r\n } = opts;\r\n const { periodId, organizationId, closedBy } = params;\r\n requireOrgScope(orgField, organizationId);\r\n\r\n const { session, ownSession } = await acquireSession(\r\n AccountModel.db,\r\n params.session,\r\n logger,\r\n );\r\n let success = false;\r\n\r\n try {\r\n const queryOpts = session ? { session } : {};\r\n\r\n // 1. Fetch and validate the fiscal period (org-scoped)\r\n const periodQuery: Record<string, unknown> = { _id: periodId };\r\n if (orgField && organizationId) periodQuery[orgField] = organizationId;\r\n const period = await FiscalPeriodModel.findOne(periodQuery, null, queryOpts).lean() as Record<string, unknown> | null;\r\n if (!period) throw Errors.notFound('Fiscal period not found');\r\n if (period.closed) throw Errors.fiscal('Fiscal period is already closed');\r\n\r\n const startDate = period.startDate as Date;\r\n const endDate = period.endDate as Date;\r\n\r\n // 2. Find all income statement accounts for this org\r\n const accountQuery: Record<string, unknown> = { active: true };\r\n if (orgField && organizationId) accountQuery[orgField] = organizationId;\r\n const allAccounts = await AccountModel.find(accountQuery, null, queryOpts).lean() as Array<Record<string, unknown>>;\r\n\r\n const isAccounts: Array<{ id: unknown; code: string; isIncome: boolean }> = [];\r\n let retainedEarningsId: unknown = null;\r\n\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at) continue;\r\n\r\n if (acc.accountTypeCode === retainedEarningsCode) {\r\n retainedEarningsId = acc._id;\r\n }\r\n\r\n if (at.isGroup || at.isTotal) continue;\r\n if (at.category.startsWith('Income Statement')) {\r\n isAccounts.push({\r\n id: acc._id,\r\n code: at.code,\r\n isIncome: at.category === 'Income Statement-Income',\r\n });\r\n }\r\n }\r\n\r\n if (!retainedEarningsId) {\r\n throw Errors.fiscal(\r\n `Retained earnings account (code: ${retainedEarningsCode}) not found. ` +\r\n 'Create this account before closing the fiscal period.',\r\n );\r\n }\r\n\r\n // 3. Aggregate balances for all IS accounts in the period\r\n const baseMatch: Record<string, unknown> = {\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n };\r\n if (orgField && organizationId) baseMatch[orgField] = organizationId;\r\n\r\n const isIds = isAccounts.map(a => a.id);\r\n const balances = isIds.length > 0\r\n ? await JournalEntryModel.aggregate([\r\n { $match: baseMatch },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds } } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ], queryOpts) as Array<{ _id: unknown; d: number; c: number }>\r\n : [];\r\n\r\n // 4. Build closing journal entry items\r\n const closingItems: Array<{ account: unknown; debit: number; credit: number; label: string }> = [];\r\n let netIncome = 0;\r\n\r\n const balMap = new Map(balances.map(b => [String(b._id), b]));\r\n\r\n for (const acc of isAccounts) {\r\n const bal = balMap.get(String(acc.id));\r\n if (!bal) continue;\r\n\r\n const net = bal.c - bal.d;\r\n if (net === 0) continue;\r\n\r\n closingItems.push({\r\n account: acc.id,\r\n debit: net > 0 ? net : 0,\r\n credit: net < 0 ? Math.abs(net) : 0,\r\n label: `Close ${acc.code}`,\r\n });\r\n\r\n netIncome += net;\r\n }\r\n\r\n let closingEntryId: unknown = null;\r\n\r\n if (closingItems.length > 0) {\r\n closingItems.push({\r\n account: retainedEarningsId,\r\n debit: netIncome < 0 ? Math.abs(netIncome) : 0,\r\n credit: netIncome > 0 ? netIncome : 0,\r\n label: 'Transfer net income to retained earnings',\r\n });\r\n\r\n const totalDebit = closingItems.reduce((s, i) => s + i.debit, 0);\r\n const totalCredit = closingItems.reduce((s, i) => s + i.credit, 0);\r\n\r\n const closingEntryData: Record<string, unknown> = {\r\n journalType: 'YEAR_END',\r\n state: 'posted',\r\n date: endDate,\r\n label: `Fiscal year closing – ${(period.name as string) ?? 'Period'}`,\r\n journalItems: closingItems,\r\n totalDebit,\r\n totalCredit,\r\n };\r\n if (orgField && organizationId) closingEntryData[orgField] = organizationId;\r\n\r\n const [closingEntry] = await JournalEntryModel.create([closingEntryData], queryOpts);\r\n closingEntryId = closingEntry._id;\r\n }\r\n\r\n // 5. Mark the period as closed (org-scoped)\r\n const closedAt = new Date();\r\n await FiscalPeriodModel.findOneAndUpdate(\r\n periodQuery,\r\n { closed: true, closedAt, closedBy: closedBy ?? null, closingEntryId },\r\n queryOpts,\r\n );\r\n\r\n const result: FiscalCloseResult = {\r\n periodId,\r\n netIncome,\r\n closingEntryId,\r\n accountsClosed: closingItems.length - (closingItems.length > 0 ? 1 : 0),\r\n closedAt,\r\n };\r\n\r\n success = true;\r\n return result;\r\n } finally {\r\n await finalizeSession(session, ownSession, success);\r\n }\r\n}\r\n\r\n// ── Reopen ────────────────────────────────────────────────────────────────\r\n\r\nexport interface FiscalReopenResult {\r\n periodId: unknown;\r\n deletedEntryId: unknown | null;\r\n reopenedAt: Date;\r\n}\r\n\r\nexport async function reopenFiscalPeriod(\r\n opts: Pick<FiscalCloseOptions, 'JournalEntryModel' | 'FiscalPeriodModel'> & {\r\n orgField?: string;\r\n logger?: Logger;\r\n /** Any model on the same connection — used to start sessions */\r\n AccountModel?: Model<unknown>;\r\n },\r\n params: {\r\n periodId: unknown;\r\n organizationId?: unknown;\r\n reopenedBy?: string;\r\n session?: ClientSession;\r\n },\r\n): Promise<FiscalReopenResult> {\r\n const { JournalEntryModel, FiscalPeriodModel, orgField, logger = defaultLogger } = opts;\r\n const { periodId, organizationId, reopenedBy } = params;\r\n requireOrgScope(orgField, organizationId);\r\n\r\n // Use any available model's db connection for session creation\r\n const db = (opts.AccountModel ?? FiscalPeriodModel).db;\r\n const { session, ownSession } = await acquireSession(db, params.session, logger);\r\n let success = false;\r\n\r\n try {\r\n const queryOpts = session ? { session } : {};\r\n\r\n // 1. Fetch and validate the period (org-scoped)\r\n const periodQuery: Record<string, unknown> = { _id: periodId };\r\n if (orgField && organizationId) periodQuery[orgField] = organizationId;\r\n const period = await FiscalPeriodModel.findOne(periodQuery, null, queryOpts).lean() as Record<string, unknown> | null;\r\n if (!period) throw Errors.notFound('Fiscal period not found');\r\n if (!period.closed) throw Errors.fiscal('Fiscal period is not closed');\r\n\r\n // 2. Block if a later period is already closed (prevents cascade corruption)\r\n const laterQuery: Record<string, unknown> = {\r\n closed: true,\r\n startDate: { $gt: period.endDate },\r\n };\r\n if (orgField && organizationId) laterQuery[orgField] = organizationId;\r\n\r\n const laterClosed = await FiscalPeriodModel.findOne(laterQuery, null, queryOpts).lean();\r\n if (laterClosed) {\r\n throw Errors.fiscal(\r\n 'Cannot reopen: a later fiscal period is already closed. Reopen later periods first.',\r\n );\r\n }\r\n\r\n // 3. Delete the closing journal entry (if one was created)\r\n const closingEntryId = period.closingEntryId ?? null;\r\n if (closingEntryId) {\r\n await JournalEntryModel.findByIdAndDelete(closingEntryId, queryOpts);\r\n }\r\n\r\n // 4. Mark the period as reopened (org-scoped)\r\n const reopenedAt = new Date();\r\n await FiscalPeriodModel.findOneAndUpdate(\r\n periodQuery,\r\n {\r\n closed: false,\r\n closedAt: null,\r\n closedBy: null,\r\n closingEntryId: null,\r\n reopenedAt,\r\n reopenedBy: reopenedBy ?? null,\r\n },\r\n queryOpts,\r\n );\r\n\r\n const result: FiscalReopenResult = {\r\n periodId,\r\n deletedEntryId: closingEntryId,\r\n reopenedAt,\r\n };\r\n\r\n success = true;\r\n return result;\r\n } finally {\r\n await finalizeSession(session, ownSession, success);\r\n }\r\n}\r\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as createJournalEntrySchema, r as createAccountSchema, t as createFiscalPeriodSchema } from "./fiscal-period.schema-CbALaaKl.mjs";
2
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";
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-DuXDgVvb.mjs";
4
4
  import { n as Errors, t as AccountingError } from "./errors-CeqRahE-.mjs";
5
5
  import { n as finalizeSession, r as defaultLogger, t as acquireSession } from "./session-Dh0s6zG4.mjs";
6
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";
@@ -1,3 +1,3 @@
1
- import { a as generateIncomeStatement, d as generateTrialBalance, i as generateGeneralLedger, n as reopenFiscalPeriod, o as generateBalanceSheet, r as generateCashFlow, t as closeFiscalPeriod } from "../fiscal-close-CNOwv_ud.mjs";
1
+ import { a as generateIncomeStatement, d as generateTrialBalance, i as generateGeneralLedger, n as reopenFiscalPeriod, o as generateBalanceSheet, r as generateCashFlow, t as closeFiscalPeriod } from "../fiscal-close-DuXDgVvb.mjs";
2
2
 
3
3
  export { closeFiscalPeriod, generateBalanceSheet, generateCashFlow, generateGeneralLedger, generateIncomeStatement, generateTrialBalance, reopenFiscalPeriod };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/ledger",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Production-grade double-entry accounting engine for MongoDB — schemas, reports, tax, multi-tenant",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -113,4 +113,4 @@
113
113
  "typescript": "^5.7.0",
114
114
  "vitest": "^3.0.0"
115
115
  }
116
- }
116
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"fiscal-close-CNOwv_ud.mjs","names":[],"sources":["../src/utils/date-range.ts","../src/utils/filter-builder.ts","../src/reports/trial-balance.ts","../src/utils/account-helpers.ts","../src/reports/balance-sheet.ts","../src/reports/income-statement.ts","../src/reports/general-ledger.ts","../src/reports/cash-flow.ts","../src/reports/fiscal-close.ts"],"sourcesContent":["/**\r\n * Date Range Utility — Compute period boundaries for reports.\r\n */\r\n\r\nimport type { DateOption, DateRange, QuarterValue, CustomDateRange } from '../types/core.js';\r\n\r\n/**\r\n * Compute start/end dates from a date option + value.\r\n *\r\n * Examples:\r\n * getDateRange('month', '2025-03') → Mar 1 – Mar 31\r\n * getDateRange('quarter', { quarter: 2, year: 2025 }) → Apr 1 – Jun 30\r\n * getDateRange('year', 2025) → Jan 1 – Dec 31\r\n * getDateRange('custom', { startDate, endDate })\r\n */\r\nexport function getDateRange(option: DateOption, value: unknown): DateRange {\r\n switch (option) {\r\n case 'month': {\r\n // Parse 'YYYY-MM' strings explicitly to avoid UTC-vs-local timezone shift\r\n let year: number;\r\n let month: number;\r\n const strVal = String(value);\r\n const match = strVal.match(/^(\\d{4})-(\\d{1,2})$/);\r\n if (match) {\r\n year = parseInt(match[1], 10);\r\n month = parseInt(match[2], 10) - 1; // 0-indexed\r\n } else {\r\n const date = new Date(value as string | number | Date);\r\n year = date.getFullYear();\r\n month = date.getMonth();\r\n }\r\n const startDate = new Date(year, month, 1);\r\n const endDate = new Date(year, month + 1, 0, 23, 59, 59, 999);\r\n return { startDate, endDate };\r\n }\r\n\r\n case 'quarter': {\r\n const { quarter, year } = value as QuarterValue;\r\n const startMonth = (quarter - 1) * 3;\r\n const startDate = new Date(year, startMonth, 1);\r\n const endDate = new Date(year, startMonth + 3, 0, 23, 59, 59, 999);\r\n return { startDate, endDate };\r\n }\r\n\r\n case 'year': {\r\n const year = typeof value === 'number' ? value : parseInt(String(value), 10);\r\n const startDate = new Date(year, 0, 1);\r\n const endDate = new Date(year, 11, 31, 23, 59, 59, 999);\r\n return { startDate, endDate };\r\n }\r\n\r\n case 'custom': {\r\n const { startDate, endDate } = value as CustomDateRange;\r\n const end = new Date(endDate);\r\n // Normalize end date to end-of-day if time is midnight (00:00:00)\r\n if (end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0 && end.getMilliseconds() === 0) {\r\n end.setHours(23, 59, 59, 999);\r\n }\r\n return {\r\n startDate: new Date(startDate),\r\n endDate: end,\r\n };\r\n }\r\n\r\n default: {\r\n // Default: current month\r\n const now = new Date();\r\n return {\r\n startDate: new Date(now.getFullYear(), now.getMonth(), 1),\r\n endDate: new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999),\r\n };\r\n }\r\n }\r\n}\r\n\r\n/** Get fiscal year start date for a given date and fiscal start month */\r\nexport function getFiscalYearStart(date: Date, fiscalStartMonth = 1): Date {\r\n const month = fiscalStartMonth - 1; // 0-indexed\r\n const year = date.getMonth() < month ? date.getFullYear() - 1 : date.getFullYear();\r\n return new Date(year, month, 1);\r\n}\r\n","/**\r\n * Filter Builder — Sanitizes user-supplied dimension filters for aggregation pipelines.\r\n *\r\n * Prevents injection of dangerous MongoDB operators while allowing\r\n * standard equality and comparison filters on custom dimension fields.\r\n */\r\n\r\nconst BLOCKED_OPERATORS = new Set([\r\n '$where', '$expr', '$function', '$accumulator',\r\n '$merge', '$out', '$unionWith',\r\n]);\r\n\r\n/**\r\n * Build a sanitized filter object from user-supplied dimension filters.\r\n * Blocks dangerous operators ($where, $expr, $function, etc.).\r\n *\r\n * @param filters - Key-value filters (e.g. { 'journalItems.departmentId': 'dept-1' })\r\n * @returns Sanitized filter object safe for $match stages\r\n * @throws Error if a blocked operator is used\r\n */\r\nexport function buildItemFilters(filters?: Record<string, unknown>): Record<string, unknown> {\r\n if (!filters || Object.keys(filters).length === 0) return {};\r\n\r\n const result: Record<string, unknown> = {};\r\n\r\n for (const [key, value] of Object.entries(filters)) {\r\n // Block operators at top level\r\n if (key.startsWith('$')) {\r\n throw new Error(`Filter key \"${key}\" is not allowed. Use field names, not operators.`);\r\n }\r\n\r\n // Check nested values for blocked operators\r\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\r\n for (const opKey of Object.keys(value as Record<string, unknown>)) {\r\n if (BLOCKED_OPERATORS.has(opKey)) {\r\n throw new Error(`Filter operator \"${opKey}\" is not allowed.`);\r\n }\r\n }\r\n }\r\n\r\n result[key] = value;\r\n }\r\n\r\n return result;\r\n}\r\n","/**\r\n * Trial Balance Report\r\n *\r\n * Three-column trial balance: Initial + Current Period + Ending Balance.\r\n * Pure aggregation pipeline — no cached balances.\r\n */\r\n\r\nimport type { Model, PipelineStage } from 'mongoose';\r\nimport type { AccountType, CategoryKey } from '../types/core.js';\r\nimport type { TrialBalanceRow, TrialBalanceReport } from '../types/report.js';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport { getDateRange, getFiscalYearStart } from '../utils/date-range.js';\r\nimport { computeEndingBalance } from '../utils/account-helpers.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface TrialBalanceOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n fiscalYearStartMonth?: number;\r\n}\r\n\r\nexport async function generateTrialBalance(\r\n opts: TrialBalanceOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n accountId?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<TrialBalanceReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1 } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const fiscalYearStart = getFiscalYearStart(startDate, fiscalYearStartMonth);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch all active accounts\r\n const accountQuery: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) accountQuery[orgField] = params.organizationId;\r\n\r\n const allAccounts = await AccountModel.find(accountQuery).lean() as Array<Record<string, unknown>>;\r\n\r\n // Split by statement type\r\n const bsIds: unknown[] = [];\r\n const isIds: unknown[] = [];\r\n\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at || at.isGroup) continue;\r\n\r\n if (at.category.startsWith('Balance Sheet')) bsIds.push(acc._id);\r\n else if (at.category.startsWith('Income Statement')) isIds.push(acc._id);\r\n }\r\n\r\n const baseMatch: Record<string, unknown> = { state: 'posted' };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n const accountFilter = params.accountId ? { 'journalItems.account': params.accountId } : {};\r\n\r\n // Build pipelines\r\n const buildPipeline = (ids: unknown[], dateFrom: Date, dateTo: Date): PipelineStage[] => [\r\n { $match: { ...baseMatch, date: { $gte: dateFrom, $lt: dateTo } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: ids }, ...accountFilter, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ];\r\n\r\n // BS initial: all history before startDate\r\n // IS initial: fiscal year start → startDate\r\n // Current: startDate → endDate\r\n const [bsInitial, isInitial, current] = await Promise.all([\r\n bsIds.length ? JournalEntryModel.aggregate(buildPipeline(bsIds, new Date(0), startDate)) : [],\r\n isIds.length ? JournalEntryModel.aggregate(buildPipeline(isIds, fiscalYearStart, startDate)) : [],\r\n JournalEntryModel.aggregate(buildPipeline([...bsIds, ...isIds], startDate, new Date(endDate.getTime() + 1))),\r\n ]);\r\n\r\n // Merge\r\n const map = new Map<string, { iD: number; iC: number; cD: number; cC: number }>();\r\n\r\n for (const r of [...bsInitial, ...isInitial]) {\r\n const key = String(r._id);\r\n map.set(key, { iD: r.d, iC: r.c, cD: 0, cC: 0 });\r\n }\r\n for (const r of current) {\r\n const key = String(r._id);\r\n const existing = map.get(key) ?? { iD: 0, iC: 0, cD: 0, cC: 0 };\r\n existing.cD = r.d;\r\n existing.cC = r.c;\r\n map.set(key, existing);\r\n }\r\n\r\n // Build rows\r\n const accountLookup = new Map(allAccounts.map(a => [String(a._id), a]));\r\n\r\n const rows: TrialBalanceRow[] = [];\r\n for (const [id, bal] of map) {\r\n const acc = accountLookup.get(id);\r\n const totalD = bal.iD + bal.cD;\r\n const totalC = bal.iC + bal.cC;\r\n const net = totalD - totalC;\r\n\r\n rows.push({\r\n account: acc ?? id,\r\n initial: { debit: bal.iD, credit: bal.iC },\r\n current: { debit: bal.cD, credit: bal.cC },\r\n ending: net >= 0 ? { debit: net, credit: 0 } : { debit: 0, credit: Math.abs(net) },\r\n });\r\n }\r\n\r\n return { rows, period: { startDate, endDate } };\r\n}\r\n","/**\r\n * Account Helper Utilities\r\n */\r\n\r\nimport type { AccountType, TotalAccountOp, CategoryKey } from '../types/core.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\n\r\n/**\r\n * Check if an account type is a virtual tax sub-account.\r\n * Returns true if the account's parent has `isVirtualTotal: true`.\r\n * Works for any country pack — no code format assumptions.\r\n */\r\nexport function isVirtualTaxAccount(accountType: AccountType, accountMap: Map<string, AccountType>): boolean {\r\n if (!accountType.parentCode) return false;\r\n const parent = accountMap.get(accountType.parentCode);\r\n return parent?.isVirtualTotal === true;\r\n}\r\n\r\n/** Check if an account type is a balance sheet account */\r\nexport function isBalanceSheetAccountType(accountType: AccountType): boolean {\r\n const { category } = accountType;\r\n return category.endsWith('-Asset') || category.endsWith('-Liability') || category.endsWith('-Equity');\r\n}\r\n\r\n/** Check if an account type is an income statement account */\r\nexport function isIncomeStatementAccountType(accountType: AccountType): boolean {\r\n const { category } = accountType;\r\n return category.endsWith('-Income') || category.endsWith('-Expense');\r\n}\r\n\r\n/**\r\n * Calculate a total from sub-accounts using the totalAccountTypes formula.\r\n * @param formula - Array of { account, operation } instructions\r\n * @param balanceMap - Map of account code → balance\r\n */\r\nexport function calculateTotal(\r\n formula: readonly TotalAccountOp[],\r\n balanceMap: Map<string, number>,\r\n): number {\r\n let total = 0;\r\n for (const item of formula) {\r\n const balance = balanceMap.get(item.account) ?? 0;\r\n total += item.operation === '+' ? balance : -balance;\r\n }\r\n return total;\r\n}\r\n\r\n/**\r\n * Compute the ending balance for an account given its debits and credits.\r\n * Uses the account's main type to determine normal balance direction.\r\n *\r\n * Assets & Expenses: debit - credit\r\n * Liabilities, Equity & Income: credit - debit\r\n */\r\nexport function computeEndingBalance(\r\n category: CategoryKey,\r\n totalDebit: number,\r\n totalCredit: number,\r\n): number {\r\n const mainType = extractMainType(category);\r\n if (mainType === 'Asset' || mainType === 'Expense') {\r\n return totalDebit - totalCredit;\r\n }\r\n return totalCredit - totalDebit;\r\n}\r\n\r\n/**\r\n * Build a lookup map from an array of account types.\r\n */\r\nexport function buildAccountTypeMap(accountTypes: readonly AccountType[]): Map<string, AccountType> {\r\n const map = new Map<string, AccountType>();\r\n for (const at of accountTypes) {\r\n map.set(at.code, at);\r\n }\r\n return map;\r\n}\r\n","/**\r\n * Balance Sheet Report\r\n *\r\n * Assets = Liabilities + Equity\r\n * Net income injected into retained earnings for the current fiscal year.\r\n */\r\n\r\nimport type { Model, PipelineStage } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { BalanceSheetReport, ReportCategory, ReportGroup, ReportAccount } from '../types/report.js';\r\nimport { getDateRange, getFiscalYearStart } from '../utils/date-range.js';\r\nimport { computeEndingBalance, calculateTotal, isVirtualTaxAccount, buildAccountTypeMap } from '../utils/account-helpers.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\nimport type { CategoryKey } from '../types/core.js';\r\n\r\nexport interface BalanceSheetOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n fiscalYearStartMonth?: number;\r\n /** Display code for prior retained earnings (default: '3660') */\r\n retainedEarningsCode?: string;\r\n /** Display code for current year net income (default: '3680') */\r\n currentYearEarningsCode?: string;\r\n}\r\n\r\nexport async function generateBalanceSheet(\r\n opts: BalanceSheetOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n businessName?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<BalanceSheetReport> {\r\n const {\r\n AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1,\r\n retainedEarningsCode = country.retainedEarningsCode ?? '3660',\r\n currentYearEarningsCode = country.currentYearEarningsCode ?? '3680',\r\n } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const fiscalYearStart = getFiscalYearStart(endDate, fiscalYearStartMonth);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch accounts\r\n const q: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) q[orgField] = params.organizationId;\r\n const allAccounts = await AccountModel.find(q).lean() as Array<Record<string, unknown>>;\r\n\r\n // Balance sheet account IDs\r\n const bsIds = allAccounts\r\n .filter(a => {\r\n const at = country.getAccountType(a.accountTypeCode as string);\r\n return at && !at.isGroup && at.category.startsWith('Balance Sheet');\r\n })\r\n .map(a => a._id);\r\n\r\n // Income statement account IDs (for net income calculation)\r\n const isIds = allAccounts\r\n .filter(a => {\r\n const at = country.getAccountType(a.accountTypeCode as string);\r\n return at && !at.isGroup && !at.isTotal && at.category.startsWith('Income Statement');\r\n })\r\n .map(a => a._id);\r\n\r\n const baseMatch: Record<string, unknown> = { state: 'posted' };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n // Run pipelines in parallel\r\n const [bsResults, netIncomeResults, priorRetainedResults] = await Promise.all([\r\n // Balance sheet balances (all time up to endDate)\r\n JournalEntryModel.aggregate([\r\n { $match: { ...baseMatch, date: { $lte: endDate } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: bsIds }, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Promise<Array<{ _id: unknown; d: number; c: number }>>,\r\n\r\n // Net income (fiscal year start → endDate)\r\n JournalEntryModel.aggregate([\r\n { $match: { ...baseMatch, date: { $gte: fiscalYearStart, $lte: endDate } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds }, ...itemFilters } },\r\n { $group: { _id: null, d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Promise<Array<{ _id: unknown; d: number; c: number }>>,\r\n\r\n // Prior retained earnings (all income statement before fiscal year)\r\n JournalEntryModel.aggregate([\r\n { $match: { ...baseMatch, date: { $lt: fiscalYearStart } } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds }, ...itemFilters } },\r\n { $group: { _id: null, d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Promise<Array<{ _id: unknown; d: number; c: number }>>,\r\n ]);\r\n\r\n const netIncome = netIncomeResults.length > 0 ? netIncomeResults[0].c - netIncomeResults[0].d : 0;\r\n const priorRetained = priorRetainedResults.length > 0 ? priorRetainedResults[0].c - priorRetainedResults[0].d : 0;\r\n\r\n // Build categories\r\n const accountMap = new Map(allAccounts.map(a => [String(a._id), a]));\r\n const accountTypeMap = buildAccountTypeMap(country.accountTypes);\r\n const balanceMap = new Map<string, number>();\r\n\r\n const labels = country.reportLabels ?? {};\r\n const assets: ReportCategory = { name: labels.assets ?? 'Assets', total: 0, groups: [] };\r\n const liabilities: ReportCategory = { name: labels.liabilities ?? 'Liabilities', total: 0, groups: [] };\r\n const equity: ReportCategory = { name: labels.equity ?? 'Equity', total: 0, groups: [] };\r\n\r\n const groupsMap: Record<string, Record<string, ReportGroup>> = {\r\n Asset: {}, Liability: {}, Equity: {},\r\n };\r\n\r\n for (const r of bsResults) {\r\n const acc = accountMap.get(String(r._id));\r\n if (!acc) continue;\r\n\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at) continue;\r\n\r\n const mainType = extractMainType(at.category) ?? 'Asset';\r\n const balance = computeEndingBalance(at.category as CategoryKey, r.d, r.c);\r\n balanceMap.set(at.code, balance);\r\n\r\n const parentAt = at.parentCode ? country.getAccountType(at.parentCode) : undefined;\r\n const groupName = parentAt?.name ?? at.name;\r\n\r\n if (!(groupName in groupsMap[mainType])) {\r\n groupsMap[mainType][groupName] = { name: groupName, total: 0, accounts: [] };\r\n }\r\n\r\n const group = groupsMap[mainType][groupName];\r\n\r\n // Skip virtual tax sub-accounts from display but include in calculation\r\n if (!isVirtualTaxAccount(at, accountTypeMap)) {\r\n group.accounts.push({\r\n id: acc._id,\r\n name: (acc.name as string) ?? at.name,\r\n code: (acc.accountNumber as string) ?? at.code,\r\n balance,\r\n isTotal: at.isTotal,\r\n isVirtualTotal: at.isVirtualTotal,\r\n });\r\n }\r\n\r\n if (!at.isTotal) {\r\n group.total += balance;\r\n }\r\n }\r\n\r\n // Add retained earnings to equity\r\n const reGroup: ReportGroup = {\r\n name: 'Retained Earnings',\r\n total: priorRetained + netIncome,\r\n accounts: [\r\n { id: 'prior-retained', name: 'Previous Years Retained Earnings', code: retainedEarningsCode, balance: priorRetained },\r\n { id: 'current-year', name: `Current Year Net Income (${endDate.getFullYear()})`, code: currentYearEarningsCode, balance: netIncome, isCalculated: true },\r\n ],\r\n };\r\n\r\n if (!(reGroup.name in groupsMap.Equity)) {\r\n groupsMap.Equity[reGroup.name] = reGroup;\r\n } else {\r\n groupsMap.Equity[reGroup.name].accounts.push(...reGroup.accounts);\r\n groupsMap.Equity[reGroup.name].total += reGroup.total;\r\n }\r\n\r\n // Convert groups maps to arrays\r\n assets.groups = Object.values(groupsMap.Asset);\r\n liabilities.groups = Object.values(groupsMap.Liability);\r\n equity.groups = Object.values(groupsMap.Equity);\r\n\r\n // Sum totals\r\n assets.total = assets.groups.reduce((s, g) => s + g.total, 0);\r\n liabilities.total = liabilities.groups.reduce((s, g) => s + g.total, 0);\r\n equity.total = equity.groups.reduce((s, g) => s + g.total, 0);\r\n\r\n const liabilitiesAndEquity = liabilities.total + equity.total;\r\n\r\n return {\r\n metadata: {\r\n businessName: params.businessName,\r\n generatedAt: new Date().toISOString(),\r\n asOfDate: endDate.toISOString().split('T')[0],\r\n displayDate: `As of ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`,\r\n },\r\n assets,\r\n liabilities,\r\n equity,\r\n summary: {\r\n totalAssets: assets.total,\r\n totalLiabilities: liabilities.total,\r\n totalEquity: equity.total,\r\n liabilitiesAndEquity,\r\n difference: assets.total - liabilitiesAndEquity,\r\n isBalanced: assets.total === liabilitiesAndEquity,\r\n },\r\n };\r\n}\r\n","/**\r\n * Income Statement (Profit & Loss) Report\r\n *\r\n * Revenue - COGS = Gross Profit\r\n * Gross Profit - Operating Expenses = Operating Income\r\n * Operating Income ± Other = Net Income\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { IncomeStatementReport, ReportCategory, ReportGroup } from '../types/report.js';\r\nimport { getDateRange } from '../utils/date-range.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface IncomeStatementOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n}\r\n\r\nexport async function generateIncomeStatement(\r\n opts: IncomeStatementOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n businessName?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<IncomeStatementReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch accounts\r\n const q: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) q[orgField] = params.organizationId;\r\n const allAccounts = await AccountModel.find(q).lean() as Array<Record<string, unknown>>;\r\n\r\n // Income statement posting accounts only\r\n const isAccounts = allAccounts.filter(a => {\r\n const at = country.getAccountType(a.accountTypeCode as string);\r\n return at && !at.isGroup && !at.isTotal && at.category.startsWith('Income Statement');\r\n });\r\n const isIds = isAccounts.map(a => a._id);\r\n\r\n const baseMatch: Record<string, unknown> = {\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n const results = await JournalEntryModel.aggregate([\r\n { $match: baseMatch },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds }, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Array<{ _id: unknown; d: number; c: number }>;\r\n\r\n const accountMap = new Map(allAccounts.map(a => [String(a._id), a]));\r\n\r\n // Organize into revenue and expenses\r\n const revenueGroups: Record<string, ReportGroup> = {};\r\n const expenseGroups: Record<string, ReportGroup> = {};\r\n\r\n for (const r of results) {\r\n const acc = accountMap.get(String(r._id));\r\n if (!acc) continue;\r\n\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at) continue;\r\n\r\n const mainType = extractMainType(at.category);\r\n const netAmount = mainType === 'Income' ? r.c - r.d : r.d - r.c;\r\n if (netAmount === 0) continue;\r\n\r\n const parentAt = at.parentCode ? country.getAccountType(at.parentCode) : undefined;\r\n const groupName = parentAt?.name ?? at.name;\r\n\r\n const groups = mainType === 'Income' ? revenueGroups : expenseGroups;\r\n\r\n if (!(groupName in groups)) {\r\n groups[groupName] = { name: groupName, total: 0, accounts: [] };\r\n }\r\n\r\n groups[groupName].accounts.push({\r\n id: acc._id,\r\n name: (acc.name as string) ?? at.name,\r\n code: (acc.accountNumber as string) ?? at.code,\r\n balance: netAmount,\r\n });\r\n groups[groupName].total += netAmount;\r\n }\r\n\r\n const labels = country.reportLabels ?? {};\r\n const revenue: ReportCategory = {\r\n name: labels.revenue ?? 'Revenue',\r\n total: Object.values(revenueGroups).reduce((s, g) => s + g.total, 0),\r\n groups: Object.values(revenueGroups),\r\n };\r\n\r\n const expenses: ReportCategory = {\r\n name: labels.expenses ?? 'Expenses',\r\n total: Object.values(expenseGroups).reduce((s, g) => s + g.total, 0),\r\n groups: Object.values(expenseGroups),\r\n };\r\n\r\n // Calculate COGS — use pack-declared group code, fall back to common names\r\n const cogsCode = country.cogsGroupCode;\r\n const isCogs = (name: string) =>\r\n cogsCode\r\n ? name === cogsCode\r\n : name === 'Cost of Sales' || name === 'Cost of Goods Sold';\r\n\r\n const cogsGroup = expenses.groups.find(g => isCogs(g.name));\r\n const costOfSales = cogsGroup?.total ?? 0;\r\n const grossProfit = revenue.total - costOfSales;\r\n const operatingExpenses = expenses.groups\r\n .filter(g => !isCogs(g.name))\r\n .reduce((s, g) => s + g.total, 0);\r\n const operatingIncome = grossProfit - operatingExpenses;\r\n const netIncome = revenue.total - expenses.total;\r\n\r\n const periodDisplay =\r\n params.dateOption === 'year'\r\n ? `For the year ended ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`\r\n : `${startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}`;\r\n\r\n return {\r\n metadata: {\r\n businessName: params.businessName,\r\n generatedAt: new Date().toISOString(),\r\n periodStart: startDate.toISOString().split('T')[0],\r\n periodEnd: endDate.toISOString().split('T')[0],\r\n displayPeriod: periodDisplay,\r\n },\r\n revenue,\r\n costOfSales,\r\n grossProfit,\r\n expenses,\r\n operatingIncome,\r\n netIncome,\r\n };\r\n}\r\n","/**\r\n * General Ledger Report\r\n *\r\n * Shows every posted entry for selected accounts with running balances.\r\n * Uses batched queries (3 max) instead of per-account loops.\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { GeneralLedgerReport, GeneralLedgerAccount, LedgerEntry } from '../types/report.js';\r\nimport type { AccountType } from '../types/core.js';\r\nimport type { CategoryKey } from '../types/core.js';\r\nimport { getDateRange, getFiscalYearStart } from '../utils/date-range.js';\r\nimport { computeEndingBalance } from '../utils/account-helpers.js';\r\nimport { extractMainType } from '../constants/categories.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface GeneralLedgerOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n fiscalYearStartMonth?: number;\r\n}\r\n\r\nexport async function generateGeneralLedger(\r\n opts: GeneralLedgerOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n accountId?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<GeneralLedgerReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField, fiscalYearStartMonth = 1 } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const fiscalYearStart = getFiscalYearStart(startDate, fiscalYearStartMonth);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Get target accounts\r\n const acctQuery: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) acctQuery[orgField] = params.organizationId;\r\n if (params.accountId) acctQuery._id = params.accountId;\r\n\r\n const allAccounts = await AccountModel.find(acctQuery).lean() as Array<Record<string, unknown>>;\r\n\r\n // Filter to postable accounts (no groups, no totals)\r\n const filtered: Array<{ acc: Record<string, unknown>; at: AccountType }> = [];\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at || at.isGroup || at.isTotal) continue;\r\n filtered.push({ acc, at });\r\n }\r\n\r\n if (filtered.length === 0) {\r\n return { accounts: [], period: { startDate, endDate } };\r\n }\r\n\r\n // Separate BS vs IS account IDs (different opening-balance date ranges)\r\n const bsAccountIds: unknown[] = [];\r\n const isAccountIds: unknown[] = [];\r\n const allAccountIds: unknown[] = [];\r\n\r\n for (const { acc, at } of filtered) {\r\n allAccountIds.push(acc._id);\r\n if (at.category.startsWith('Balance Sheet')) {\r\n bsAccountIds.push(acc._id);\r\n } else {\r\n isAccountIds.push(acc._id);\r\n }\r\n }\r\n\r\n // Org scope helper\r\n const orgScope: Record<string, unknown> = {};\r\n if (orgField && params.organizationId) orgScope[orgField] = params.organizationId;\r\n\r\n // ── Batch queries (3 max, run in parallel) ──────────────────────────────────\r\n\r\n const openingBalancePipeline = (\r\n accountIds: unknown[],\r\n dateFilter: Record<string, unknown>,\r\n ) =>\r\n accountIds.length > 0\r\n ? JournalEntryModel.aggregate([\r\n { $match: { state: 'posted', date: dateFilter, ...orgScope } },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: accountIds }, ...itemFilters } },\r\n {\r\n $group: {\r\n _id: '$journalItems.account',\r\n d: { $sum: '$journalItems.debit' },\r\n c: { $sum: '$journalItems.credit' },\r\n },\r\n },\r\n ])\r\n : Promise.resolve([]);\r\n\r\n const [bsOpenResults, isOpenResults, periodEntries] = await Promise.all([\r\n // BS opening: all posted entries before startDate\r\n openingBalancePipeline(bsAccountIds, { $lt: startDate }),\r\n // IS opening: posted entries from fiscal year start to before startDate\r\n openingBalancePipeline(isAccountIds, { $gte: fiscalYearStart, $lt: startDate }),\r\n // Period entries: all posted entries for any target account in the period\r\n JournalEntryModel.find({\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n 'journalItems.account': { $in: allAccountIds },\r\n ...orgScope,\r\n ...itemFilters,\r\n })\r\n .select('date referenceNumber label journalItems')\r\n .sort({ date: 1 })\r\n .lean() as Promise<Array<Record<string, unknown>>>,\r\n ]);\r\n\r\n // ── Build lookup maps ───────────────────────────────────────────────────────\r\n\r\n // Opening balance by account ID\r\n const openBalMap = new Map<string, { d: number; c: number }>();\r\n for (const r of [...(bsOpenResults as Array<{ _id: unknown; d: number; c: number }>),\r\n ...(isOpenResults as Array<{ _id: unknown; d: number; c: number }>)]) {\r\n openBalMap.set(String(r._id), { d: r.d, c: r.c });\r\n }\r\n\r\n // ── Pre-index period entries by account ID (O(entries × items) once) ────────\r\n\r\n const entryItemsByAccount = new Map<string, Array<{\r\n date: Date; referenceNumber: string; label: string; debit: number; credit: number;\r\n }>>();\r\n\r\n for (const entry of periodEntries) {\r\n const items = (entry.journalItems as Array<Record<string, unknown>>) ?? [];\r\n for (const item of items) {\r\n const accId = String(item.account);\r\n const debit = (item.debit as number) ?? 0;\r\n const credit = (item.credit as number) ?? 0;\r\n\r\n let list = entryItemsByAccount.get(accId);\r\n if (!list) {\r\n list = [];\r\n entryItemsByAccount.set(accId, list);\r\n }\r\n list.push({\r\n date: entry.date as Date,\r\n referenceNumber: (entry.referenceNumber as string) ?? '',\r\n label: (entry.label as string) ?? '',\r\n debit,\r\n credit,\r\n });\r\n }\r\n }\r\n\r\n // ── Assemble per-account results (O(accounts + total items)) ──────────────\r\n\r\n const glAccounts: GeneralLedgerAccount[] = [];\r\n\r\n for (const { acc, at } of filtered) {\r\n const accIdStr = String(acc._id);\r\n const openData = openBalMap.get(accIdStr);\r\n const openingBalance = openData\r\n ? computeEndingBalance(at.category as CategoryKey, openData.d, openData.c)\r\n : 0;\r\n\r\n let runningBalance = openingBalance;\r\n const entries: LedgerEntry[] = [];\r\n const mainType = extractMainType(at.category as CategoryKey);\r\n\r\n const accountItems = entryItemsByAccount.get(accIdStr) ?? [];\r\n for (const item of accountItems) {\r\n const delta =\r\n mainType === 'Asset' || mainType === 'Expense'\r\n ? item.debit - item.credit\r\n : item.credit - item.debit;\r\n\r\n runningBalance += delta;\r\n\r\n entries.push({\r\n date: item.date,\r\n referenceNumber: item.referenceNumber,\r\n label: item.label,\r\n debit: item.debit,\r\n credit: item.credit,\r\n runningBalance,\r\n });\r\n }\r\n\r\n glAccounts.push({\r\n account: acc,\r\n openingBalance,\r\n entries,\r\n closingBalance: runningBalance,\r\n });\r\n }\r\n\r\n return { accounts: glAccounts, period: { startDate, endDate } };\r\n}\r\n","/**\r\n * Cash Flow Statement\r\n *\r\n * Groups transactions by cashFlowCategory from account type definitions:\r\n * Operating, Investing, Financing.\r\n * Uses aggregation pipeline — no in-memory processing.\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport type { CashFlowCategory, CategoryKey } from '../types/core.js';\r\nimport type { CashFlowReport } from '../types/report.js';\r\nimport { getDateRange } from '../utils/date-range.js';\r\nimport { computeEndingBalance } from '../utils/account-helpers.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { buildItemFilters } from '../utils/filter-builder.js';\r\n\r\nexport interface CashFlowOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n}\r\n\r\nexport async function generateCashFlow(\r\n opts: CashFlowOptions,\r\n params: {\r\n organizationId?: unknown;\r\n dateOption: 'month' | 'quarter' | 'year' | 'custom';\r\n dateValue: unknown;\r\n businessName?: string;\r\n filters?: Record<string, unknown>;\r\n },\r\n): Promise<CashFlowReport> {\r\n const { AccountModel, JournalEntryModel, country, orgField } = opts;\r\n requireOrgScope(orgField, params.organizationId);\r\n const { startDate, endDate } = getDateRange(params.dateOption, params.dateValue);\r\n const itemFilters = buildItemFilters(params.filters);\r\n\r\n // Fetch accounts\r\n const q: Record<string, unknown> = { active: true };\r\n if (orgField && params.organizationId) q[orgField] = params.organizationId;\r\n const allAccounts = await AccountModel.find(q).lean() as Array<Record<string, unknown>>;\r\n\r\n // Build maps: accountId -> metadata, accountId -> raw account doc\r\n const accountCfMap = new Map<string, { category: CategoryKey; cfCategory: CashFlowCategory }>();\r\n const accountMap = new Map(allAccounts.map(a => [String(a._id), a]));\r\n const cfAccountIds: unknown[] = [];\r\n\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at || at.isGroup || at.isTotal) continue;\r\n\r\n const cf = at.cashFlowCategory;\r\n if (!cf) continue;\r\n\r\n // Normalize case: 'operating' → 'Operating'\r\n const normalized = (cf.charAt(0).toUpperCase() + cf.slice(1)) as CashFlowCategory;\r\n accountCfMap.set(String(acc._id), { category: at.category, cfCategory: normalized });\r\n cfAccountIds.push(acc._id);\r\n }\r\n\r\n // Aggregate journal items for accounts with cashFlowCategory\r\n const baseMatch: Record<string, unknown> = {\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n };\r\n if (orgField && params.organizationId) baseMatch[orgField] = params.organizationId;\r\n\r\n const results = cfAccountIds.length > 0\r\n ? await JournalEntryModel.aggregate([\r\n { $match: baseMatch },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: cfAccountIds }, ...itemFilters } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ]) as Array<{ _id: unknown; d: number; c: number }>\r\n : [];\r\n\r\n // Accumulate by category\r\n const flows: Record<CashFlowCategory, { total: number; accounts: Array<{ name: string; code: string; amount: number }> }> = {\r\n Operating: { total: 0, accounts: [] },\r\n Investing: { total: 0, accounts: [] },\r\n Financing: { total: 0, accounts: [] },\r\n };\r\n\r\n for (const r of results) {\r\n const accIdStr = String(r._id);\r\n const meta = accountCfMap.get(accIdStr);\r\n if (!meta) continue;\r\n\r\n // Net cash flow: for assets/expenses, debit increases (cash out), credit decreases (cash in)\r\n // For liabilities/equity/income, credit increases, debit decreases\r\n const amount = computeEndingBalance(meta.category, r.d, r.c);\r\n const acc = accountMap.get(accIdStr);\r\n const at = country.getAccountType(acc?.accountTypeCode as string);\r\n\r\n flows[meta.cfCategory].accounts.push({\r\n name: (acc?.name as string) ?? at?.name ?? '',\r\n code: (acc?.accountNumber as string) ?? at?.code ?? '',\r\n amount,\r\n });\r\n flows[meta.cfCategory].total += amount;\r\n }\r\n\r\n const netCashFlow = flows.Operating.total + flows.Investing.total + flows.Financing.total;\r\n\r\n const periodDisplay = `${startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${endDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}`;\r\n\r\n return {\r\n metadata: {\r\n businessName: params.businessName,\r\n generatedAt: new Date().toISOString(),\r\n periodStart: startDate.toISOString().split('T')[0],\r\n periodEnd: endDate.toISOString().split('T')[0],\r\n displayPeriod: periodDisplay,\r\n },\r\n operating: flows.Operating,\r\n investing: flows.Investing,\r\n financing: flows.Financing,\r\n netCashFlow,\r\n };\r\n}\r\n","/**\r\n * Fiscal Year Closing & Reopening\r\n *\r\n * Close: zeroes income/expense accounts, transfers net income to retained\r\n * earnings via a YEAR_END journal entry, marks the period closed.\r\n *\r\n * Reopen: validates no later period is closed, deletes the closing entry,\r\n * marks the period open again with an audit trail.\r\n *\r\n * Session management: creates an internal transaction by default.\r\n * Pass an external session to join a caller-managed transaction instead.\r\n */\r\n\r\nimport type { Model, ClientSession } from 'mongoose';\r\nimport type { CountryPack } from '../country/index.js';\r\nimport { requireOrgScope } from '../utils/tenant-guard.js';\r\nimport { Errors } from '../utils/errors.js';\r\nimport type { Logger } from '../utils/logger.js';\r\nimport { defaultLogger } from '../utils/logger.js';\r\nimport { acquireSession, finalizeSession } from '../utils/session.js';\r\n\r\nexport interface FiscalCloseOptions {\r\n AccountModel: Model<unknown>;\r\n JournalEntryModel: Model<unknown>;\r\n FiscalPeriodModel: Model<unknown>;\r\n country: CountryPack;\r\n orgField?: string;\r\n retainedEarningsCode?: string;\r\n logger?: Logger;\r\n}\r\n\r\nexport interface FiscalCloseResult {\r\n periodId: unknown;\r\n netIncome: number;\r\n closingEntryId: unknown | null;\r\n accountsClosed: number;\r\n closedAt: Date;\r\n}\r\n\r\nexport async function closeFiscalPeriod(\r\n opts: FiscalCloseOptions,\r\n params: {\r\n periodId: unknown;\r\n organizationId?: unknown;\r\n closedBy?: string;\r\n session?: ClientSession;\r\n },\r\n): Promise<FiscalCloseResult> {\r\n const {\r\n AccountModel,\r\n JournalEntryModel,\r\n FiscalPeriodModel,\r\n country,\r\n orgField,\r\n retainedEarningsCode = country.retainedEarningsCode ?? '3660',\r\n logger = defaultLogger,\r\n } = opts;\r\n const { periodId, organizationId, closedBy } = params;\r\n requireOrgScope(orgField, organizationId);\r\n\r\n const { session, ownSession } = await acquireSession(\r\n AccountModel.db,\r\n params.session,\r\n logger,\r\n );\r\n let success = false;\r\n\r\n try {\r\n const queryOpts = session ? { session } : {};\r\n\r\n // 1. Fetch and validate the fiscal period (org-scoped)\r\n const periodQuery: Record<string, unknown> = { _id: periodId };\r\n if (orgField && organizationId) periodQuery[orgField] = organizationId;\r\n const period = await FiscalPeriodModel.findOne(periodQuery, null, queryOpts).lean() as Record<string, unknown> | null;\r\n if (!period) throw Errors.notFound('Fiscal period not found');\r\n if (period.closed) throw Errors.fiscal('Fiscal period is already closed');\r\n\r\n const startDate = period.startDate as Date;\r\n const endDate = period.endDate as Date;\r\n\r\n // 2. Find all income statement accounts for this org\r\n const accountQuery: Record<string, unknown> = { active: true };\r\n if (orgField && organizationId) accountQuery[orgField] = organizationId;\r\n const allAccounts = await AccountModel.find(accountQuery, null, queryOpts).lean() as Array<Record<string, unknown>>;\r\n\r\n const isAccounts: Array<{ id: unknown; code: string; isIncome: boolean }> = [];\r\n let retainedEarningsId: unknown = null;\r\n\r\n for (const acc of allAccounts) {\r\n const at = country.getAccountType(acc.accountTypeCode as string);\r\n if (!at) continue;\r\n\r\n if (acc.accountTypeCode === retainedEarningsCode) {\r\n retainedEarningsId = acc._id;\r\n }\r\n\r\n if (at.isGroup || at.isTotal) continue;\r\n if (at.category.startsWith('Income Statement')) {\r\n isAccounts.push({\r\n id: acc._id,\r\n code: at.code,\r\n isIncome: at.category === 'Income Statement-Income',\r\n });\r\n }\r\n }\r\n\r\n if (!retainedEarningsId) {\r\n throw Errors.fiscal(\r\n `Retained earnings account (code: ${retainedEarningsCode}) not found. ` +\r\n 'Create this account before closing the fiscal period.',\r\n );\r\n }\r\n\r\n // 3. Aggregate balances for all IS accounts in the period\r\n const baseMatch: Record<string, unknown> = {\r\n state: 'posted',\r\n date: { $gte: startDate, $lte: endDate },\r\n };\r\n if (orgField && organizationId) baseMatch[orgField] = organizationId;\r\n\r\n const isIds = isAccounts.map(a => a.id);\r\n const balances = isIds.length > 0\r\n ? await JournalEntryModel.aggregate([\r\n { $match: baseMatch },\r\n { $unwind: '$journalItems' },\r\n { $match: { 'journalItems.account': { $in: isIds } } },\r\n { $group: { _id: '$journalItems.account', d: { $sum: '$journalItems.debit' }, c: { $sum: '$journalItems.credit' } } },\r\n ], queryOpts) as Array<{ _id: unknown; d: number; c: number }>\r\n : [];\r\n\r\n // 4. Build closing journal entry items\r\n const closingItems: Array<{ account: unknown; debit: number; credit: number; label: string }> = [];\r\n let netIncome = 0;\r\n\r\n const balMap = new Map(balances.map(b => [String(b._id), b]));\r\n\r\n for (const acc of isAccounts) {\r\n const bal = balMap.get(String(acc.id));\r\n if (!bal) continue;\r\n\r\n const net = bal.c - bal.d;\r\n if (net === 0) continue;\r\n\r\n closingItems.push({\r\n account: acc.id,\r\n debit: net > 0 ? net : 0,\r\n credit: net < 0 ? Math.abs(net) : 0,\r\n label: `Close ${acc.code}`,\r\n });\r\n\r\n netIncome += net;\r\n }\r\n\r\n let closingEntryId: unknown = null;\r\n\r\n if (closingItems.length > 0) {\r\n closingItems.push({\r\n account: retainedEarningsId,\r\n debit: netIncome < 0 ? Math.abs(netIncome) : 0,\r\n credit: netIncome > 0 ? netIncome : 0,\r\n label: 'Transfer net income to retained earnings',\r\n });\r\n\r\n const totalDebit = closingItems.reduce((s, i) => s + i.debit, 0);\r\n const totalCredit = closingItems.reduce((s, i) => s + i.credit, 0);\r\n\r\n const closingEntryData: Record<string, unknown> = {\r\n journalType: 'YEAR_END',\r\n state: 'posted',\r\n date: endDate,\r\n label: `Fiscal year closing – ${(period.name as string) ?? 'Period'}`,\r\n journalItems: closingItems,\r\n totalDebit,\r\n totalCredit,\r\n };\r\n if (orgField && organizationId) closingEntryData[orgField] = organizationId;\r\n\r\n const [closingEntry] = await JournalEntryModel.create([closingEntryData], queryOpts);\r\n closingEntryId = closingEntry._id;\r\n }\r\n\r\n // 5. Mark the period as closed (org-scoped)\r\n const closedAt = new Date();\r\n await FiscalPeriodModel.findOneAndUpdate(\r\n periodQuery,\r\n { closed: true, closedAt, closedBy: closedBy ?? null, closingEntryId },\r\n queryOpts,\r\n );\r\n\r\n const result: FiscalCloseResult = {\r\n periodId,\r\n netIncome,\r\n closingEntryId,\r\n accountsClosed: closingItems.length - (closingItems.length > 0 ? 1 : 0),\r\n closedAt,\r\n };\r\n\r\n success = true;\r\n return result;\r\n } finally {\r\n await finalizeSession(session, ownSession, success);\r\n }\r\n}\r\n\r\n// ── Reopen ────────────────────────────────────────────────────────────────\r\n\r\nexport interface FiscalReopenResult {\r\n periodId: unknown;\r\n deletedEntryId: unknown | null;\r\n reopenedAt: Date;\r\n}\r\n\r\nexport async function reopenFiscalPeriod(\r\n opts: Pick<FiscalCloseOptions, 'JournalEntryModel' | 'FiscalPeriodModel'> & {\r\n orgField?: string;\r\n logger?: Logger;\r\n /** Any model on the same connection — used to start sessions */\r\n AccountModel?: Model<unknown>;\r\n },\r\n params: {\r\n periodId: unknown;\r\n organizationId?: unknown;\r\n reopenedBy?: string;\r\n session?: ClientSession;\r\n },\r\n): Promise<FiscalReopenResult> {\r\n const { JournalEntryModel, FiscalPeriodModel, orgField, logger = defaultLogger } = opts;\r\n const { periodId, organizationId, reopenedBy } = params;\r\n requireOrgScope(orgField, organizationId);\r\n\r\n // Use any available model's db connection for session creation\r\n const db = (opts.AccountModel ?? FiscalPeriodModel).db;\r\n const { session, ownSession } = await acquireSession(db, params.session, logger);\r\n let success = false;\r\n\r\n try {\r\n const queryOpts = session ? { session } : {};\r\n\r\n // 1. Fetch and validate the period (org-scoped)\r\n const periodQuery: Record<string, unknown> = { _id: periodId };\r\n if (orgField && organizationId) periodQuery[orgField] = organizationId;\r\n const period = await FiscalPeriodModel.findOne(periodQuery, null, queryOpts).lean() as Record<string, unknown> | null;\r\n if (!period) throw Errors.notFound('Fiscal period not found');\r\n if (!period.closed) throw Errors.fiscal('Fiscal period is not closed');\r\n\r\n // 2. Block if a later period is already closed (prevents cascade corruption)\r\n const laterQuery: Record<string, unknown> = {\r\n closed: true,\r\n startDate: { $gt: period.endDate },\r\n };\r\n if (orgField && organizationId) laterQuery[orgField] = organizationId;\r\n\r\n const laterClosed = await FiscalPeriodModel.findOne(laterQuery, null, queryOpts).lean();\r\n if (laterClosed) {\r\n throw Errors.fiscal(\r\n 'Cannot reopen: a later fiscal period is already closed. Reopen later periods first.',\r\n );\r\n }\r\n\r\n // 3. Delete the closing journal entry (if one was created)\r\n const closingEntryId = period.closingEntryId ?? null;\r\n if (closingEntryId) {\r\n await JournalEntryModel.findByIdAndDelete(closingEntryId, queryOpts);\r\n }\r\n\r\n // 4. Mark the period as reopened (org-scoped)\r\n const reopenedAt = new Date();\r\n await FiscalPeriodModel.findOneAndUpdate(\r\n periodQuery,\r\n {\r\n closed: false,\r\n closedAt: null,\r\n closedBy: null,\r\n closingEntryId: null,\r\n reopenedAt,\r\n reopenedBy: reopenedBy ?? null,\r\n },\r\n queryOpts,\r\n );\r\n\r\n const result: FiscalReopenResult = {\r\n periodId,\r\n deletedEntryId: closingEntryId,\r\n reopenedAt,\r\n };\r\n\r\n success = true;\r\n return result;\r\n } finally {\r\n await finalizeSession(session, ownSession, success);\r\n }\r\n}\r\n"],"mappings":"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"}