import * as plugins from './plugins.js'; import { getDb, getDbSync } from './skr.database.js'; import type { TAccountType, TSKRType, IAccountData } from './skr.types.js'; const { SmartDataDbDoc, svDb, unI, index, searchable } = plugins.smartdata; @plugins.smartdata.Collection(() => getDbSync()) export class Account extends SmartDataDbDoc { @unI() public id: string; @svDb() @index() public accountNumber: string; @svDb() @searchable() public accountName: string; @svDb() @index() public accountClass: number; @svDb() public accountGroup: number; @svDb() public accountSubgroup: number; @svDb() public accountType: TAccountType; @svDb() @index() public skrType: TSKRType; @svDb() @searchable() public description: string; @svDb() public vatRate: number; @svDb() public balance: number; @svDb() public debitTotal: number; @svDb() public creditTotal: number; @svDb() public isActive: boolean; @svDb() public isSystemAccount: boolean; @svDb() public isAutomaticAccount: boolean; @svDb() public createdAt: Date; @svDb() public updatedAt: Date; constructor(data?: Partial) { super(); if (data) { this.id = plugins.smartunique.shortId(); this.accountNumber = data.accountNumber || ''; this.accountName = data.accountName || ''; this.accountClass = data.accountClass || 0; this.accountType = data.accountType || 'asset'; this.skrType = data.skrType || 'SKR03'; this.description = data.description || ''; this.vatRate = data.vatRate || 0; this.isActive = data.isActive !== undefined ? data.isActive : true; // Parse account structure from number if (this.accountNumber && this.accountNumber.length === 4) { this.accountClass = parseInt(this.accountNumber[0]); this.accountGroup = parseInt(this.accountNumber[1]); this.accountSubgroup = parseInt(this.accountNumber[2]); } else { this.accountGroup = 0; this.accountSubgroup = 0; } this.balance = 0; this.debitTotal = 0; this.creditTotal = 0; this.isSystemAccount = true; this.isAutomaticAccount = data.isAutomaticAccount || false; this.createdAt = new Date(); this.updatedAt = new Date(); } } public static async createAccount(data: IAccountData): Promise { const account = new Account(data); await account.save(); return account; } public static async getAccountByNumber( accountNumber: string, skrType: TSKRType, ): Promise { const account = await Account.getInstance({ accountNumber, skrType, }); return account; } public static async getAccountsByClass( accountClass: number, skrType: TSKRType, ): Promise { const accounts = await Account.getInstances({ accountClass, skrType, isActive: true, }); return accounts; } public static async getAccountsByType( accountType: TAccountType, skrType: TSKRType, ): Promise { const accounts = await Account.getInstances({ accountType, skrType, isActive: true, }); return accounts; } public static async searchAccounts( searchTerm: string, skrType?: TSKRType, ): Promise { const query: any = {}; if (skrType) { query.skrType = skrType; } const accounts = await Account.getInstances(query); // Filter by search term const lowerSearchTerm = searchTerm.toLowerCase(); return accounts.filter( (account) => account.accountNumber.includes(searchTerm) || account.accountName.toLowerCase().includes(lowerSearchTerm) || account.description.toLowerCase().includes(lowerSearchTerm), ); } /** * Check if account number is in debtor range (10000-69999) * Debtor accounts (Debitorenkonten) are individual customer accounts */ public static isInDebtorRange(accountNumber: string): boolean { const num = parseInt(accountNumber); return num >= 10000 && num <= 69999; } /** * Check if account number is in creditor range (70000-99999) * Creditor accounts (Kreditorenkonten) are individual vendor accounts */ public static isInCreditorRange(accountNumber: string): boolean { const num = parseInt(accountNumber); return num >= 70000 && num <= 99999; } /** * Check if account is an automatic account (Automatikkonto) * Automatic accounts like 1400/1600 cannot be posted to directly */ public static isAutomaticAccount(accountNumber: string, skrType: TSKRType): boolean { // SKR03: 1400 (Forderungen), 1600 (Verbindlichkeiten) // SKR04: 1400 (Forderungen), 1600 (Verbindlichkeiten) // Note: In SKR04, 3300 is "Fahrzeugkosten" (vehicle costs), NOT an automatic account if (skrType === 'SKR03') { return accountNumber === '1400' || accountNumber === '1600'; } else { return accountNumber === '1400' || accountNumber === '1600'; } } /** * Validate account for posting - throws error if account cannot be posted to */ public static async validateAccountForPosting( accountNumber: string, skrType: TSKRType, ): Promise { // Check if automatic account if (Account.isAutomaticAccount(accountNumber, skrType)) { throw new Error( `Account ${accountNumber} is an automatic account (Automatikkonto) and cannot be posted to directly. ` + `Use debtor accounts (10000-69999) or creditor accounts (70000-99999) instead.` ); } // Get account to verify it exists const account = await Account.getAccountByNumber(accountNumber, skrType); if (!account) { throw new Error( `Account ${accountNumber} not found in ${skrType}. ` + `Please create the account before posting.` ); } // Check if account is active if (!account.isActive) { throw new Error( `Account ${accountNumber} is inactive and cannot be posted to.` ); } } /** * Check if this account instance is a debtor account */ public isDebtorAccount(): boolean { return Account.isInDebtorRange(this.accountNumber); } /** * Check if this account instance is a creditor account */ public isCreditorAccount(): boolean { return Account.isInCreditorRange(this.accountNumber); } public async updateBalance( debitAmount: number = 0, creditAmount: number = 0, ): Promise { this.debitTotal += debitAmount; this.creditTotal += creditAmount; // Calculate balance based on account type switch (this.accountType) { case 'asset': case 'expense': // Normal debit accounts this.balance = this.debitTotal - this.creditTotal; break; case 'liability': case 'equity': case 'revenue': // Normal credit accounts this.balance = this.creditTotal - this.debitTotal; break; } this.updatedAt = new Date(); await this.save(); } public async deactivate(): Promise { this.isActive = false; this.updatedAt = new Date(); await this.save(); } public async activate(): Promise { this.isActive = true; this.updatedAt = new Date(); await this.save(); } public getNormalBalance(): 'debit' | 'credit' { switch (this.accountType) { case 'asset': case 'expense': return 'debit'; case 'liability': case 'equity': case 'revenue': return 'credit'; } } public async beforeSave(): Promise { // Validate account number format const accountLength = this.accountNumber?.length || 0; if (!this.accountNumber || (accountLength !== 4 && accountLength !== 5)) { throw new Error( `Invalid account number format: ${this.accountNumber}. Must be 4 digits (standard SKR) or 5 digits (debtor/creditor).`, ); } // Validate account number is numeric if (!/^\d{4,5}$/.test(this.accountNumber)) { throw new Error( `Account number must contain only digits: ${this.accountNumber}`, ); } // For 5-digit accounts, validate they are in debtor (10000-69999) or creditor (70000-99999) ranges if (accountLength === 5) { const accountNum = parseInt(this.accountNumber); const isDebtor = accountNum >= 10000 && accountNum <= 69999; const isCreditor = accountNum >= 70000 && accountNum <= 99999; if (!isDebtor && !isCreditor) { throw new Error( `5-digit account number ${this.accountNumber} must be in debtor range (10000-69999) or creditor range (70000-99999).`, ); } } // Validate account class matches first digit const firstDigit = parseInt(this.accountNumber[0]); if (this.accountClass !== firstDigit) { throw new Error( `Account class ${this.accountClass} does not match account number ${this.accountNumber}`, ); } // Validate SKR type if (this.skrType !== 'SKR03' && this.skrType !== 'SKR04') { throw new Error(`Invalid SKR type: ${this.skrType}`); } // Mark automatic accounts (Automatikkonten) // These are summary accounts that cannot be posted to directly if (Account.isAutomaticAccount(this.accountNumber, this.skrType)) { this.isAutomaticAccount = true; } } }