import * as plugins from './plugins.js'; import { getDbSync } from './skr.database.js'; import { Account } from './skr.classes.account.js'; import type { TSKRType, TTransactionStatus, ITransactionData, } from './skr.types.js'; const { SmartDataDbDoc, svDb, unI, index, searchable } = plugins.smartdata; @plugins.smartdata.Collection(() => getDbSync()) export class Transaction extends SmartDataDbDoc { @unI() public id: string; @svDb() @index() public transactionNumber: string; @svDb() @index() public date: Date; @svDb() @index() public debitAccount: string; @svDb() @index() public creditAccount: string; @svDb() public amount: number; @svDb() @searchable() public description: string; @svDb() @index() public reference: string; @svDb() @index() public skrType: TSKRType; @svDb() public vatAmount: number; @svDb() public costCenter: string; @svDb() @index() public status: TTransactionStatus; @svDb() public reversalOf: string; @svDb() public reversedBy: string; @svDb() @index() public period: string; // Format: YYYY-MM @svDb() public fiscalYear: number; @svDb() public createdAt: Date; @svDb() public postedAt: Date; @svDb() public createdBy: string; constructor(data?: Partial) { super(); if (data) { this.id = plugins.smartunique.shortId(); this.transactionNumber = this.generateTransactionNumber(); this.date = data.date || new Date(); this.debitAccount = data.debitAccount || ''; this.creditAccount = data.creditAccount || ''; this.amount = data.amount || 0; this.description = data.description || ''; this.reference = data.reference || ''; this.skrType = data.skrType || 'SKR03'; this.vatAmount = data.vatAmount || 0; this.costCenter = data.costCenter || ''; this.status = 'pending'; this.reversalOf = ''; this.reversedBy = ''; // Set period and fiscal year const transDate = new Date(this.date); this.period = `${transDate.getFullYear()}-${String(transDate.getMonth() + 1).padStart(2, '0')}`; this.fiscalYear = transDate.getFullYear(); this.createdAt = new Date(); this.postedAt = null; this.createdBy = 'system'; } } private generateTransactionNumber(): string { const timestamp = Date.now(); const random = Math.floor(Math.random() * 1000); return `TXN-${timestamp}-${random}`; } public static async createTransaction( data: ITransactionData, ): Promise { const transaction = new Transaction(data); await transaction.validateAndPost(); return transaction; } public static async getTransactionById( id: string, ): Promise { const transaction = await Transaction.getInstance({ id }); return transaction; } public static async getTransactionsByAccount( accountNumber: string, skrType: TSKRType, ): Promise { const transactionsDebit = await Transaction.getInstances({ debitAccount: accountNumber, skrType, status: 'posted', }); const transactionsCredit = await Transaction.getInstances({ creditAccount: accountNumber, skrType, status: 'posted', }); const transactions = [...transactionsDebit, ...transactionsCredit]; return transactions; } public static async getTransactionsByPeriod( period: string, skrType: TSKRType, ): Promise { const transactions = await Transaction.getInstances({ period, skrType, status: 'posted', }); return transactions; } public static async getTransactionsByDateRange( dateFrom: Date, dateTo: Date, skrType: TSKRType, ): Promise { const allTransactions = await Transaction.getInstances({ skrType, status: 'posted', }); const transactions = allTransactions.filter( (t) => t.date >= dateFrom && t.date <= dateTo, ); return transactions; } public async validateAndPost(): Promise { // Validate transaction await this.validateTransaction(); // Update account balances await this.updateAccountBalances(); // Mark as posted this.status = 'posted'; this.postedAt = new Date(); await this.save(); } private async validateTransaction(): Promise { // Check if accounts exist const debitAccount = await Account.getAccountByNumber( this.debitAccount, this.skrType, ); const creditAccount = await Account.getAccountByNumber( this.creditAccount, this.skrType, ); if (!debitAccount) { throw new Error( `Debit account ${this.debitAccount} not found for ${this.skrType}`, ); } if (!creditAccount) { throw new Error( `Credit account ${this.creditAccount} not found for ${this.skrType}`, ); } // Check if accounts are active if (!debitAccount.isActive) { throw new Error(`Debit account ${this.debitAccount} is not active`); } if (!creditAccount.isActive) { throw new Error(`Credit account ${this.creditAccount} is not active`); } // Validate amount if (this.amount <= 0) { throw new Error('Transaction amount must be greater than zero'); } // Check for same account if (this.debitAccount === this.creditAccount) { throw new Error('Debit and credit accounts cannot be the same'); } } private async updateAccountBalances(): Promise { const debitAccount = await Account.getAccountByNumber( this.debitAccount, this.skrType, ); const creditAccount = await Account.getAccountByNumber( this.creditAccount, this.skrType, ); if (debitAccount) { await debitAccount.updateBalance(this.amount, 0); } if (creditAccount) { await creditAccount.updateBalance(0, this.amount); } } public async reverseTransaction(): Promise { if (this.status !== 'posted') { throw new Error('Can only reverse posted transactions'); } if (this.reversedBy) { throw new Error('Transaction has already been reversed'); } // Create reversal transaction const reversalData: ITransactionData = { date: new Date(), debitAccount: this.creditAccount, // Swap accounts creditAccount: this.debitAccount, // Swap accounts amount: this.amount, description: `Reversal of ${this.transactionNumber}: ${this.description}`, reference: `REV-${this.transactionNumber}`, skrType: this.skrType, vatAmount: this.vatAmount, costCenter: this.costCenter, }; const reversalTransaction = new Transaction(reversalData); reversalTransaction.reversalOf = this.id; await reversalTransaction.validateAndPost(); // Update original transaction this.reversedBy = reversalTransaction.id; this.status = 'reversed'; await this.save(); return reversalTransaction; } public async beforeSave(): Promise { // Additional validation before saving if (!this.debitAccount || !this.creditAccount) { throw new Error('Both debit and credit accounts are required'); } if (!this.date) { throw new Error('Transaction date is required'); } if (!this.description) { throw new Error('Transaction description is required'); } } }