Files
skr/ts/skr.classes.transaction.ts
Juergen Kunz 8a9056e767
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
feat(core): initial release of SKR03/SKR04 German accounting standards implementation
- Complete implementation of German standard charts of accounts
- SKR03 (Process Structure Principle) for trading/service companies
- SKR04 (Financial Classification Principle) for manufacturing companies
- Double-entry bookkeeping with MongoDB persistence
- Comprehensive reporting suite with DATEV export
- Full TypeScript support and type safety
2025-08-09 12:00:40 +00:00

301 lines
7.3 KiB
TypeScript

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<Transaction, Transaction> {
@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<ITransactionData>) {
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<Transaction> {
const transaction = new Transaction(data);
await transaction.validateAndPost();
return transaction;
}
public static async getTransactionById(
id: string,
): Promise<Transaction | null> {
const transaction = await Transaction.getInstance({ id });
return transaction;
}
public static async getTransactionsByAccount(
accountNumber: string,
skrType: TSKRType,
): Promise<Transaction[]> {
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<Transaction[]> {
const transactions = await Transaction.getInstances({
period,
skrType,
status: 'posted',
});
return transactions;
}
public static async getTransactionsByDateRange(
dateFrom: Date,
dateTo: Date,
skrType: TSKRType,
): Promise<Transaction[]> {
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<void> {
// 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<void> {
// 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<void> {
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<Transaction> {
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<void> {
// 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');
}
}
}