@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance computation for the v2 ledger: always aggregated from the
|
|
3
|
+
* journal, never incrementally stored. Exact integer cents throughout.
|
|
4
|
+
*/
|
|
5
|
+
import { getDbSync } from './skr.database.js';
|
|
6
|
+
import { JOURNAL_ENTRY_COLLECTION } from './skr.period.js';
|
|
7
|
+
import type { TSKRType } from './skr.types.js';
|
|
8
|
+
|
|
9
|
+
export interface IAccountBalanceCents {
|
|
10
|
+
accountNumber: string;
|
|
11
|
+
debitCents: number;
|
|
12
|
+
creditCents: number;
|
|
13
|
+
/** debit - credit; sign interpretation belongs to the account type */
|
|
14
|
+
balanceCents: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ITrialBalanceCents {
|
|
18
|
+
accounts: IAccountBalanceCents[];
|
|
19
|
+
totalDebitCents: number;
|
|
20
|
+
totalCreditCents: number;
|
|
21
|
+
isBalanced: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IBalanceQueryOptions {
|
|
25
|
+
dateFrom?: Date;
|
|
26
|
+
dateTo?: Date;
|
|
27
|
+
/** include festgeschriebene entries (default true) */
|
|
28
|
+
includeFinalized?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function entryMatch(skrType: TSKRType, options?: IBalanceQueryOptions): Record<string, unknown> {
|
|
32
|
+
const statuses = options?.includeFinalized === false ? ['posted'] : ['posted', 'finalized'];
|
|
33
|
+
const match: Record<string, unknown> = {
|
|
34
|
+
schemaVersion: 2,
|
|
35
|
+
skrType,
|
|
36
|
+
status: { $in: statuses },
|
|
37
|
+
};
|
|
38
|
+
if (options?.dateFrom || options?.dateTo) {
|
|
39
|
+
const range: Record<string, Date> = {};
|
|
40
|
+
if (options.dateFrom) range.$gte = options.dateFrom;
|
|
41
|
+
if (options.dateTo) range.$lte = options.dateTo;
|
|
42
|
+
match.date = range;
|
|
43
|
+
}
|
|
44
|
+
return match;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Aggregated trial balance over all schemaVersion-2 entries.
|
|
49
|
+
*/
|
|
50
|
+
export async function aggregateTrialBalance(
|
|
51
|
+
skrType: TSKRType,
|
|
52
|
+
options?: IBalanceQueryOptions,
|
|
53
|
+
): Promise<ITrialBalanceCents> {
|
|
54
|
+
const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
|
|
55
|
+
const rows = await collection
|
|
56
|
+
.aggregate<{ _id: string; debitCents: number; creditCents: number }>([
|
|
57
|
+
{ $match: entryMatch(skrType, options) },
|
|
58
|
+
{ $unwind: '$lines' },
|
|
59
|
+
{
|
|
60
|
+
$group: {
|
|
61
|
+
_id: '$lines.accountNumber',
|
|
62
|
+
debitCents: {
|
|
63
|
+
$sum: { $cond: [{ $eq: ['$lines.side', 'debit'] }, '$lines.amountCents', 0] },
|
|
64
|
+
},
|
|
65
|
+
creditCents: {
|
|
66
|
+
$sum: { $cond: [{ $eq: ['$lines.side', 'credit'] }, '$lines.amountCents', 0] },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{ $sort: { _id: 1 } },
|
|
71
|
+
])
|
|
72
|
+
.toArray();
|
|
73
|
+
|
|
74
|
+
const accounts: IAccountBalanceCents[] = rows.map((row) => ({
|
|
75
|
+
accountNumber: row._id,
|
|
76
|
+
debitCents: row.debitCents,
|
|
77
|
+
creditCents: row.creditCents,
|
|
78
|
+
balanceCents: row.debitCents - row.creditCents,
|
|
79
|
+
}));
|
|
80
|
+
const totalDebitCents = accounts.reduce((sum, account) => sum + account.debitCents, 0);
|
|
81
|
+
const totalCreditCents = accounts.reduce((sum, account) => sum + account.creditCents, 0);
|
|
82
|
+
return {
|
|
83
|
+
accounts,
|
|
84
|
+
totalDebitCents,
|
|
85
|
+
totalCreditCents,
|
|
86
|
+
isBalanced: totalDebitCents === totalCreditCents,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Exact balance of one account.
|
|
92
|
+
*/
|
|
93
|
+
export async function aggregateAccountBalance(
|
|
94
|
+
accountNumber: string,
|
|
95
|
+
skrType: TSKRType,
|
|
96
|
+
options?: IBalanceQueryOptions,
|
|
97
|
+
): Promise<IAccountBalanceCents> {
|
|
98
|
+
const collection = getDbSync().mongoDb.collection(JOURNAL_ENTRY_COLLECTION);
|
|
99
|
+
const rows = await collection
|
|
100
|
+
.aggregate<{ _id: string; debitCents: number; creditCents: number }>([
|
|
101
|
+
{ $match: { ...entryMatch(skrType, options), 'lines.accountNumber': accountNumber } },
|
|
102
|
+
{ $unwind: '$lines' },
|
|
103
|
+
{ $match: { 'lines.accountNumber': accountNumber } },
|
|
104
|
+
{
|
|
105
|
+
$group: {
|
|
106
|
+
_id: '$lines.accountNumber',
|
|
107
|
+
debitCents: {
|
|
108
|
+
$sum: { $cond: [{ $eq: ['$lines.side', 'debit'] }, '$lines.amountCents', 0] },
|
|
109
|
+
},
|
|
110
|
+
creditCents: {
|
|
111
|
+
$sum: { $cond: [{ $eq: ['$lines.side', 'credit'] }, '$lines.amountCents', 0] },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
])
|
|
116
|
+
.toArray();
|
|
117
|
+
const row = rows[0];
|
|
118
|
+
if (!row) {
|
|
119
|
+
return { accountNumber, debitCents: 0, creditCents: 0, balanceCents: 0 };
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
accountNumber,
|
|
123
|
+
debitCents: row.debitCents,
|
|
124
|
+
creditCents: row.creditCents,
|
|
125
|
+
balanceCents: row.debitCents - row.creditCents,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -12,10 +12,12 @@ import type {
|
|
|
12
12
|
IJournalEntry,
|
|
13
13
|
IJournalEntryLine,
|
|
14
14
|
} from './skr.types.js';
|
|
15
|
+
import { ImmutableEntryError } from './skr.errors.js';
|
|
15
16
|
|
|
16
17
|
declare abstract class SmartDataDbDocBase {
|
|
17
|
-
public
|
|
18
|
-
public
|
|
18
|
+
public creationStatus: 'db' | 'new' | 'mixed';
|
|
19
|
+
public save(opts?: { session?: any }): Promise<void>;
|
|
20
|
+
public delete(opts?: { session?: any }): Promise<void>;
|
|
19
21
|
public static getInstance<T>(
|
|
20
22
|
this: new (...args: any[]) => T,
|
|
21
23
|
query: Record<string, any>,
|
|
@@ -72,7 +74,34 @@ export class JournalEntry extends SmartDataDbDoc {
|
|
|
72
74
|
|
|
73
75
|
@svDb()
|
|
74
76
|
@index()
|
|
75
|
-
public status!: 'draft' | 'posted' | 'reversed';
|
|
77
|
+
public status!: 'draft' | 'posted' | 'reversed' | 'finalized';
|
|
78
|
+
|
|
79
|
+
// --- schemaVersion 2: GoBD hash chain + Festschreibung ------------------
|
|
80
|
+
@svDb()
|
|
81
|
+
public schemaVersion?: number;
|
|
82
|
+
|
|
83
|
+
@svDb()
|
|
84
|
+
@index()
|
|
85
|
+
public sequenceNumber?: number;
|
|
86
|
+
|
|
87
|
+
@svDb()
|
|
88
|
+
public prevHash?: string;
|
|
89
|
+
|
|
90
|
+
@svDb()
|
|
91
|
+
public entryHash?: string;
|
|
92
|
+
|
|
93
|
+
@svDb()
|
|
94
|
+
public finalizedAt?: Date | null;
|
|
95
|
+
|
|
96
|
+
@svDb()
|
|
97
|
+
@index()
|
|
98
|
+
public reversalOf?: string;
|
|
99
|
+
|
|
100
|
+
@svDb()
|
|
101
|
+
public taxScenario?: string;
|
|
102
|
+
|
|
103
|
+
@svDb()
|
|
104
|
+
public currency?: string;
|
|
76
105
|
|
|
77
106
|
@svDb()
|
|
78
107
|
public transactionIds!: string[];
|
|
@@ -299,8 +328,14 @@ export class JournalEntry extends SmartDataDbDoc {
|
|
|
299
328
|
}
|
|
300
329
|
}
|
|
301
330
|
|
|
302
|
-
// Validate posting key consistency across all lines
|
|
303
|
-
|
|
331
|
+
// Validate posting key consistency across all lines (v1 numeric keys only;
|
|
332
|
+
// schemaVersion-2 lines carry string buKeys validated by the core)
|
|
333
|
+
const consistencyValidation = validatePostingKeyConsistency(
|
|
334
|
+
this.lines.filter(
|
|
335
|
+
(line): line is IJournalEntryLine & { postingKey: NonNullable<IJournalEntryLine['postingKey']> } =>
|
|
336
|
+
line.postingKey !== undefined,
|
|
337
|
+
),
|
|
338
|
+
);
|
|
304
339
|
if (!consistencyValidation.isValid) {
|
|
305
340
|
validationErrors.push(...consistencyValidation.errors);
|
|
306
341
|
}
|
|
@@ -439,6 +474,13 @@ export class JournalEntry extends SmartDataDbDoc {
|
|
|
439
474
|
}
|
|
440
475
|
|
|
441
476
|
public async beforeSave(): Promise<void> {
|
|
477
|
+
// GoBD immutability: once a hashed (schemaVersion 2) entry is persisted it
|
|
478
|
+
// must never change — corrections happen via Storno entries only.
|
|
479
|
+
// Festschreibung stamps bypass doc saves (raw updates outside the hash).
|
|
480
|
+
if (this.creationStatus === 'db' && this.entryHash) {
|
|
481
|
+
throw new ImmutableEntryError(this.id);
|
|
482
|
+
}
|
|
483
|
+
|
|
442
484
|
// Normalize any negative amounts to the correct side
|
|
443
485
|
this.sanitizeLines();
|
|
444
486
|
// Recalculate totals before saving
|
|
@@ -457,4 +499,11 @@ export class JournalEntry extends SmartDataDbDoc {
|
|
|
457
499
|
throw new Error('Journal entry must have at least one line');
|
|
458
500
|
}
|
|
459
501
|
}
|
|
502
|
+
|
|
503
|
+
public async delete(opts?: { session?: any }): Promise<void> {
|
|
504
|
+
if (this.entryHash) {
|
|
505
|
+
throw new ImmutableEntryError(this.id);
|
|
506
|
+
}
|
|
507
|
+
await super.delete(opts);
|
|
508
|
+
}
|
|
460
509
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { Account } from './skr.classes.account.js';
|
|
3
|
-
import { Transaction } from './skr.classes.transaction.js';
|
|
4
3
|
import { Ledger } from './skr.classes.ledger.js';
|
|
4
|
+
import * as core from './core/index.js';
|
|
5
|
+
import { accountLineViews, loadDraftsForExport } from './skr.txview.js';
|
|
5
6
|
import type {
|
|
6
7
|
TSKRType,
|
|
7
8
|
ITrialBalanceReport,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
IBalanceSheet,
|
|
12
13
|
IBalanceSheetEntry,
|
|
13
14
|
IReportParams,
|
|
15
|
+
ITransactionView,
|
|
14
16
|
} from './skr.types.js';
|
|
15
17
|
|
|
16
18
|
export class Reports {
|
|
@@ -49,26 +51,37 @@ export class Reports {
|
|
|
49
51
|
let totalCredits = 0;
|
|
50
52
|
|
|
51
53
|
for (const account of accounts) {
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
// Per-account activity from the v2 (hashed) journal entries
|
|
55
|
+
const views = await accountLineViews(
|
|
56
|
+
account.accountNumber,
|
|
57
|
+
this.skrType,
|
|
58
|
+
params?.dateTo
|
|
59
|
+
? { skrType: this.skrType, dateTo: params.dateTo }
|
|
60
|
+
: undefined,
|
|
61
|
+
);
|
|
62
|
+
let debitTotal = 0;
|
|
63
|
+
let creditTotal = 0;
|
|
64
|
+
for (const view of views) {
|
|
65
|
+
if (view.debitAccount === account.accountNumber) debitTotal += view.amount;
|
|
66
|
+
if (view.creditAccount === account.accountNumber) creditTotal += view.amount;
|
|
67
|
+
}
|
|
68
|
+
const signedBalance =
|
|
69
|
+
account.accountType === 'asset' || account.accountType === 'expense'
|
|
70
|
+
? debitTotal - creditTotal
|
|
71
|
+
: creditTotal - debitTotal;
|
|
72
|
+
|
|
73
|
+
if (debitTotal !== 0 || creditTotal !== 0) {
|
|
61
74
|
const entry: ITrialBalanceEntry = {
|
|
62
75
|
accountNumber: account.accountNumber,
|
|
63
76
|
accountName: account.accountName,
|
|
64
|
-
debitBalance:
|
|
65
|
-
creditBalance:
|
|
66
|
-
netBalance:
|
|
77
|
+
debitBalance: debitTotal,
|
|
78
|
+
creditBalance: creditTotal,
|
|
79
|
+
netBalance: signedBalance,
|
|
67
80
|
};
|
|
68
81
|
|
|
69
82
|
entries.push(entry);
|
|
70
|
-
totalDebits +=
|
|
71
|
-
totalCredits +=
|
|
83
|
+
totalDebits += debitTotal;
|
|
84
|
+
totalCredits += creditTotal;
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -342,48 +355,17 @@ export class Reports {
|
|
|
342
355
|
account: Account,
|
|
343
356
|
params?: IReportParams,
|
|
344
357
|
): Promise<number> {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.skrType,
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
// Apply date filter if provided
|
|
351
|
-
if (params?.dateFrom || params?.dateTo) {
|
|
352
|
-
// Normalize dates for inclusive comparison
|
|
353
|
-
const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
|
|
354
|
-
const dateTo = params.dateTo ? new Date(params.dateTo) : null;
|
|
355
|
-
|
|
356
|
-
// Set dateFrom to start of day (00:00:00.000)
|
|
357
|
-
if (dateFrom) {
|
|
358
|
-
dateFrom.setHours(0, 0, 0, 0);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Set dateTo to end of day (23:59:59.999) for inclusive comparison
|
|
362
|
-
if (dateTo) {
|
|
363
|
-
dateTo.setHours(23, 59, 59, 999);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
transactions = transactions.filter((transaction) => {
|
|
367
|
-
const txDate = transaction.date instanceof Date
|
|
368
|
-
? transaction.date
|
|
369
|
-
: new Date(transaction.date);
|
|
370
|
-
const txTime = txDate.getTime();
|
|
371
|
-
|
|
372
|
-
if (dateFrom && txTime < dateFrom.getTime()) return false;
|
|
373
|
-
if (dateTo && txTime > dateTo.getTime()) return false;
|
|
374
|
-
return true;
|
|
375
|
-
});
|
|
376
|
-
}
|
|
358
|
+
// v2: per-account activity is projected from hashed journal entries
|
|
359
|
+
const views = await accountLineViews(account.accountNumber, this.skrType, params);
|
|
377
360
|
|
|
378
361
|
let debitTotal = 0;
|
|
379
362
|
let creditTotal = 0;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
debitTotal += transaction.amount;
|
|
363
|
+
for (const view of views) {
|
|
364
|
+
if (view.debitAccount === account.accountNumber) {
|
|
365
|
+
debitTotal += view.amount;
|
|
384
366
|
}
|
|
385
|
-
if (
|
|
386
|
-
creditTotal +=
|
|
367
|
+
if (view.creditAccount === account.accountNumber) {
|
|
368
|
+
creditTotal += view.amount;
|
|
387
369
|
}
|
|
388
370
|
}
|
|
389
371
|
|
|
@@ -489,44 +471,10 @@ export class Reports {
|
|
|
489
471
|
private async getAccountTransactions(
|
|
490
472
|
accountNumber: string,
|
|
491
473
|
params?: IReportParams,
|
|
492
|
-
): Promise<
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
// Apply date filter
|
|
499
|
-
if (params?.dateFrom || params?.dateTo) {
|
|
500
|
-
// Normalize dates for inclusive comparison
|
|
501
|
-
const dateFrom = params.dateFrom ? new Date(params.dateFrom) : null;
|
|
502
|
-
const dateTo = params.dateTo ? new Date(params.dateTo) : null;
|
|
503
|
-
|
|
504
|
-
// Set dateFrom to start of day (00:00:00.000)
|
|
505
|
-
if (dateFrom) {
|
|
506
|
-
dateFrom.setHours(0, 0, 0, 0);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Set dateTo to end of day (23:59:59.999) for inclusive comparison
|
|
510
|
-
if (dateTo) {
|
|
511
|
-
dateTo.setHours(23, 59, 59, 999);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
transactions = transactions.filter((transaction) => {
|
|
515
|
-
const txDate = transaction.date instanceof Date
|
|
516
|
-
? transaction.date
|
|
517
|
-
: new Date(transaction.date);
|
|
518
|
-
const txTime = txDate.getTime();
|
|
519
|
-
|
|
520
|
-
if (dateFrom && txTime < dateFrom.getTime()) return false;
|
|
521
|
-
if (dateTo && txTime > dateTo.getTime()) return false;
|
|
522
|
-
return true;
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Sort by date
|
|
527
|
-
transactions.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
528
|
-
|
|
529
|
-
return transactions;
|
|
474
|
+
): Promise<ITransactionView[]> {
|
|
475
|
+
// v2: transaction-shaped line views from hashed journal entries,
|
|
476
|
+
// already date-filtered (inclusive day range) and sorted
|
|
477
|
+
return accountLineViews(accountNumber, this.skrType, params);
|
|
530
478
|
}
|
|
531
479
|
|
|
532
480
|
/**
|
|
@@ -744,41 +692,21 @@ export class Reports {
|
|
|
744
692
|
* Export to DATEV format
|
|
745
693
|
*/
|
|
746
694
|
public async exportToDATEV(params?: IReportParams): Promise<string> {
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
695
|
+
// EXTF 700 / Formatversion 13 over the v2 (hashed) journal entries
|
|
696
|
+
const dateFrom = params?.dateFrom ?? new Date('1970-01-01T00:00:00.000Z');
|
|
697
|
+
const dateTo = params?.dateTo ?? new Date('2999-12-31T00:00:00.000Z');
|
|
698
|
+
const drafts = await loadDraftsForExport(this.skrType, dateFrom, dateTo);
|
|
699
|
+
const rows = core.journalDraftsToDatevRows(drafts);
|
|
700
|
+
const { csv } = core.writeExtfBuchungsstapel(rows, {
|
|
701
|
+
createdAt: new Date(),
|
|
702
|
+
consultantNumber: 0,
|
|
703
|
+
clientNumber: 0,
|
|
704
|
+
fiscalYearStart: new Date(Date.UTC(dateFrom.getUTCFullYear(), 0, 1)),
|
|
705
|
+
dateFrom,
|
|
706
|
+
dateTo,
|
|
707
|
+
label: 'Buchungsstapel',
|
|
708
|
+
festschreibung: false,
|
|
752
709
|
});
|
|
753
|
-
|
|
754
|
-
const lines: string[] = [];
|
|
755
|
-
|
|
756
|
-
// DATEV header
|
|
757
|
-
lines.push('EXTF;510;21;"Buchungsstapel";1;;;;;;;;;;;;;;');
|
|
758
|
-
|
|
759
|
-
for (const transaction of transactions) {
|
|
760
|
-
const date = transaction.date
|
|
761
|
-
.toISOString()
|
|
762
|
-
.split('T')[0]
|
|
763
|
-
.replace(/-/g, '');
|
|
764
|
-
const line = [
|
|
765
|
-
transaction.amount.toFixed(2).replace('.', ','),
|
|
766
|
-
'S',
|
|
767
|
-
'EUR',
|
|
768
|
-
'',
|
|
769
|
-
'',
|
|
770
|
-
transaction.debitAccount,
|
|
771
|
-
transaction.creditAccount,
|
|
772
|
-
'',
|
|
773
|
-
date,
|
|
774
|
-
'',
|
|
775
|
-
transaction.description.substring(0, 60),
|
|
776
|
-
'',
|
|
777
|
-
].join(';');
|
|
778
|
-
|
|
779
|
-
lines.push(line);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
return lines.join('\n');
|
|
710
|
+
return csv;
|
|
783
711
|
}
|
|
784
712
|
}
|
package/ts/skr.database.ts
CHANGED
|
@@ -3,6 +3,9 @@ import type { IDatabaseConfig } from './skr.types.js';
|
|
|
3
3
|
|
|
4
4
|
let dbInstance: plugins.smartdata.SmartdataDb | null = null;
|
|
5
5
|
|
|
6
|
+
/** MongoDB ClientSession type, derived from smartdata to avoid a direct mongodb dependency. */
|
|
7
|
+
export type TDbSession = ReturnType<plugins.smartdata.SmartdataDb['startSession']>;
|
|
8
|
+
|
|
6
9
|
export const getDb = async (
|
|
7
10
|
config?: IDatabaseConfig,
|
|
8
11
|
): Promise<plugins.smartdata.SmartdataDb> => {
|
package/ts/skr.errors.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for the v2 posting path so callers can react precisely.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class ImmutableEntryError extends Error {
|
|
6
|
+
constructor(entryId: string) {
|
|
7
|
+
super(
|
|
8
|
+
`Journal entry ${entryId} is festgeschrieben (hashed) and immutable — corrections only via Storno entries`,
|
|
9
|
+
);
|
|
10
|
+
this.name = 'ImmutableEntryError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class PeriodLockedError extends Error {
|
|
15
|
+
constructor(skrType: string, period: string) {
|
|
16
|
+
super(`Period ${period} (${skrType}) is festgeschrieben — no further postings allowed`);
|
|
17
|
+
this.name = 'PeriodLockedError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class SequenceConflictError extends Error {
|
|
22
|
+
constructor(skrType: string, fiscalYear: number) {
|
|
23
|
+
super(
|
|
24
|
+
`Hash-chain head moved concurrently for ${skrType}/${fiscalYear} — transaction aborted`,
|
|
25
|
+
);
|
|
26
|
+
this.name = 'SequenceConflictError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class NotImplementedError extends Error {
|
|
31
|
+
constructor(feature: string) {
|
|
32
|
+
super(feature);
|
|
33
|
+
this.name = 'NotImplementedError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class TransactionsUnsupportedError extends Error {
|
|
38
|
+
constructor(detail: string) {
|
|
39
|
+
super(
|
|
40
|
+
`MongoDB deployment does not support transactions (replica set required) — filing-grade posting has no non-transactional fallback. ${detail}`,
|
|
41
|
+
);
|
|
42
|
+
this.name = 'TransactionsUnsupportedError';
|
|
43
|
+
}
|
|
44
|
+
}
|
package/ts/skr.export.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
3
4
|
import type { IAccountData, ITransactionData, IJournalEntry, TSKRType } from './skr.types.js';
|
|
4
5
|
|
|
5
6
|
export interface IExportOptions {
|
|
@@ -370,12 +371,8 @@ COMPLIANCE
|
|
|
370
371
|
Buffer.from(hash, 'hex')
|
|
371
372
|
);
|
|
372
373
|
|
|
373
|
-
//
|
|
374
|
-
const hashFn = (data: Buffer) =>
|
|
375
|
-
// Convert async to sync by using crypto directly
|
|
376
|
-
const crypto = require('crypto');
|
|
377
|
-
return crypto.createHash('sha256').update(data).digest();
|
|
378
|
-
};
|
|
374
|
+
// Sync SHA-256 for MerkleTree (ESM import — require() is unavailable here)
|
|
375
|
+
const hashFn = (data: Buffer) => createHash('sha256').update(data).digest();
|
|
379
376
|
|
|
380
377
|
const tree = new plugins.MerkleTree(leaves, hashFn, {
|
|
381
378
|
sortPairs: true
|