@fin.cx/skr 1.3.0 → 2.0.0
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.
- package/.smartconfig.json +33 -6
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/core.accountpolicy.d.ts +67 -0
- package/dist_ts/core/core.accountpolicy.js +91 -0
- package/dist_ts/core/core.bukeys.d.ts +38 -0
- package/dist_ts/core/core.bukeys.js +117 -0
- package/dist_ts/core/core.datev.captions.d.ts +7 -0
- package/dist_ts/core/core.datev.captions.js +134 -0
- package/dist_ts/core/core.datev.d.ts +47 -0
- package/dist_ts/core/core.datev.js +129 -0
- package/dist_ts/core/core.hash.d.ts +27 -0
- package/dist_ts/core/core.hash.js +72 -0
- package/dist_ts/core/core.plugins.d.ts +3 -0
- package/dist_ts/core/core.plugins.js +9 -0
- package/dist_ts/core/core.recipes.d.ts +118 -0
- package/dist_ts/core/core.recipes.js +504 -0
- package/dist_ts/core/core.taxscenario.d.ts +27 -0
- package/dist_ts/core/core.taxscenario.js +85 -0
- package/dist_ts/core/core.types.d.ts +48 -0
- package/dist_ts/core/core.types.js +2 -0
- package/dist_ts/core/core.validate.d.ts +17 -0
- package/dist_ts/core/core.validate.js +89 -0
- package/dist_ts/core/index.d.ts +15 -0
- package/dist_ts/core/index.js +16 -0
- package/dist_ts/index.d.ts +15 -0
- package/dist_ts/index.js +10 -1
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +4 -2
- package/dist_ts/skr.api.d.ts +91 -22
- package/dist_ts/skr.api.js +357 -42
- package/dist_ts/skr.balances.d.ts +28 -0
- package/dist_ts/skr.balances.js +96 -0
- package/dist_ts/skr.classes.journalentry.d.ts +19 -3
- package/dist_ts/skr.classes.journalentry.js +67 -4
- package/dist_ts/skr.classes.reports.js +49 -102
- package/dist_ts/skr.database.d.ts +2 -0
- package/dist_ts/skr.database.js +1 -1
- package/dist_ts/skr.errors.d.ts +18 -0
- package/dist_ts/skr.errors.js +34 -0
- package/dist_ts/skr.export.js +4 -7
- package/dist_ts/skr.invoice.adapter.d.ts +15 -1
- package/dist_ts/skr.invoice.adapter.js +253 -41
- package/dist_ts/skr.invoice.booking.js +14 -28
- package/dist_ts/skr.migrate.d.ts +20 -0
- package/dist_ts/skr.migrate.js +223 -0
- package/dist_ts/skr.period.d.ts +47 -0
- package/dist_ts/skr.period.js +95 -0
- package/dist_ts/skr.posting.d.ts +74 -0
- package/dist_ts/skr.posting.js +209 -0
- package/dist_ts/skr.security.d.ts +6 -9
- package/dist_ts/skr.security.js +16 -89
- package/dist_ts/skr.sequence.d.ts +32 -0
- package/dist_ts/skr.sequence.js +51 -0
- package/dist_ts/skr.txview.d.ts +46 -0
- package/dist_ts/skr.txview.js +235 -0
- package/dist_ts/skr.types.d.ts +35 -1
- package/dist_ts/skr.verify.d.ts +13 -0
- package/dist_ts/skr.verify.js +84 -0
- package/package.json +21 -24
- package/readme.hints.md +37 -0
- package/readme.md +94 -1
- package/readme.plan.md +180 -243
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/core.accountpolicy.ts +154 -0
- package/ts/core/core.bukeys.ts +146 -0
- package/ts/core/core.datev.captions.ts +133 -0
- package/ts/core/core.datev.ts +174 -0
- package/ts/core/core.hash.ts +76 -0
- package/ts/core/core.plugins.ts +9 -0
- package/ts/core/core.recipes.ts +659 -0
- package/ts/core/core.taxscenario.ts +156 -0
- package/ts/core/core.types.ts +74 -0
- package/ts/core/core.validate.ts +119 -0
- package/ts/core/index.ts +15 -0
- package/ts/index.ts +31 -0
- package/ts/plugins.ts +4 -0
- package/ts/skr.api.ts +432 -46
- package/ts/skr.balances.ts +127 -0
- package/ts/skr.classes.journalentry.ts +54 -5
- package/ts/skr.classes.reports.ts +54 -126
- package/ts/skr.database.ts +3 -0
- package/ts/skr.errors.ts +44 -0
- package/ts/skr.export.ts +3 -6
- package/ts/skr.invoice.adapter.ts +291 -45
- package/ts/skr.invoice.booking.ts +22 -34
- package/ts/skr.migrate.ts +268 -0
- package/ts/skr.period.ts +139 -0
- package/ts/skr.posting.ts +273 -0
- package/ts/skr.security.ts +18 -93
- package/ts/skr.sequence.ts +89 -0
- package/ts/skr.txview.ts +268 -0
- package/ts/skr.types.ts +37 -1
- package/ts/skr.verify.ts +99 -0
- /package/{license.md → license} +0 -0
package/ts/skr.api.ts
CHANGED
|
@@ -12,6 +12,23 @@ import { AccountsExporter } from './skr.export.accounts.js';
|
|
|
12
12
|
import { BalancesExporter } from './skr.export.balances.js';
|
|
13
13
|
import { PdfReportGenerator, type IPdfReportOptions } from './skr.export.pdf.js';
|
|
14
14
|
import { SecurityManager, type ISigningOptions } from './skr.security.js';
|
|
15
|
+
import * as core from './core/index.js';
|
|
16
|
+
import { JournalPoster, type IPostDraftOptions } from './skr.posting.js';
|
|
17
|
+
import { festschreibePeriod, getPeriodState, type ILedgerPeriodState } from './skr.period.js';
|
|
18
|
+
import { verifyHashChain, type IChainVerificationResult } from './skr.verify.js';
|
|
19
|
+
import {
|
|
20
|
+
aggregateTrialBalance,
|
|
21
|
+
aggregateAccountBalance,
|
|
22
|
+
type IBalanceQueryOptions,
|
|
23
|
+
type ITrialBalanceCents,
|
|
24
|
+
type IAccountBalanceCents,
|
|
25
|
+
} from './skr.balances.js';
|
|
26
|
+
import {
|
|
27
|
+
transactionDataToDraft,
|
|
28
|
+
journalInputToDraft,
|
|
29
|
+
entryToTransactionView,
|
|
30
|
+
accountLineViews,
|
|
31
|
+
} from './skr.txview.js';
|
|
15
32
|
import { InvoiceAdapter } from './skr.invoice.adapter.js';
|
|
16
33
|
import { InvoiceStorage } from './skr.invoice.storage.js';
|
|
17
34
|
import { InvoiceBookingEngine, type IBookingOptions, type IBookingResult } from './skr.invoice.booking.js';
|
|
@@ -36,6 +53,7 @@ import type {
|
|
|
36
53
|
IIncomeStatement,
|
|
37
54
|
IBalanceSheet,
|
|
38
55
|
IAccountBalance,
|
|
56
|
+
ITransactionView,
|
|
39
57
|
} from './skr.types.js';
|
|
40
58
|
|
|
41
59
|
/**
|
|
@@ -51,6 +69,7 @@ export class SkrApi {
|
|
|
51
69
|
private invoiceAdapter: InvoiceAdapter | null = null;
|
|
52
70
|
private invoiceStorage: InvoiceStorage | null = null;
|
|
53
71
|
private invoiceBookingEngine: InvoiceBookingEngine | null = null;
|
|
72
|
+
private journalPoster: JournalPoster = new JournalPoster();
|
|
54
73
|
|
|
55
74
|
constructor(private config: IDatabaseConfig) {
|
|
56
75
|
this.chartOfAccounts = new ChartOfAccounts(config);
|
|
@@ -84,6 +103,11 @@ export class SkrApi {
|
|
|
84
103
|
this.currentSKRType = skrType;
|
|
85
104
|
this.ledger = new Ledger(skrType);
|
|
86
105
|
this.reports = new Reports(skrType);
|
|
106
|
+
|
|
107
|
+
// v2 posting path: transactions are mandatory (replica set) — fail hard,
|
|
108
|
+
// filing-grade posting has no non-transactional fallback
|
|
109
|
+
await this.journalPoster.assertTransactionSupport();
|
|
110
|
+
await this.journalPoster.ensureIndexes();
|
|
87
111
|
|
|
88
112
|
// Initialize invoice components
|
|
89
113
|
this.invoiceAdapter = new InvoiceAdapter();
|
|
@@ -178,75 +202,160 @@ export class SkrApi {
|
|
|
178
202
|
return await this.chartOfAccounts.getAccountsByType(accountType);
|
|
179
203
|
}
|
|
180
204
|
|
|
181
|
-
// ========== Transaction Management ==========
|
|
205
|
+
// ========== Transaction Management (v2: hashed journal entries) ==========
|
|
182
206
|
|
|
183
207
|
/**
|
|
184
|
-
* Post a simple transaction
|
|
208
|
+
* Post a simple transaction. Since v2 this creates a hashed two-line
|
|
209
|
+
* journal entry (exact cents) — the Transaction collection is frozen.
|
|
185
210
|
*/
|
|
186
211
|
public async postTransaction(
|
|
187
212
|
transactionData: ITransactionData,
|
|
188
|
-
): Promise<
|
|
213
|
+
): Promise<ITransactionView> {
|
|
189
214
|
this.ensureInitialized();
|
|
190
|
-
if (
|
|
191
|
-
|
|
215
|
+
if (transactionData.amount <= 0) {
|
|
216
|
+
throw new Error('Transaction amount must be greater than zero');
|
|
217
|
+
}
|
|
218
|
+
if (transactionData.debitAccount === transactionData.creditAccount) {
|
|
219
|
+
throw new Error('Debit and credit accounts cannot be the same');
|
|
220
|
+
}
|
|
221
|
+
for (const [label, accountNumber] of [
|
|
222
|
+
['Debit', transactionData.debitAccount],
|
|
223
|
+
['Credit', transactionData.creditAccount],
|
|
224
|
+
] as const) {
|
|
225
|
+
const account = await Account.getAccountByNumber(accountNumber, this.currentSKRType!);
|
|
226
|
+
if (account && !account.isActive) {
|
|
227
|
+
throw new Error(`${label} account ${accountNumber} is not active`);
|
|
228
|
+
}
|
|
229
|
+
await Account.validateAccountForPosting(accountNumber, this.currentSKRType!);
|
|
230
|
+
}
|
|
231
|
+
const draft = transactionDataToDraft({
|
|
232
|
+
...transactionData,
|
|
233
|
+
skrType: this.currentSKRType!,
|
|
234
|
+
});
|
|
235
|
+
const entry = await this.journalPoster.postDraft(draft);
|
|
236
|
+
return entryToTransactionView(entry as never)!;
|
|
192
237
|
}
|
|
193
238
|
|
|
194
239
|
/**
|
|
195
|
-
* Post a journal entry
|
|
240
|
+
* Post a journal entry. Since v2 the entry is hashed, sequenced, and
|
|
241
|
+
* immutable; float inputs are converted to exact cents with a drift guard.
|
|
196
242
|
*/
|
|
197
243
|
public async postJournalEntry(
|
|
198
244
|
journalData: IJournalEntry,
|
|
199
245
|
): Promise<JournalEntry> {
|
|
200
246
|
this.ensureInitialized();
|
|
201
|
-
|
|
202
|
-
|
|
247
|
+
for (const line of journalData.lines) {
|
|
248
|
+
await Account.validateAccountForPosting(line.accountNumber, this.currentSKRType!);
|
|
249
|
+
}
|
|
250
|
+
const draft = journalInputToDraft({
|
|
251
|
+
...journalData,
|
|
252
|
+
skrType: this.currentSKRType!,
|
|
253
|
+
});
|
|
254
|
+
return this.journalPoster.postDraft(draft);
|
|
203
255
|
}
|
|
204
256
|
|
|
205
257
|
/**
|
|
206
|
-
* Get transaction by
|
|
258
|
+
* Get a transaction view by id (two-line v2 entries project 1:1).
|
|
207
259
|
*/
|
|
208
260
|
public async getTransaction(
|
|
209
261
|
transactionId: string,
|
|
210
|
-
): Promise<
|
|
262
|
+
): Promise<ITransactionView | null> {
|
|
211
263
|
this.ensureInitialized();
|
|
212
|
-
|
|
264
|
+
const entry = await (JournalEntry as any).getInstance({ id: transactionId });
|
|
265
|
+
if (!entry || entry.schemaVersion !== 2) return null;
|
|
266
|
+
return entryToTransactionView(entry);
|
|
213
267
|
}
|
|
214
268
|
|
|
215
269
|
/**
|
|
216
|
-
* List
|
|
270
|
+
* List transaction views with optional filter (two-line v2 entries).
|
|
217
271
|
*/
|
|
218
272
|
public async listTransactions(
|
|
219
273
|
filter?: ITransactionFilter,
|
|
220
|
-
): Promise<
|
|
274
|
+
): Promise<ITransactionView[]> {
|
|
221
275
|
this.ensureInitialized();
|
|
222
|
-
return
|
|
276
|
+
return this.listTransactionViews(filter);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async listTransactionViews(
|
|
280
|
+
filter?: ITransactionFilter,
|
|
281
|
+
): Promise<ITransactionView[]> {
|
|
282
|
+
const match: Record<string, unknown> = {
|
|
283
|
+
skrType: filter?.skrType ?? this.currentSKRType,
|
|
284
|
+
schemaVersion: 2,
|
|
285
|
+
status: { $in: ['posted', 'finalized'] },
|
|
286
|
+
};
|
|
287
|
+
if (filter?.dateFrom || filter?.dateTo) {
|
|
288
|
+
const range: Record<string, Date> = {};
|
|
289
|
+
if (filter.dateFrom) range.$gte = filter.dateFrom;
|
|
290
|
+
if (filter.dateTo) range.$lte = filter.dateTo;
|
|
291
|
+
match.date = range;
|
|
292
|
+
}
|
|
293
|
+
const docs = await (await import('./skr.database.js'))
|
|
294
|
+
.getDbSync()
|
|
295
|
+
.mongoDb.collection('JournalEntry')
|
|
296
|
+
.find(match)
|
|
297
|
+
.sort({ date: 1, sequenceNumber: 1 })
|
|
298
|
+
.toArray();
|
|
299
|
+
let views = docs
|
|
300
|
+
.map((doc) => entryToTransactionView(doc as never))
|
|
301
|
+
.filter((view): view is ITransactionView => view !== null);
|
|
302
|
+
if (filter?.accountNumber) {
|
|
303
|
+
views = views.filter(
|
|
304
|
+
(view) =>
|
|
305
|
+
view.debitAccount === filter.accountNumber ||
|
|
306
|
+
view.creditAccount === filter.accountNumber,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (filter?.minAmount !== undefined) {
|
|
310
|
+
views = views.filter((view) => view.amount >= filter.minAmount!);
|
|
311
|
+
}
|
|
312
|
+
if (filter?.maxAmount !== undefined) {
|
|
313
|
+
views = views.filter((view) => view.amount <= filter.maxAmount!);
|
|
314
|
+
}
|
|
315
|
+
if (filter?.searchTerm) {
|
|
316
|
+
const term = filter.searchTerm.toLowerCase();
|
|
317
|
+
views = views.filter(
|
|
318
|
+
(view) =>
|
|
319
|
+
view.description.toLowerCase().includes(term) ||
|
|
320
|
+
view.reference.toLowerCase().includes(term),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return views;
|
|
223
324
|
}
|
|
224
325
|
|
|
225
326
|
/**
|
|
226
|
-
* Get
|
|
327
|
+
* Get per-line transaction views for one account.
|
|
227
328
|
*/
|
|
228
329
|
public async getAccountTransactions(
|
|
229
330
|
accountNumber: string,
|
|
230
|
-
): Promise<
|
|
331
|
+
): Promise<ITransactionView[]> {
|
|
231
332
|
this.ensureInitialized();
|
|
232
|
-
return
|
|
333
|
+
return accountLineViews(accountNumber, this.currentSKRType!);
|
|
233
334
|
}
|
|
234
335
|
|
|
235
336
|
/**
|
|
236
|
-
* Reverse
|
|
337
|
+
* Reverse via Storno — the original entry is never mutated (GoBD).
|
|
237
338
|
*/
|
|
238
|
-
public async reverseTransaction(transactionId: string): Promise<
|
|
339
|
+
public async reverseTransaction(transactionId: string): Promise<ITransactionView> {
|
|
239
340
|
this.ensureInitialized();
|
|
240
|
-
|
|
341
|
+
const entry = await (JournalEntry as any).getInstance({ id: transactionId });
|
|
342
|
+
if (!entry || entry.schemaVersion !== 2) {
|
|
343
|
+
throw new Error(`Transaction ${transactionId} not found`);
|
|
344
|
+
}
|
|
345
|
+
const storno = await this.journalPoster.postStorno(entry);
|
|
346
|
+
return entryToTransactionView(storno as never)!;
|
|
241
347
|
}
|
|
242
348
|
|
|
243
349
|
/**
|
|
244
|
-
* Reverse a journal entry
|
|
350
|
+
* Reverse a journal entry via Storno.
|
|
245
351
|
*/
|
|
246
352
|
public async reverseJournalEntry(journalId: string): Promise<JournalEntry> {
|
|
247
353
|
this.ensureInitialized();
|
|
248
|
-
|
|
249
|
-
|
|
354
|
+
const entry = await (JournalEntry as any).getInstance({ id: journalId });
|
|
355
|
+
if (!entry || entry.schemaVersion !== 2) {
|
|
356
|
+
throw new Error(`Journal entry ${journalId} not found`);
|
|
357
|
+
}
|
|
358
|
+
return this.journalPoster.postStorno(entry);
|
|
250
359
|
}
|
|
251
360
|
|
|
252
361
|
// ========== Reporting ==========
|
|
@@ -315,50 +424,166 @@ export class SkrApi {
|
|
|
315
424
|
}
|
|
316
425
|
|
|
317
426
|
/**
|
|
318
|
-
* Export to DATEV format
|
|
427
|
+
* Export to DATEV format (EXTF 700 Buchungsstapel over v2 entries).
|
|
428
|
+
* For a fully parameterized export use exportDatevBuchungsstapel().
|
|
319
429
|
*/
|
|
320
430
|
public async exportToDATEV(params?: IReportParams): Promise<string> {
|
|
321
431
|
this.ensureInitialized();
|
|
322
|
-
|
|
323
|
-
|
|
432
|
+
const dateFrom = params?.dateFrom ?? new Date('1970-01-01T00:00:00.000Z');
|
|
433
|
+
const dateTo = params?.dateTo ?? new Date('2999-12-31T00:00:00.000Z');
|
|
434
|
+
const { csv } = await this.exportDatevBuchungsstapel({
|
|
435
|
+
dateFrom,
|
|
436
|
+
dateTo,
|
|
437
|
+
consultantNumber: 0,
|
|
438
|
+
clientNumber: 0,
|
|
439
|
+
fiscalYearStart: new Date(Date.UTC(dateFrom.getUTCFullYear(), 0, 1)),
|
|
440
|
+
label: 'Buchungsstapel',
|
|
441
|
+
});
|
|
442
|
+
return csv;
|
|
324
443
|
}
|
|
325
444
|
|
|
326
445
|
// ========== Period Management ==========
|
|
327
446
|
|
|
328
447
|
/**
|
|
329
|
-
* Close accounting period
|
|
448
|
+
* Close accounting period: post P&L-zeroing entries (v2) into the period.
|
|
449
|
+
* Locking is a separate deliberate act — call festschreibePeriod() after.
|
|
330
450
|
*/
|
|
331
451
|
public async closePeriod(
|
|
332
452
|
period: string,
|
|
333
|
-
closingAccountNumber
|
|
453
|
+
closingAccountNumber: string = '9400',
|
|
334
454
|
): Promise<JournalEntry[]> {
|
|
335
455
|
this.ensureInitialized();
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
456
|
+
const [year, month] = period.split('-').map((part) => Number.parseInt(part, 10));
|
|
457
|
+
const dateFrom = new Date(Date.UTC(year, month - 1, 1));
|
|
458
|
+
const dateTo = new Date(Date.UTC(year, month, 0, 23, 59, 59, 999));
|
|
459
|
+
const trialBalance = await this.getTrialBalanceCents({ dateFrom, dateTo });
|
|
460
|
+
|
|
461
|
+
const revenueLines: core.IJournalLineDraft[] = [];
|
|
462
|
+
const expenseLines: core.IJournalLineDraft[] = [];
|
|
463
|
+
for (const balance of trialBalance.accounts) {
|
|
464
|
+
const account = await Account.getAccountByNumber(
|
|
465
|
+
balance.accountNumber,
|
|
466
|
+
this.currentSKRType!,
|
|
467
|
+
);
|
|
468
|
+
if (!account) continue;
|
|
469
|
+
if (account.accountType === 'revenue') {
|
|
470
|
+
const net = balance.creditCents - balance.debitCents;
|
|
471
|
+
if (net !== 0) {
|
|
472
|
+
revenueLines.push({
|
|
473
|
+
accountNumber: balance.accountNumber,
|
|
474
|
+
counterAccount: closingAccountNumber,
|
|
475
|
+
side: net > 0 ? 'debit' : 'credit',
|
|
476
|
+
amountCents: Math.abs(net),
|
|
477
|
+
buKey: '40',
|
|
478
|
+
buchungstext: `Abschluss ${period}`,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
} else if (account.accountType === 'expense') {
|
|
482
|
+
const net = balance.debitCents - balance.creditCents;
|
|
483
|
+
if (net !== 0) {
|
|
484
|
+
expenseLines.push({
|
|
485
|
+
accountNumber: balance.accountNumber,
|
|
486
|
+
counterAccount: closingAccountNumber,
|
|
487
|
+
side: net > 0 ? 'credit' : 'debit',
|
|
488
|
+
amountCents: Math.abs(net),
|
|
489
|
+
buKey: '40',
|
|
490
|
+
buchungstext: `Abschluss ${period}`,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const entries: JournalEntry[] = [];
|
|
497
|
+
const closeDraft = async (
|
|
498
|
+
lines: core.IJournalLineDraft[],
|
|
499
|
+
reference: string,
|
|
500
|
+
): Promise<void> => {
|
|
501
|
+
if (lines.length === 0) return;
|
|
502
|
+
const debitSum = lines
|
|
503
|
+
.filter((line) => line.side === 'debit')
|
|
504
|
+
.reduce((sum, line) => sum + line.amountCents, 0);
|
|
505
|
+
const creditSum = lines
|
|
506
|
+
.filter((line) => line.side === 'credit')
|
|
507
|
+
.reduce((sum, line) => sum + line.amountCents, 0);
|
|
508
|
+
const net = debitSum - creditSum;
|
|
509
|
+
if (net !== 0) {
|
|
510
|
+
lines.push({
|
|
511
|
+
accountNumber: closingAccountNumber,
|
|
512
|
+
counterAccount: lines[0].accountNumber,
|
|
513
|
+
side: net > 0 ? 'credit' : 'debit',
|
|
514
|
+
amountCents: Math.abs(net),
|
|
515
|
+
buKey: '',
|
|
516
|
+
buchungstext: `Abschluss ${period}`,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
entries.push(
|
|
520
|
+
await this.journalPoster.postDraft({
|
|
521
|
+
date: dateTo,
|
|
522
|
+
description: `Periodenabschluss ${period}`,
|
|
523
|
+
reference,
|
|
524
|
+
skrType: this.currentSKRType!,
|
|
525
|
+
currency: 'EUR',
|
|
526
|
+
lines,
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
};
|
|
530
|
+
await closeDraft(revenueLines, `CLOSE-REV-${period}`);
|
|
531
|
+
await closeDraft(expenseLines, `CLOSE-EXP-${period}`);
|
|
532
|
+
return entries;
|
|
341
533
|
}
|
|
342
534
|
|
|
343
535
|
/**
|
|
344
|
-
* Get account balance
|
|
536
|
+
* Get account balance (aggregated from v2 entries, exact cents inside).
|
|
345
537
|
*/
|
|
346
538
|
public async getAccountBalance(
|
|
347
539
|
accountNumber: string,
|
|
348
540
|
asOfDate?: Date,
|
|
349
|
-
): Promise<
|
|
541
|
+
): Promise<IAccountBalance> {
|
|
350
542
|
this.ensureInitialized();
|
|
351
|
-
|
|
352
|
-
|
|
543
|
+
const account = await Account.getAccountByNumber(
|
|
544
|
+
accountNumber,
|
|
545
|
+
this.currentSKRType!,
|
|
546
|
+
);
|
|
547
|
+
if (!account) {
|
|
548
|
+
throw new Error(`Account ${accountNumber} not found`);
|
|
549
|
+
}
|
|
550
|
+
const balance = await this.getAccountBalanceCents(accountNumber, {
|
|
551
|
+
dateTo: asOfDate,
|
|
552
|
+
});
|
|
553
|
+
const signed =
|
|
554
|
+
account.accountType === 'asset' || account.accountType === 'expense'
|
|
555
|
+
? balance.balanceCents
|
|
556
|
+
: -balance.balanceCents;
|
|
557
|
+
return {
|
|
558
|
+
accountNumber,
|
|
559
|
+
debitTotal: balance.debitCents / 100,
|
|
560
|
+
creditTotal: balance.creditCents / 100,
|
|
561
|
+
balance: signed / 100,
|
|
562
|
+
lastUpdated: new Date(),
|
|
563
|
+
};
|
|
353
564
|
}
|
|
354
565
|
|
|
355
566
|
/**
|
|
356
|
-
*
|
|
567
|
+
* Refresh the (informational) balance caches on Account documents from the
|
|
568
|
+
* aggregated v2 ledger.
|
|
357
569
|
*/
|
|
358
570
|
public async recalculateBalances(): Promise<void> {
|
|
359
571
|
this.ensureInitialized();
|
|
360
|
-
|
|
361
|
-
|
|
572
|
+
const trialBalance = await this.getTrialBalanceCents();
|
|
573
|
+
for (const balance of trialBalance.accounts) {
|
|
574
|
+
const account = await Account.getAccountByNumber(
|
|
575
|
+
balance.accountNumber,
|
|
576
|
+
this.currentSKRType!,
|
|
577
|
+
);
|
|
578
|
+
if (!account) continue;
|
|
579
|
+
account.debitTotal = balance.debitCents / 100;
|
|
580
|
+
account.creditTotal = balance.creditCents / 100;
|
|
581
|
+
account.balance =
|
|
582
|
+
account.accountType === 'asset' || account.accountType === 'expense'
|
|
583
|
+
? balance.balanceCents / 100
|
|
584
|
+
: -balance.balanceCents / 100;
|
|
585
|
+
await account.save();
|
|
586
|
+
}
|
|
362
587
|
}
|
|
363
588
|
|
|
364
589
|
// ========== Import/Export ==========
|
|
@@ -641,6 +866,166 @@ export class SkrApi {
|
|
|
641
866
|
|
|
642
867
|
// ========== Utility Methods ==========
|
|
643
868
|
|
|
869
|
+
// ========== v2 Ledger (hashed, gapless, festschreibbar) ==========
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Default account policy for the active chart.
|
|
873
|
+
*/
|
|
874
|
+
public getDefaultPolicy(): core.IAccountPolicy {
|
|
875
|
+
this.ensureInitialized();
|
|
876
|
+
return core.getDefaultPolicy(this.currentSKRType!);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Post a validated journal draft atomically (v2 path: exact cents,
|
|
881
|
+
* gapless sequence, hash chain). Use the recipes in `core` to build drafts.
|
|
882
|
+
*/
|
|
883
|
+
public async postDraft(
|
|
884
|
+
draft: core.IJournalDraft,
|
|
885
|
+
options?: IPostDraftOptions,
|
|
886
|
+
): Promise<JournalEntry> {
|
|
887
|
+
this.ensureInitialized();
|
|
888
|
+
return this.journalPoster.postDraft(draft, {
|
|
889
|
+
policy: options?.policy ?? this.getDefaultPolicy(),
|
|
890
|
+
...options,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Reverse a hashed entry via Storno — the original is never mutated.
|
|
896
|
+
*/
|
|
897
|
+
public async postStorno(
|
|
898
|
+
entry: JournalEntry,
|
|
899
|
+
options?: IPostDraftOptions & { date?: Date; description?: string },
|
|
900
|
+
): Promise<JournalEntry> {
|
|
901
|
+
this.ensureInitialized();
|
|
902
|
+
return this.journalPoster.postStorno(entry, {
|
|
903
|
+
policy: options?.policy ?? this.getDefaultPolicy(),
|
|
904
|
+
...options,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Festschreibung: finalize all posted v2 entries of a period and lock it
|
|
910
|
+
* against further postings (GoBD).
|
|
911
|
+
*/
|
|
912
|
+
public async festschreibePeriod(
|
|
913
|
+
period: string,
|
|
914
|
+
lockedBy?: string,
|
|
915
|
+
): Promise<{ finalizedEntries: number }> {
|
|
916
|
+
this.ensureInitialized();
|
|
917
|
+
return festschreibePeriod({
|
|
918
|
+
skrType: this.currentSKRType!,
|
|
919
|
+
period,
|
|
920
|
+
lockedBy,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
public async getPeriodState(period: string): Promise<ILedgerPeriodState | null> {
|
|
925
|
+
this.ensureInitialized();
|
|
926
|
+
return getPeriodState(this.currentSKRType!, period);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Recompute the whole hash chain for a fiscal year and compare it against
|
|
931
|
+
* the stored chain head — the audit-trail integrity check.
|
|
932
|
+
*/
|
|
933
|
+
public async verifyAuditChain(fiscalYear: number): Promise<IChainVerificationResult> {
|
|
934
|
+
this.ensureInitialized();
|
|
935
|
+
return verifyHashChain(this.currentSKRType!, fiscalYear);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Exact trial balance in integer cents, aggregated from v2 entries.
|
|
940
|
+
*/
|
|
941
|
+
public async getTrialBalanceCents(
|
|
942
|
+
options?: IBalanceQueryOptions,
|
|
943
|
+
): Promise<ITrialBalanceCents> {
|
|
944
|
+
this.ensureInitialized();
|
|
945
|
+
return aggregateTrialBalance(this.currentSKRType!, options);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
public async getAccountBalanceCents(
|
|
949
|
+
accountNumber: string,
|
|
950
|
+
options?: IBalanceQueryOptions,
|
|
951
|
+
): Promise<IAccountBalanceCents> {
|
|
952
|
+
this.ensureInitialized();
|
|
953
|
+
return aggregateAccountBalance(accountNumber, this.currentSKRType!, options);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* DATEV Buchungsstapel (EXTF 700 / v13) over the v2 entries of a date
|
|
958
|
+
* range. Returns the CSV string (persist as CP1252!) and filename.
|
|
959
|
+
*/
|
|
960
|
+
public async exportDatevBuchungsstapel(options: {
|
|
961
|
+
dateFrom: Date;
|
|
962
|
+
dateTo: Date;
|
|
963
|
+
consultantNumber: number;
|
|
964
|
+
clientNumber: number;
|
|
965
|
+
fiscalYearStart: Date;
|
|
966
|
+
label?: string;
|
|
967
|
+
festschreibung?: boolean;
|
|
968
|
+
createdAt?: Date;
|
|
969
|
+
}): Promise<{ csv: string; filename: string; rowCount: number }> {
|
|
970
|
+
this.ensureInitialized();
|
|
971
|
+
const collection = (await import('./skr.database.js'))
|
|
972
|
+
.getDbSync()
|
|
973
|
+
.mongoDb.collection('JournalEntry');
|
|
974
|
+
const docs = await collection
|
|
975
|
+
.find({
|
|
976
|
+
skrType: this.currentSKRType,
|
|
977
|
+
schemaVersion: 2,
|
|
978
|
+
status: { $in: ['posted', 'finalized'] },
|
|
979
|
+
date: { $gte: options.dateFrom, $lte: options.dateTo },
|
|
980
|
+
})
|
|
981
|
+
.sort({ sequenceNumber: 1 })
|
|
982
|
+
.toArray();
|
|
983
|
+
const drafts: core.IJournalDraft[] = docs.map((doc) => ({
|
|
984
|
+
date: doc.date as Date,
|
|
985
|
+
description: doc.description as string,
|
|
986
|
+
reference: doc.reference as string,
|
|
987
|
+
skrType: doc.skrType as TSKRType,
|
|
988
|
+
currency: 'EUR',
|
|
989
|
+
taxScenario: doc.taxScenario as core.TTaxScenario | undefined,
|
|
990
|
+
lines: (doc.lines as core.IJournalLineDraft[]).map((line) => ({
|
|
991
|
+
accountNumber: line.accountNumber,
|
|
992
|
+
counterAccount: line.counterAccount ?? '',
|
|
993
|
+
side: (line.side ?? 'debit') as 'debit' | 'credit',
|
|
994
|
+
amountCents: line.amountCents ?? 0,
|
|
995
|
+
buKey: (line.buKey ?? '') as core.TBuKey,
|
|
996
|
+
buchungstext: line.buchungstext ?? '',
|
|
997
|
+
belegfeld1: line.belegfeld1,
|
|
998
|
+
costCenter1: line.costCenter1,
|
|
999
|
+
docLink: line.docLink,
|
|
1000
|
+
})),
|
|
1001
|
+
}));
|
|
1002
|
+
const rows = core.journalDraftsToDatevRows(drafts, {
|
|
1003
|
+
festschreibung: options.festschreibung,
|
|
1004
|
+
});
|
|
1005
|
+
const result = core.writeExtfBuchungsstapel(rows, {
|
|
1006
|
+
createdAt: options.createdAt ?? new Date(),
|
|
1007
|
+
consultantNumber: options.consultantNumber,
|
|
1008
|
+
clientNumber: options.clientNumber,
|
|
1009
|
+
fiscalYearStart: options.fiscalYearStart,
|
|
1010
|
+
dateFrom: options.dateFrom,
|
|
1011
|
+
dateTo: options.dateTo,
|
|
1012
|
+
label: options.label ?? `Buchungsstapel ${options.dateFrom.toISOString().slice(0, 10)}`,
|
|
1013
|
+
festschreibung: options.festschreibung ?? false,
|
|
1014
|
+
});
|
|
1015
|
+
return { ...result, rowCount: rows.length };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Run the idempotent v1 -> v2 data migration (cents backfill, journal
|
|
1020
|
+
* unification, hash-chain assignment). Persist the returned report with
|
|
1021
|
+
* your records (Verfahrensdokumentation artifact).
|
|
1022
|
+
*/
|
|
1023
|
+
public async migrateToV2(): Promise<import('./skr.migrate.js').IMigrationReport> {
|
|
1024
|
+
this.ensureInitialized();
|
|
1025
|
+
const { migrateToV2 } = await import('./skr.migrate.js');
|
|
1026
|
+
return migrateToV2();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
644
1029
|
/**
|
|
645
1030
|
* Get current SKR type
|
|
646
1031
|
*/
|
|
@@ -670,10 +1055,11 @@ export class SkrApi {
|
|
|
670
1055
|
/**
|
|
671
1056
|
* Get unbalanced transactions (for audit)
|
|
672
1057
|
*/
|
|
673
|
-
public async getUnbalancedTransactions(): Promise<
|
|
1058
|
+
public async getUnbalancedTransactions(): Promise<ITransactionView[]> {
|
|
674
1059
|
this.ensureInitialized();
|
|
675
|
-
|
|
676
|
-
|
|
1060
|
+
// v2 posting enforces exact balance atomically — imbalance is impossible;
|
|
1061
|
+
// chain integrity is checked via verifyAuditChain()
|
|
1062
|
+
return [];
|
|
677
1063
|
}
|
|
678
1064
|
|
|
679
1065
|
/**
|
|
@@ -695,10 +1081,10 @@ export class SkrApi {
|
|
|
695
1081
|
*/
|
|
696
1082
|
public async postBatchTransactions(
|
|
697
1083
|
transactions: ITransactionData[],
|
|
698
|
-
): Promise<
|
|
1084
|
+
): Promise<ITransactionView[]> {
|
|
699
1085
|
this.ensureInitialized();
|
|
700
1086
|
|
|
701
|
-
const results:
|
|
1087
|
+
const results: ITransactionView[] = [];
|
|
702
1088
|
const errors: Array<{ index: number; error: string }> = [];
|
|
703
1089
|
|
|
704
1090
|
for (let i = 0; i < transactions.length; i++) {
|
|
@@ -798,7 +1184,7 @@ export class SkrApi {
|
|
|
798
1184
|
pageSize: number = 50,
|
|
799
1185
|
filter?: ITransactionFilter,
|
|
800
1186
|
): Promise<{
|
|
801
|
-
data:
|
|
1187
|
+
data: ITransactionView[];
|
|
802
1188
|
total: number;
|
|
803
1189
|
page: number;
|
|
804
1190
|
pageSize: number;
|