import * as plugins from './plugins.js'; import { getDb, closeDb } from './skr.database.js'; import { Account } from './skr.classes.account.js'; import { Transaction } from './skr.classes.transaction.js'; import { JournalEntry } from './skr.classes.journalentry.js'; import { SKR03_ACCOUNTS, SKR03_ACCOUNT_CLASSES } from './skr03.data.js'; import { SKR04_ACCOUNTS, SKR04_ACCOUNT_CLASSES } from './skr04.data.js'; import type { IDatabaseConfig, TSKRType, IAccountData, IAccountFilter, ITransactionFilter, ITransactionData, IJournalEntry, } from './skr.types.js'; export class ChartOfAccounts { private logger: plugins.smartlog.Smartlog; private initialized: boolean = false; private skrType: TSKRType | null = null; constructor(private config?: IDatabaseConfig) { this.logger = new plugins.smartlog.Smartlog({ logContext: { company: 'fin.cx', companyunit: 'skr', containerName: 'ChartOfAccounts', environment: 'local', runtime: 'node', zone: 'local', }, }); this.logger.enableConsole(); } /** * Initialize the database connection */ public async init(): Promise { if (this.initialized) { this.logger.log('info', 'ChartOfAccounts already initialized'); return; } if (!this.config) { throw new Error('Database configuration required for initialization'); } await getDb(this.config); this.initialized = true; this.logger.log('info', 'ChartOfAccounts initialized successfully'); } /** * Initialize SKR03 chart of accounts */ public async initializeSKR03(): Promise { await this.init(); this.logger.log('info', 'Initializing SKR03 chart of accounts'); // Check if SKR03 accounts already exist const existingAccounts = await Account.getInstances({ skrType: 'SKR03' }); if (existingAccounts.length > 0) { this.logger.log( 'info', `SKR03 already initialized with ${existingAccounts.length} accounts`, ); this.skrType = 'SKR03'; return; } // Create all SKR03 accounts const accounts: Account[] = []; for (const accountData of SKR03_ACCOUNTS) { const account = await Account.createAccount(accountData); accounts.push(account); } this.skrType = 'SKR03'; this.logger.log( 'info', `Successfully initialized SKR03 with ${accounts.length} accounts`, ); } /** * Initialize SKR04 chart of accounts */ public async initializeSKR04(): Promise { await this.init(); this.logger.log('info', 'Initializing SKR04 chart of accounts'); // Check if SKR04 accounts already exist const existingAccounts = await Account.getInstances({ skrType: 'SKR04' }); if (existingAccounts.length > 0) { this.logger.log( 'info', `SKR04 already initialized with ${existingAccounts.length} accounts`, ); this.skrType = 'SKR04'; return; } // Create all SKR04 accounts const accounts: Account[] = []; for (const accountData of SKR04_ACCOUNTS) { const account = await Account.createAccount(accountData); accounts.push(account); } this.skrType = 'SKR04'; this.logger.log( 'info', `Successfully initialized SKR04 with ${accounts.length} accounts`, ); } /** * Get the current SKR type */ public getSKRType(): TSKRType | null { return this.skrType; } /** * Set the active SKR type */ public setSKRType(skrType: TSKRType): void { this.skrType = skrType; } /** * Get account by number */ public async getAccountByNumber( accountNumber: string, ): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } return await Account.getAccountByNumber(accountNumber, this.skrType); } /** * Get accounts by class */ public async getAccountsByClass(accountClass: number): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } return await Account.getAccountsByClass(accountClass, this.skrType); } /** * Get accounts by type */ public async getAccountsByType( accountType: IAccountData['accountType'], ): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } return await Account.getAccountsByType(accountType, this.skrType); } /** * Create a custom account */ public async createCustomAccount( accountData: Partial, ): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } // Ensure the account uses the current SKR type const fullAccountData: IAccountData = { accountNumber: accountData.accountNumber || '', accountName: accountData.accountName || '', accountClass: accountData.accountClass || 0, accountType: accountData.accountType || 'asset', skrType: this.skrType, description: accountData.description, vatRate: accountData.vatRate, isActive: accountData.isActive !== undefined ? accountData.isActive : true, }; // Validate account number doesn't already exist const existing = await this.getAccountByNumber( fullAccountData.accountNumber, ); if (existing) { throw new Error( `Account ${fullAccountData.accountNumber} already exists`, ); } return await Account.createAccount(fullAccountData); } /** * Update an existing account */ public async updateAccount( accountNumber: string, updates: Partial, ): Promise { const account = await this.getAccountByNumber(accountNumber); if (!account) { throw new Error(`Account ${accountNumber} not found`); } // Apply updates if (updates.accountName !== undefined) account.accountName = updates.accountName; if (updates.description !== undefined) account.description = updates.description; if (updates.vatRate !== undefined) account.vatRate = updates.vatRate; if (updates.isActive !== undefined) account.isActive = updates.isActive; account.updatedAt = new Date(); await account.save(); return account; } /** * Delete a custom account (only non-system accounts) */ public async deleteAccount(accountNumber: string): Promise { const account = await this.getAccountByNumber(accountNumber); if (!account) { throw new Error(`Account ${accountNumber} not found`); } if (account.isSystemAccount) { throw new Error(`Cannot delete system account ${accountNumber}`); } // Check if account has transactions const transactions = await Transaction.getTransactionsByAccount( accountNumber, account.skrType, ); if (transactions.length > 0) { throw new Error( `Cannot delete account ${accountNumber} with existing transactions`, ); } await account.delete(); } /** * Search accounts */ public async searchAccounts(searchTerm: string): Promise { return await Account.searchAccounts(searchTerm, this.skrType); } /** * Get all accounts */ public async getAllAccounts(filter?: IAccountFilter): Promise { const query: any = {}; if (this.skrType) { query.skrType = this.skrType; } if (filter) { if (filter.accountClass !== undefined) query.accountClass = filter.accountClass; if (filter.accountType !== undefined) query.accountType = filter.accountType; if (filter.isActive !== undefined) query.isActive = filter.isActive; } const accounts = await Account.getInstances(query); // Apply text search if provided if (filter?.searchTerm) { const lowerSearchTerm = filter.searchTerm.toLowerCase(); return accounts.filter( (account) => account.accountNumber.includes(filter.searchTerm) || account.accountName.toLowerCase().includes(lowerSearchTerm) || account.description.toLowerCase().includes(lowerSearchTerm), ); } return accounts; } /** * Post a simple transaction */ public async postTransaction( transactionData: ITransactionData, ): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } // Ensure the transaction uses the current SKR type const fullTransactionData: ITransactionData = { ...transactionData, skrType: this.skrType, }; return await Transaction.createTransaction(fullTransactionData); } /** * Post a journal entry */ public async postJournalEntry( journalData: IJournalEntry, ): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } // Ensure the journal entry uses the current SKR type const fullJournalData: IJournalEntry = { ...journalData, skrType: this.skrType, }; const journalEntry = await JournalEntry.createJournalEntry(fullJournalData); await journalEntry.post(); return journalEntry; } /** * Get transactions for an account */ public async getAccountTransactions( accountNumber: string, ): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } return await Transaction.getTransactionsByAccount( accountNumber, this.skrType, ); } /** * Get transactions by filter */ public async getTransactions( filter?: ITransactionFilter, ): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } const query: any = { skrType: this.skrType, status: 'posted', }; if (filter) { if (filter.dateFrom || filter.dateTo) { query.date = {}; if (filter.dateFrom) query.date.$gte = filter.dateFrom; if (filter.dateTo) query.date.$lte = filter.dateTo; } if (filter.accountNumber) { query.$or = [ { debitAccount: filter.accountNumber }, { creditAccount: filter.accountNumber }, ]; } if (filter.minAmount || filter.maxAmount) { query.amount = {}; if (filter.minAmount) query.amount.$gte = filter.minAmount; if (filter.maxAmount) query.amount.$lte = filter.maxAmount; } } const transactions = await Transaction.getInstances(query); // Apply text search if provided if (filter?.searchTerm) { const lowerSearchTerm = filter.searchTerm.toLowerCase(); return transactions.filter( (transaction) => transaction.description.toLowerCase().includes(lowerSearchTerm) || transaction.reference.toLowerCase().includes(lowerSearchTerm), ); } return transactions; } /** * Reverse a transaction */ public async reverseTransaction(transactionId: string): Promise { const transaction = await Transaction.getTransactionById(transactionId); if (!transaction) { throw new Error(`Transaction ${transactionId} not found`); } return await transaction.reverseTransaction(); } /** * Get account class description */ public getAccountClassDescription(accountClass: number): string { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } const classes = this.skrType === 'SKR03' ? SKR03_ACCOUNT_CLASSES : SKR04_ACCOUNT_CLASSES; return ( classes[accountClass as keyof typeof classes] || `Class ${accountClass}` ); } /** * Import accounts from CSV */ public async importAccountsFromCSV(csvContent: string): Promise { if (!this.skrType) { throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.'); } const lines = csvContent.split('\n').filter((line) => line.trim()); let importedCount = 0; for (const line of lines) { // Parse CSV line (expecting format: "account";"name";"description";"type";"active") const parts = line .split(';') .map((part) => part.replace(/"/g, '').trim()); if (parts.length >= 5) { const accountData: IAccountData = { accountNumber: parts[0], accountName: parts[1], accountClass: parseInt(parts[0][0]), accountType: parts[3] as IAccountData['accountType'], skrType: this.skrType, description: parts[2], isActive: parts[4].toLowerCase() === 'standard' || parts[4].toLowerCase() === 'active', }; try { await this.createCustomAccount(accountData); importedCount++; } catch (error) { this.logger.log( 'warn', `Failed to import account ${parts[0]}: ${error.message}`, ); } } } return importedCount; } /** * Export accounts to CSV */ public async exportAccountsToCSV(): Promise { const accounts = await this.getAllAccounts(); const csvLines: string[] = []; csvLines.push('"Account";"Name";"Description";"Type";"Active"'); for (const account of accounts) { csvLines.push( `"${account.accountNumber}";"${account.accountName}";"${account.description}";"${account.accountType}";"${account.isActive ? 'Active' : 'Inactive'}"`, ); } return csvLines.join('\n'); } /** * Close the database connection */ public async close(): Promise { await closeDb(); this.initialized = false; this.logger.log('info', 'ChartOfAccounts closed'); } }