@fin.cx/skr 1.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.
Files changed (44) hide show
  1. package/dist_ts/index.d.ts +10 -0
  2. package/dist_ts/index.js +11 -0
  3. package/dist_ts/plugins.d.ts +5 -0
  4. package/dist_ts/plugins.js +7 -0
  5. package/dist_ts/skr.api.d.ts +181 -0
  6. package/dist_ts/skr.api.js +402 -0
  7. package/dist_ts/skr.classes.account.d.ts +34 -0
  8. package/dist_ts/skr.classes.account.js +224 -0
  9. package/dist_ts/skr.classes.chartofaccounts.d.ts +99 -0
  10. package/dist_ts/skr.classes.chartofaccounts.js +377 -0
  11. package/dist_ts/skr.classes.journalentry.d.ts +33 -0
  12. package/dist_ts/skr.classes.journalentry.js +296 -0
  13. package/dist_ts/skr.classes.ledger.d.ts +61 -0
  14. package/dist_ts/skr.classes.ledger.js +363 -0
  15. package/dist_ts/skr.classes.reports.d.ts +55 -0
  16. package/dist_ts/skr.classes.reports.js +514 -0
  17. package/dist_ts/skr.classes.transaction.d.ts +37 -0
  18. package/dist_ts/skr.classes.transaction.js +263 -0
  19. package/dist_ts/skr.database.d.ts +5 -0
  20. package/dist_ts/skr.database.js +28 -0
  21. package/dist_ts/skr.types.d.ts +126 -0
  22. package/dist_ts/skr.types.js +2 -0
  23. package/dist_ts/skr03.data.d.ts +18 -0
  24. package/dist_ts/skr03.data.js +890 -0
  25. package/dist_ts/skr04.data.d.ts +18 -0
  26. package/dist_ts/skr04.data.js +912 -0
  27. package/npmextra.json +17 -0
  28. package/package.json +61 -0
  29. package/readme.hints.md +3 -0
  30. package/readme.md +409 -0
  31. package/readme.plan.md +243 -0
  32. package/ts/index.ts +10 -0
  33. package/ts/plugins.ts +7 -0
  34. package/ts/skr.api.ts +533 -0
  35. package/ts/skr.classes.account.ts +238 -0
  36. package/ts/skr.classes.chartofaccounts.ts +508 -0
  37. package/ts/skr.classes.journalentry.ts +318 -0
  38. package/ts/skr.classes.ledger.ts +528 -0
  39. package/ts/skr.classes.reports.ts +721 -0
  40. package/ts/skr.classes.transaction.ts +300 -0
  41. package/ts/skr.database.ts +39 -0
  42. package/ts/skr.types.ts +154 -0
  43. package/ts/skr03.data.ts +901 -0
  44. package/ts/skr04.data.ts +923 -0
@@ -0,0 +1,508 @@
1
+ import * as plugins from './plugins.js';
2
+ import { getDb, closeDb } from './skr.database.js';
3
+ import { Account } from './skr.classes.account.js';
4
+ import { Transaction } from './skr.classes.transaction.js';
5
+ import { JournalEntry } from './skr.classes.journalentry.js';
6
+ import { SKR03_ACCOUNTS, SKR03_ACCOUNT_CLASSES } from './skr03.data.js';
7
+ import { SKR04_ACCOUNTS, SKR04_ACCOUNT_CLASSES } from './skr04.data.js';
8
+ import type {
9
+ IDatabaseConfig,
10
+ TSKRType,
11
+ IAccountData,
12
+ IAccountFilter,
13
+ ITransactionFilter,
14
+ ITransactionData,
15
+ IJournalEntry,
16
+ } from './skr.types.js';
17
+
18
+ export class ChartOfAccounts {
19
+ private logger: plugins.smartlog.Smartlog;
20
+ private initialized: boolean = false;
21
+ private skrType: TSKRType | null = null;
22
+
23
+ constructor(private config?: IDatabaseConfig) {
24
+ this.logger = new plugins.smartlog.Smartlog({
25
+ logContext: {
26
+ company: 'fin.cx',
27
+ companyunit: 'skr',
28
+ containerName: 'ChartOfAccounts',
29
+ environment: 'local',
30
+ runtime: 'node',
31
+ zone: 'local',
32
+ },
33
+ });
34
+ this.logger.enableConsole();
35
+ }
36
+
37
+ /**
38
+ * Initialize the database connection
39
+ */
40
+ public async init(): Promise<void> {
41
+ if (this.initialized) {
42
+ this.logger.log('info', 'ChartOfAccounts already initialized');
43
+ return;
44
+ }
45
+
46
+ if (!this.config) {
47
+ throw new Error('Database configuration required for initialization');
48
+ }
49
+
50
+ await getDb(this.config);
51
+ this.initialized = true;
52
+ this.logger.log('info', 'ChartOfAccounts initialized successfully');
53
+ }
54
+
55
+ /**
56
+ * Initialize SKR03 chart of accounts
57
+ */
58
+ public async initializeSKR03(): Promise<void> {
59
+ await this.init();
60
+
61
+ this.logger.log('info', 'Initializing SKR03 chart of accounts');
62
+
63
+ // Check if SKR03 accounts already exist
64
+ const existingAccounts = await Account.getInstances({ skrType: 'SKR03' });
65
+ if (existingAccounts.length > 0) {
66
+ this.logger.log(
67
+ 'info',
68
+ `SKR03 already initialized with ${existingAccounts.length} accounts`,
69
+ );
70
+ this.skrType = 'SKR03';
71
+ return;
72
+ }
73
+
74
+ // Create all SKR03 accounts
75
+ const accounts: Account[] = [];
76
+ for (const accountData of SKR03_ACCOUNTS) {
77
+ const account = await Account.createAccount(accountData);
78
+ accounts.push(account);
79
+ }
80
+
81
+ this.skrType = 'SKR03';
82
+ this.logger.log(
83
+ 'info',
84
+ `Successfully initialized SKR03 with ${accounts.length} accounts`,
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Initialize SKR04 chart of accounts
90
+ */
91
+ public async initializeSKR04(): Promise<void> {
92
+ await this.init();
93
+
94
+ this.logger.log('info', 'Initializing SKR04 chart of accounts');
95
+
96
+ // Check if SKR04 accounts already exist
97
+ const existingAccounts = await Account.getInstances({ skrType: 'SKR04' });
98
+ if (existingAccounts.length > 0) {
99
+ this.logger.log(
100
+ 'info',
101
+ `SKR04 already initialized with ${existingAccounts.length} accounts`,
102
+ );
103
+ this.skrType = 'SKR04';
104
+ return;
105
+ }
106
+
107
+ // Create all SKR04 accounts
108
+ const accounts: Account[] = [];
109
+ for (const accountData of SKR04_ACCOUNTS) {
110
+ const account = await Account.createAccount(accountData);
111
+ accounts.push(account);
112
+ }
113
+
114
+ this.skrType = 'SKR04';
115
+ this.logger.log(
116
+ 'info',
117
+ `Successfully initialized SKR04 with ${accounts.length} accounts`,
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Get the current SKR type
123
+ */
124
+ public getSKRType(): TSKRType | null {
125
+ return this.skrType;
126
+ }
127
+
128
+ /**
129
+ * Set the active SKR type
130
+ */
131
+ public setSKRType(skrType: TSKRType): void {
132
+ this.skrType = skrType;
133
+ }
134
+
135
+ /**
136
+ * Get account by number
137
+ */
138
+ public async getAccountByNumber(
139
+ accountNumber: string,
140
+ ): Promise<Account | null> {
141
+ if (!this.skrType) {
142
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
143
+ }
144
+
145
+ return await Account.getAccountByNumber(accountNumber, this.skrType);
146
+ }
147
+
148
+ /**
149
+ * Get accounts by class
150
+ */
151
+ public async getAccountsByClass(accountClass: number): Promise<Account[]> {
152
+ if (!this.skrType) {
153
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
154
+ }
155
+
156
+ return await Account.getAccountsByClass(accountClass, this.skrType);
157
+ }
158
+
159
+ /**
160
+ * Get accounts by type
161
+ */
162
+ public async getAccountsByType(
163
+ accountType: IAccountData['accountType'],
164
+ ): Promise<Account[]> {
165
+ if (!this.skrType) {
166
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
167
+ }
168
+
169
+ return await Account.getAccountsByType(accountType, this.skrType);
170
+ }
171
+
172
+ /**
173
+ * Create a custom account
174
+ */
175
+ public async createCustomAccount(
176
+ accountData: Partial<IAccountData>,
177
+ ): Promise<Account> {
178
+ if (!this.skrType) {
179
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
180
+ }
181
+
182
+ // Ensure the account uses the current SKR type
183
+ const fullAccountData: IAccountData = {
184
+ accountNumber: accountData.accountNumber || '',
185
+ accountName: accountData.accountName || '',
186
+ accountClass: accountData.accountClass || 0,
187
+ accountType: accountData.accountType || 'asset',
188
+ skrType: this.skrType,
189
+ description: accountData.description,
190
+ vatRate: accountData.vatRate,
191
+ isActive:
192
+ accountData.isActive !== undefined ? accountData.isActive : true,
193
+ };
194
+
195
+ // Validate account number doesn't already exist
196
+ const existing = await this.getAccountByNumber(
197
+ fullAccountData.accountNumber,
198
+ );
199
+ if (existing) {
200
+ throw new Error(
201
+ `Account ${fullAccountData.accountNumber} already exists`,
202
+ );
203
+ }
204
+
205
+ return await Account.createAccount(fullAccountData);
206
+ }
207
+
208
+ /**
209
+ * Update an existing account
210
+ */
211
+ public async updateAccount(
212
+ accountNumber: string,
213
+ updates: Partial<IAccountData>,
214
+ ): Promise<Account> {
215
+ const account = await this.getAccountByNumber(accountNumber);
216
+ if (!account) {
217
+ throw new Error(`Account ${accountNumber} not found`);
218
+ }
219
+
220
+ // Apply updates
221
+ if (updates.accountName !== undefined)
222
+ account.accountName = updates.accountName;
223
+ if (updates.description !== undefined)
224
+ account.description = updates.description;
225
+ if (updates.vatRate !== undefined) account.vatRate = updates.vatRate;
226
+ if (updates.isActive !== undefined) account.isActive = updates.isActive;
227
+
228
+ account.updatedAt = new Date();
229
+ await account.save();
230
+
231
+ return account;
232
+ }
233
+
234
+ /**
235
+ * Delete a custom account (only non-system accounts)
236
+ */
237
+ public async deleteAccount(accountNumber: string): Promise<void> {
238
+ const account = await this.getAccountByNumber(accountNumber);
239
+ if (!account) {
240
+ throw new Error(`Account ${accountNumber} not found`);
241
+ }
242
+
243
+ if (account.isSystemAccount) {
244
+ throw new Error(`Cannot delete system account ${accountNumber}`);
245
+ }
246
+
247
+ // Check if account has transactions
248
+ const transactions = await Transaction.getTransactionsByAccount(
249
+ accountNumber,
250
+ account.skrType,
251
+ );
252
+ if (transactions.length > 0) {
253
+ throw new Error(
254
+ `Cannot delete account ${accountNumber} with existing transactions`,
255
+ );
256
+ }
257
+
258
+ await account.delete();
259
+ }
260
+
261
+ /**
262
+ * Search accounts
263
+ */
264
+ public async searchAccounts(searchTerm: string): Promise<Account[]> {
265
+ return await Account.searchAccounts(searchTerm, this.skrType);
266
+ }
267
+
268
+ /**
269
+ * Get all accounts
270
+ */
271
+ public async getAllAccounts(filter?: IAccountFilter): Promise<Account[]> {
272
+ const query: any = {};
273
+
274
+ if (this.skrType) {
275
+ query.skrType = this.skrType;
276
+ }
277
+
278
+ if (filter) {
279
+ if (filter.accountClass !== undefined)
280
+ query.accountClass = filter.accountClass;
281
+ if (filter.accountType !== undefined)
282
+ query.accountType = filter.accountType;
283
+ if (filter.isActive !== undefined) query.isActive = filter.isActive;
284
+ }
285
+
286
+ const accounts = await Account.getInstances(query);
287
+
288
+ // Apply text search if provided
289
+ if (filter?.searchTerm) {
290
+ const lowerSearchTerm = filter.searchTerm.toLowerCase();
291
+ return accounts.filter(
292
+ (account) =>
293
+ account.accountNumber.includes(filter.searchTerm) ||
294
+ account.accountName.toLowerCase().includes(lowerSearchTerm) ||
295
+ account.description.toLowerCase().includes(lowerSearchTerm),
296
+ );
297
+ }
298
+
299
+ return accounts;
300
+ }
301
+
302
+ /**
303
+ * Post a simple transaction
304
+ */
305
+ public async postTransaction(
306
+ transactionData: ITransactionData,
307
+ ): Promise<Transaction> {
308
+ if (!this.skrType) {
309
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
310
+ }
311
+
312
+ // Ensure the transaction uses the current SKR type
313
+ const fullTransactionData: ITransactionData = {
314
+ ...transactionData,
315
+ skrType: this.skrType,
316
+ };
317
+
318
+ return await Transaction.createTransaction(fullTransactionData);
319
+ }
320
+
321
+ /**
322
+ * Post a journal entry
323
+ */
324
+ public async postJournalEntry(
325
+ journalData: IJournalEntry,
326
+ ): Promise<JournalEntry> {
327
+ if (!this.skrType) {
328
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
329
+ }
330
+
331
+ // Ensure the journal entry uses the current SKR type
332
+ const fullJournalData: IJournalEntry = {
333
+ ...journalData,
334
+ skrType: this.skrType,
335
+ };
336
+
337
+ const journalEntry = await JournalEntry.createJournalEntry(fullJournalData);
338
+ await journalEntry.post();
339
+
340
+ return journalEntry;
341
+ }
342
+
343
+ /**
344
+ * Get transactions for an account
345
+ */
346
+ public async getAccountTransactions(
347
+ accountNumber: string,
348
+ ): Promise<Transaction[]> {
349
+ if (!this.skrType) {
350
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
351
+ }
352
+
353
+ return await Transaction.getTransactionsByAccount(
354
+ accountNumber,
355
+ this.skrType,
356
+ );
357
+ }
358
+
359
+ /**
360
+ * Get transactions by filter
361
+ */
362
+ public async getTransactions(
363
+ filter?: ITransactionFilter,
364
+ ): Promise<Transaction[]> {
365
+ if (!this.skrType) {
366
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
367
+ }
368
+
369
+ const query: any = {
370
+ skrType: this.skrType,
371
+ status: 'posted',
372
+ };
373
+
374
+ if (filter) {
375
+ if (filter.dateFrom || filter.dateTo) {
376
+ query.date = {};
377
+ if (filter.dateFrom) query.date.$gte = filter.dateFrom;
378
+ if (filter.dateTo) query.date.$lte = filter.dateTo;
379
+ }
380
+
381
+ if (filter.accountNumber) {
382
+ query.$or = [
383
+ { debitAccount: filter.accountNumber },
384
+ { creditAccount: filter.accountNumber },
385
+ ];
386
+ }
387
+
388
+ if (filter.minAmount || filter.maxAmount) {
389
+ query.amount = {};
390
+ if (filter.minAmount) query.amount.$gte = filter.minAmount;
391
+ if (filter.maxAmount) query.amount.$lte = filter.maxAmount;
392
+ }
393
+ }
394
+
395
+ const transactions = await Transaction.getInstances(query);
396
+
397
+ // Apply text search if provided
398
+ if (filter?.searchTerm) {
399
+ const lowerSearchTerm = filter.searchTerm.toLowerCase();
400
+ return transactions.filter(
401
+ (transaction) =>
402
+ transaction.description.toLowerCase().includes(lowerSearchTerm) ||
403
+ transaction.reference.toLowerCase().includes(lowerSearchTerm),
404
+ );
405
+ }
406
+
407
+ return transactions;
408
+ }
409
+
410
+ /**
411
+ * Reverse a transaction
412
+ */
413
+ public async reverseTransaction(transactionId: string): Promise<Transaction> {
414
+ const transaction = await Transaction.getTransactionById(transactionId);
415
+ if (!transaction) {
416
+ throw new Error(`Transaction ${transactionId} not found`);
417
+ }
418
+
419
+ return await transaction.reverseTransaction();
420
+ }
421
+
422
+ /**
423
+ * Get account class description
424
+ */
425
+ public getAccountClassDescription(accountClass: number): string {
426
+ if (!this.skrType) {
427
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
428
+ }
429
+
430
+ const classes =
431
+ this.skrType === 'SKR03' ? SKR03_ACCOUNT_CLASSES : SKR04_ACCOUNT_CLASSES;
432
+ return (
433
+ classes[accountClass as keyof typeof classes] || `Class ${accountClass}`
434
+ );
435
+ }
436
+
437
+ /**
438
+ * Import accounts from CSV
439
+ */
440
+ public async importAccountsFromCSV(csvContent: string): Promise<number> {
441
+ if (!this.skrType) {
442
+ throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
443
+ }
444
+
445
+ const lines = csvContent.split('\n').filter((line) => line.trim());
446
+ let importedCount = 0;
447
+
448
+ for (const line of lines) {
449
+ // Parse CSV line (expecting format: "account";"name";"description";"type";"active")
450
+ const parts = line
451
+ .split(';')
452
+ .map((part) => part.replace(/"/g, '').trim());
453
+
454
+ if (parts.length >= 5) {
455
+ const accountData: IAccountData = {
456
+ accountNumber: parts[0],
457
+ accountName: parts[1],
458
+ accountClass: parseInt(parts[0][0]),
459
+ accountType: parts[3] as IAccountData['accountType'],
460
+ skrType: this.skrType,
461
+ description: parts[2],
462
+ isActive:
463
+ parts[4].toLowerCase() === 'standard' ||
464
+ parts[4].toLowerCase() === 'active',
465
+ };
466
+
467
+ try {
468
+ await this.createCustomAccount(accountData);
469
+ importedCount++;
470
+ } catch (error) {
471
+ this.logger.log(
472
+ 'warn',
473
+ `Failed to import account ${parts[0]}: ${error.message}`,
474
+ );
475
+ }
476
+ }
477
+ }
478
+
479
+ return importedCount;
480
+ }
481
+
482
+ /**
483
+ * Export accounts to CSV
484
+ */
485
+ public async exportAccountsToCSV(): Promise<string> {
486
+ const accounts = await this.getAllAccounts();
487
+
488
+ const csvLines: string[] = [];
489
+ csvLines.push('"Account";"Name";"Description";"Type";"Active"');
490
+
491
+ for (const account of accounts) {
492
+ csvLines.push(
493
+ `"${account.accountNumber}";"${account.accountName}";"${account.description}";"${account.accountType}";"${account.isActive ? 'Active' : 'Inactive'}"`,
494
+ );
495
+ }
496
+
497
+ return csvLines.join('\n');
498
+ }
499
+
500
+ /**
501
+ * Close the database connection
502
+ */
503
+ public async close(): Promise<void> {
504
+ await closeDb();
505
+ this.initialized = false;
506
+ this.logger.log('info', 'ChartOfAccounts closed');
507
+ }
508
+ }