@moatless/bookkeeping 0.1.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 (93) hide show
  1. package/dist/accounting/index.d.ts +9 -0
  2. package/dist/accounting/index.js +14 -0
  3. package/dist/accounting/line-generator.d.ts +34 -0
  4. package/dist/accounting/line-generator.js +136 -0
  5. package/dist/accounting/tax-codes.d.ts +32 -0
  6. package/dist/accounting/tax-codes.js +279 -0
  7. package/dist/accounting/traktamente-rates.d.ts +48 -0
  8. package/dist/accounting/traktamente-rates.js +325 -0
  9. package/dist/accounting/types.d.ts +69 -0
  10. package/dist/accounting/types.js +5 -0
  11. package/dist/accounting/validation.d.ts +41 -0
  12. package/dist/accounting/validation.js +118 -0
  13. package/dist/auth/fortnox-login.d.ts +15 -0
  14. package/dist/auth/fortnox-login.js +170 -0
  15. package/dist/auth/index.d.ts +3 -0
  16. package/dist/auth/index.js +3 -0
  17. package/dist/auth/prompts.d.ts +6 -0
  18. package/dist/auth/prompts.js +56 -0
  19. package/dist/auth/token-store.d.ts +19 -0
  20. package/dist/auth/token-store.js +54 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +21 -0
  23. package/dist/progress/index.d.ts +1 -0
  24. package/dist/progress/index.js +1 -0
  25. package/dist/progress/sync-progress.d.ts +31 -0
  26. package/dist/progress/sync-progress.js +65 -0
  27. package/dist/services/bokio-journal.d.ts +29 -0
  28. package/dist/services/bokio-journal.js +175 -0
  29. package/dist/services/document-download.d.ts +46 -0
  30. package/dist/services/document-download.js +105 -0
  31. package/dist/services/fortnox-inbox.d.ts +18 -0
  32. package/dist/services/fortnox-inbox.js +150 -0
  33. package/dist/services/fortnox-journal.d.ts +22 -0
  34. package/dist/services/fortnox-journal.js +166 -0
  35. package/dist/services/index.d.ts +6 -0
  36. package/dist/services/index.js +6 -0
  37. package/dist/services/journal-sync.d.ts +23 -0
  38. package/dist/services/journal-sync.js +124 -0
  39. package/dist/services/journal.service.d.ts +45 -0
  40. package/dist/services/journal.service.js +204 -0
  41. package/dist/storage/filesystem.d.ts +49 -0
  42. package/dist/storage/filesystem.js +122 -0
  43. package/dist/storage/index.d.ts +2 -0
  44. package/dist/storage/index.js +1 -0
  45. package/dist/storage/interface.d.ts +48 -0
  46. package/dist/storage/interface.js +5 -0
  47. package/dist/sync-types.d.ts +61 -0
  48. package/dist/sync-types.js +1 -0
  49. package/dist/transformers/bokio.d.ts +10 -0
  50. package/dist/transformers/bokio.js +56 -0
  51. package/dist/transformers/fortnox.d.ts +6 -0
  52. package/dist/transformers/fortnox.js +39 -0
  53. package/dist/transformers/index.d.ts +3 -0
  54. package/dist/transformers/index.js +2 -0
  55. package/dist/types/discarded-item.d.ts +29 -0
  56. package/dist/types/discarded-item.js +1 -0
  57. package/dist/types/document.d.ts +63 -0
  58. package/dist/types/document.js +9 -0
  59. package/dist/types/exported-document.d.ts +61 -0
  60. package/dist/types/exported-document.js +9 -0
  61. package/dist/types/exported-fiscal-year.d.ts +10 -0
  62. package/dist/types/exported-fiscal-year.js +1 -0
  63. package/dist/types/exported-inbox-document.d.ts +14 -0
  64. package/dist/types/exported-inbox-document.js +10 -0
  65. package/dist/types/fiscal-year.d.ts +10 -0
  66. package/dist/types/fiscal-year.js +1 -0
  67. package/dist/types/index.d.ts +10 -0
  68. package/dist/types/index.js +10 -0
  69. package/dist/types/journal-entry.d.ts +79 -0
  70. package/dist/types/journal-entry.js +12 -0
  71. package/dist/types/ledger-account.d.ts +5 -0
  72. package/dist/types/ledger-account.js +1 -0
  73. package/dist/utils/file-namer.d.ts +48 -0
  74. package/dist/utils/file-namer.js +80 -0
  75. package/dist/utils/git.d.ts +9 -0
  76. package/dist/utils/git.js +41 -0
  77. package/dist/utils/index.d.ts +6 -0
  78. package/dist/utils/index.js +6 -0
  79. package/dist/utils/paths.d.ts +17 -0
  80. package/dist/utils/paths.js +24 -0
  81. package/dist/utils/retry.d.ts +17 -0
  82. package/dist/utils/retry.js +48 -0
  83. package/dist/utils/templates.d.ts +12 -0
  84. package/dist/utils/templates.js +222 -0
  85. package/dist/utils/yaml.d.ts +12 -0
  86. package/dist/utils/yaml.js +47 -0
  87. package/dist/yaml/entry-helpers.d.ts +57 -0
  88. package/dist/yaml/entry-helpers.js +125 -0
  89. package/dist/yaml/index.d.ts +2 -0
  90. package/dist/yaml/index.js +2 -0
  91. package/dist/yaml/yaml-serializer.d.ts +21 -0
  92. package/dist/yaml/yaml-serializer.js +60 -0
  93. package/package.json +37 -0
@@ -0,0 +1,45 @@
1
+ import type { JournalEntry } from "../types";
2
+ import type { IStorageService } from "../storage/interface";
3
+ export interface JournalEntryRecord {
4
+ entry: JournalEntry;
5
+ entryPath: string;
6
+ entryDir: string;
7
+ }
8
+ export interface UpsertJournalEntryResult {
9
+ action: "created" | "updated" | "unchanged";
10
+ entryDir: string;
11
+ }
12
+ /**
13
+ * Journal entry service - handles journal entries in storage
14
+ * Works with both GitHub and filesystem storage backends
15
+ */
16
+ export declare class JournalService {
17
+ private storage;
18
+ constructor(storage: IStorageService);
19
+ private static getEntryKey;
20
+ /**
21
+ * List all journal entries from a fiscal year
22
+ * Returns entries with their externalId for sync matching
23
+ */
24
+ listJournalEntries(fiscalYear: number): Promise<Map<string, JournalEntry>>;
25
+ /**
26
+ * List all journal entries from a fiscal year with their paths
27
+ */
28
+ listJournalEntryRecords(fiscalYear: number): Promise<Map<string, JournalEntryRecord>>;
29
+ /**
30
+ * Create a new journal entry
31
+ */
32
+ createJournalEntry(fiscalYear: number, entry: JournalEntry): Promise<void>;
33
+ /**
34
+ * Update an existing journal entry
35
+ */
36
+ updateJournalEntry(fiscalYear: number, existingEntry: JournalEntry, updatedEntry: JournalEntry): Promise<void>;
37
+ /**
38
+ * Create or update a journal entry, returning the entry directory path used.
39
+ */
40
+ upsertJournalEntry(fiscalYear: number, entry: JournalEntry, existingRecord?: JournalEntryRecord): Promise<UpsertJournalEntryResult>;
41
+ /**
42
+ * Check if two entries are equal (for change detection)
43
+ */
44
+ static entriesEqual(a: JournalEntry, b: JournalEntry): boolean;
45
+ }
@@ -0,0 +1,204 @@
1
+ import { journalEntryDirFromPath, journalEntryPath } from "../utils/file-namer";
2
+ import { parseYaml, toYaml } from "../utils/yaml";
3
+ /**
4
+ * Journal entry service - handles journal entries in storage
5
+ * Works with both GitHub and filesystem storage backends
6
+ */
7
+ export class JournalService {
8
+ storage;
9
+ constructor(storage) {
10
+ this.storage = storage;
11
+ }
12
+ static getEntryKey(entry) {
13
+ return (entry.externalId ??
14
+ `${entry.series ?? ""}-${entry.entryNumber}`);
15
+ }
16
+ /**
17
+ * List all journal entries from a fiscal year
18
+ * Returns entries with their externalId for sync matching
19
+ */
20
+ async listJournalEntries(fiscalYear) {
21
+ const records = await this.listJournalEntryRecords(fiscalYear);
22
+ const entriesMap = new Map();
23
+ for (const [key, record] of records) {
24
+ entriesMap.set(key, record.entry);
25
+ }
26
+ return entriesMap;
27
+ }
28
+ /**
29
+ * List all journal entries from a fiscal year with their paths
30
+ */
31
+ async listJournalEntryRecords(fiscalYear) {
32
+ const fiscalYearDir = `journal-entries/FY-${fiscalYear}`;
33
+ const entriesMap = new Map();
34
+ try {
35
+ const directories = await this.storage.listDirectory(fiscalYearDir);
36
+ for (const dir of directories) {
37
+ if (dir.type !== "dir" || dir.name.startsWith("_")) {
38
+ continue;
39
+ }
40
+ const entryPath = `${dir.path}/entry.yaml`;
41
+ try {
42
+ const { content } = await this.storage.readFile(entryPath);
43
+ const journalEntry = parseYaml(content);
44
+ const key = JournalService.getEntryKey(journalEntry);
45
+ entriesMap.set(key, {
46
+ entry: journalEntry,
47
+ entryPath,
48
+ entryDir: journalEntryDirFromPath(entryPath),
49
+ });
50
+ }
51
+ catch (error) {
52
+ console.error(`Failed to read ${entryPath}:`, error);
53
+ }
54
+ }
55
+ }
56
+ catch (_error) {
57
+ // Directory might not exist yet
58
+ console.warn(`Fiscal year directory not found: ${fiscalYearDir}`);
59
+ }
60
+ return entriesMap;
61
+ }
62
+ /**
63
+ * Create a new journal entry
64
+ */
65
+ async createJournalEntry(fiscalYear, entry) {
66
+ const yaml = toYaml(entry);
67
+ const filePath = journalEntryPath(fiscalYear, entry.series ?? null, entry.entryNumber, entry.entryDate, entry.description);
68
+ const message = `Create journal entry: ${entry.series ?? ""}${entry.entryNumber} - ${entry.description}`;
69
+ await this.storage.writeFile(filePath, yaml, { message });
70
+ }
71
+ /**
72
+ * Update an existing journal entry
73
+ */
74
+ async updateJournalEntry(fiscalYear, existingEntry, updatedEntry) {
75
+ const yaml = toYaml(updatedEntry);
76
+ // Use existing entry's path info (series, number, date, description)
77
+ const existingPath = journalEntryPath(fiscalYear, existingEntry.series ?? null, existingEntry.entryNumber, existingEntry.entryDate, existingEntry.description);
78
+ const updatedPath = journalEntryPath(fiscalYear, updatedEntry.series ?? null, updatedEntry.entryNumber, updatedEntry.entryDate, updatedEntry.description);
79
+ let filePath = existingPath;
80
+ if (updatedPath !== existingPath) {
81
+ const renameDirectory = this.storage.renameDirectory;
82
+ if (renameDirectory) {
83
+ const existingDir = journalEntryDirFromPath(existingPath);
84
+ const updatedDir = journalEntryDirFromPath(updatedPath);
85
+ if (existingDir !== updatedDir) {
86
+ try {
87
+ await renameDirectory(existingDir, updatedDir);
88
+ filePath = updatedPath;
89
+ }
90
+ catch {
91
+ filePath = existingPath;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ try {
97
+ const { sha } = await this.storage.readFile(filePath);
98
+ const message = `Update journal entry: ${updatedEntry.series ?? ""}${updatedEntry.entryNumber} - ${updatedEntry.description}`;
99
+ await this.storage.writeFile(filePath, yaml, { message, sha });
100
+ }
101
+ catch (_error) {
102
+ // If file doesn't exist at expected path, create it
103
+ const message = `Update journal entry: ${updatedEntry.series ?? ""}${updatedEntry.entryNumber} - ${updatedEntry.description}`;
104
+ await this.storage.writeFile(filePath, yaml, { message });
105
+ }
106
+ }
107
+ /**
108
+ * Create or update a journal entry, returning the entry directory path used.
109
+ */
110
+ async upsertJournalEntry(fiscalYear, entry, existingRecord) {
111
+ const entryPath = journalEntryPath(fiscalYear, entry.series ?? null, entry.entryNumber, entry.entryDate, entry.description);
112
+ const entryDir = journalEntryDirFromPath(entryPath);
113
+ if (!existingRecord) {
114
+ const message = `Create journal entry: ${entry.series ?? ""}${entry.entryNumber} - ${entry.description}`;
115
+ await this.storage.writeFile(entryPath, toYaml(entry), { message });
116
+ return { action: "created", entryDir };
117
+ }
118
+ if (JournalService.entriesEqual(existingRecord.entry, entry)) {
119
+ return { action: "unchanged", entryDir: existingRecord.entryDir };
120
+ }
121
+ let targetPath = existingRecord.entryPath;
122
+ let targetDir = existingRecord.entryDir;
123
+ if (existingRecord.entryPath !== entryPath) {
124
+ const renameDirectory = this.storage.renameDirectory;
125
+ if (renameDirectory) {
126
+ if (existingRecord.entryDir !== entryDir) {
127
+ try {
128
+ await renameDirectory(existingRecord.entryDir, entryDir);
129
+ targetPath = entryPath;
130
+ targetDir = entryDir;
131
+ }
132
+ catch {
133
+ targetPath = existingRecord.entryPath;
134
+ targetDir = existingRecord.entryDir;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ const yaml = toYaml(entry);
140
+ const message = `Update journal entry: ${entry.series ?? ""}${entry.entryNumber} - ${entry.description}`;
141
+ try {
142
+ const { sha } = await this.storage.readFile(targetPath);
143
+ await this.storage.writeFile(targetPath, yaml, { message, sha });
144
+ }
145
+ catch {
146
+ await this.storage.writeFile(targetPath, yaml, { message });
147
+ }
148
+ return { action: "updated", entryDir: targetDir };
149
+ }
150
+ /**
151
+ * Check if two entries are equal (for change detection)
152
+ */
153
+ static entriesEqual(a, b) {
154
+ // Compare key fields
155
+ if ((a.series ?? null) !== (b.series ?? null) ||
156
+ a.entryNumber !== b.entryNumber ||
157
+ a.description !== b.description ||
158
+ a.entryDate !== b.entryDate ||
159
+ a.status !== b.status ||
160
+ a.currency !== b.currency ||
161
+ a.totalDebit.amount !== b.totalDebit.amount ||
162
+ a.totalDebit.currency !== b.totalDebit.currency ||
163
+ (a.totalDebit.originalAmount ?? null) !== (b.totalDebit.originalAmount ?? null) ||
164
+ (a.totalDebit.originalCurrency ?? null) !== (b.totalDebit.originalCurrency ?? null) ||
165
+ a.totalCredit.amount !== b.totalCredit.amount ||
166
+ a.totalCredit.currency !== b.totalCredit.currency ||
167
+ (a.totalCredit.originalAmount ?? null) !== (b.totalCredit.originalAmount ?? null) ||
168
+ (a.totalCredit.originalCurrency ?? null) !== (b.totalCredit.originalCurrency ?? null) ||
169
+ (a.voucherNumber ?? null) !== (b.voucherNumber ?? null) ||
170
+ (a.voucherSeriesCode ?? null) !== (b.voucherSeriesCode ?? null) ||
171
+ (a.reversingEntryExternalId ?? null) !== (b.reversingEntryExternalId ?? null) ||
172
+ (a.reversedByEntryExternalId ?? null) !== (b.reversedByEntryExternalId ?? null)) {
173
+ return false;
174
+ }
175
+ // Compare lines count
176
+ if (a.lines.length !== b.lines.length) {
177
+ return false;
178
+ }
179
+ // Compare each line
180
+ for (let i = 0; i < a.lines.length; i++) {
181
+ const lineA = a.lines[i];
182
+ const lineB = b.lines[i];
183
+ if (!lineA || !lineB)
184
+ return false;
185
+ if (lineA.lineNumber !== lineB.lineNumber ||
186
+ lineA.account !== lineB.account ||
187
+ lineA.debit.amount !== lineB.debit.amount ||
188
+ lineA.debit.currency !== lineB.debit.currency ||
189
+ (lineA.debit.originalAmount ?? null) !== (lineB.debit.originalAmount ?? null) ||
190
+ (lineA.debit.originalCurrency ?? null) !== (lineB.debit.originalCurrency ?? null) ||
191
+ lineA.credit.amount !== lineB.credit.amount ||
192
+ lineA.credit.currency !== lineB.credit.currency ||
193
+ (lineA.credit.originalAmount ?? null) !== (lineB.credit.originalAmount ?? null) ||
194
+ (lineA.credit.originalCurrency ?? null) !== (lineB.credit.originalCurrency ?? null) ||
195
+ (lineA.memo ?? null) !== (lineB.memo ?? null) ||
196
+ (lineA.taxCode ?? null) !== (lineB.taxCode ?? null) ||
197
+ (lineA.costCenter ?? null) !== (lineB.costCenter ?? null) ||
198
+ (lineA.ledgerAccountName ?? null) !== (lineB.ledgerAccountName ?? null)) {
199
+ return false;
200
+ }
201
+ }
202
+ return true;
203
+ }
204
+ }
@@ -0,0 +1,49 @@
1
+ import type { IStorageService, FileContent, StorageEntry, WriteOptions } from "./interface";
2
+ /**
3
+ * Filesystem implementation of storage service.
4
+ * Uses local filesystem for file operations (for CLI tools like gitops).
5
+ */
6
+ export declare class FilesystemStorageService implements IStorageService {
7
+ private basePath;
8
+ constructor(basePath: string);
9
+ /**
10
+ * Resolve a relative path to an absolute path
11
+ */
12
+ private resolvePath;
13
+ /**
14
+ * Generate a SHA-like hash from file content for version tracking
15
+ */
16
+ private contentHash;
17
+ /**
18
+ * Read a file from the filesystem
19
+ */
20
+ readFile(filePath: string): Promise<FileContent>;
21
+ /**
22
+ * Write a file to the filesystem
23
+ */
24
+ writeFile(filePath: string, content: string, _options?: WriteOptions): Promise<void>;
25
+ /**
26
+ * Write a binary file to the filesystem
27
+ */
28
+ writeBinaryFile(filePath: string, content: Uint8Array): Promise<void>;
29
+ /**
30
+ * List contents of a directory
31
+ */
32
+ listDirectory(dirPath: string): Promise<StorageEntry[]>;
33
+ /**
34
+ * Delete a file from the filesystem
35
+ */
36
+ deleteFile(filePath: string, _sha: string, _message?: string): Promise<void>;
37
+ /**
38
+ * Filesystem doesn't support public URLs
39
+ */
40
+ getPublicUrl(_path: string): string | null;
41
+ /**
42
+ * Check if a file exists
43
+ */
44
+ exists(filePath: string): Promise<boolean>;
45
+ /**
46
+ * Rename a directory
47
+ */
48
+ renameDirectory(oldPath: string, newPath: string): Promise<void>;
49
+ }
@@ -0,0 +1,122 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import * as crypto from "node:crypto";
4
+ /**
5
+ * Filesystem implementation of storage service.
6
+ * Uses local filesystem for file operations (for CLI tools like gitops).
7
+ */
8
+ export class FilesystemStorageService {
9
+ basePath;
10
+ constructor(basePath) {
11
+ this.basePath = basePath;
12
+ }
13
+ /**
14
+ * Resolve a relative path to an absolute path
15
+ */
16
+ resolvePath(relativePath) {
17
+ return path.join(this.basePath, relativePath);
18
+ }
19
+ /**
20
+ * Generate a SHA-like hash from file content for version tracking
21
+ */
22
+ contentHash(content) {
23
+ return crypto.createHash("sha1").update(content).digest("hex");
24
+ }
25
+ /**
26
+ * Read a file from the filesystem
27
+ */
28
+ async readFile(filePath) {
29
+ const absolutePath = this.resolvePath(filePath);
30
+ const content = await fs.readFile(absolutePath, "utf-8");
31
+ return {
32
+ content,
33
+ sha: this.contentHash(content),
34
+ };
35
+ }
36
+ /**
37
+ * Write a file to the filesystem
38
+ */
39
+ async writeFile(filePath, content, _options) {
40
+ const absolutePath = this.resolvePath(filePath);
41
+ // Ensure directory exists
42
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
43
+ await fs.writeFile(absolutePath, content, "utf-8");
44
+ }
45
+ /**
46
+ * Write a binary file to the filesystem
47
+ */
48
+ async writeBinaryFile(filePath, content) {
49
+ const absolutePath = this.resolvePath(filePath);
50
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
51
+ await fs.writeFile(absolutePath, content);
52
+ }
53
+ /**
54
+ * List contents of a directory
55
+ */
56
+ async listDirectory(dirPath) {
57
+ const absolutePath = this.resolvePath(dirPath);
58
+ const entries = [];
59
+ try {
60
+ const items = await fs.readdir(absolutePath, { withFileTypes: true });
61
+ for (const item of items) {
62
+ if (item.isFile()) {
63
+ entries.push({
64
+ name: item.name,
65
+ type: "file",
66
+ path: path.join(dirPath, item.name),
67
+ });
68
+ }
69
+ else if (item.isDirectory()) {
70
+ entries.push({
71
+ name: item.name,
72
+ type: "dir",
73
+ path: path.join(dirPath, item.name),
74
+ });
75
+ }
76
+ }
77
+ }
78
+ catch (error) {
79
+ // Directory doesn't exist - return empty array
80
+ if (error.code === "ENOENT") {
81
+ return [];
82
+ }
83
+ throw error;
84
+ }
85
+ return entries;
86
+ }
87
+ /**
88
+ * Delete a file from the filesystem
89
+ */
90
+ async deleteFile(filePath, _sha, _message) {
91
+ const absolutePath = this.resolvePath(filePath);
92
+ await fs.unlink(absolutePath);
93
+ }
94
+ /**
95
+ * Filesystem doesn't support public URLs
96
+ */
97
+ getPublicUrl(_path) {
98
+ return null;
99
+ }
100
+ /**
101
+ * Check if a file exists
102
+ */
103
+ async exists(filePath) {
104
+ try {
105
+ await fs.access(this.resolvePath(filePath));
106
+ return true;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ /**
113
+ * Rename a directory
114
+ */
115
+ async renameDirectory(oldPath, newPath) {
116
+ const absoluteOldPath = this.resolvePath(oldPath);
117
+ const absoluteNewPath = this.resolvePath(newPath);
118
+ // Ensure parent directory of new path exists
119
+ await fs.mkdir(path.dirname(absoluteNewPath), { recursive: true });
120
+ await fs.rename(absoluteOldPath, absoluteNewPath);
121
+ }
122
+ }
@@ -0,0 +1,2 @@
1
+ export type { IStorageService, StorageEntry, FileContent, WriteOptions, } from "./interface";
2
+ export { FilesystemStorageService } from "./filesystem";
@@ -0,0 +1 @@
1
+ export { FilesystemStorageService } from "./filesystem";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Storage abstraction interface.
3
+ * Allows swapping between different storage backends (GitHub, Filesystem, Azure Blob, etc.)
4
+ */
5
+ export interface StorageEntry {
6
+ name: string;
7
+ type: "file" | "dir";
8
+ path: string;
9
+ }
10
+ export interface FileContent {
11
+ /** Text content (binary files should be Base64-encoded by caller) */
12
+ content: string;
13
+ /** Version identifier for optimistic locking (e.g., Git SHA, ETag, mtime) */
14
+ sha: string;
15
+ }
16
+ export interface WriteOptions {
17
+ /** Commit message (GitHub) or audit log entry (other backends) */
18
+ message?: string;
19
+ /** Required for updates - ensures no concurrent modifications */
20
+ sha?: string;
21
+ }
22
+ export interface IStorageService {
23
+ /**
24
+ * Read a file's content and version identifier.
25
+ * @throws Error if file does not exist
26
+ */
27
+ readFile(path: string): Promise<FileContent>;
28
+ /**
29
+ * Write a file (create or update).
30
+ * For updates, provide sha in options for optimistic locking.
31
+ */
32
+ writeFile(path: string, content: string, options?: WriteOptions): Promise<void>;
33
+ /**
34
+ * List contents of a directory.
35
+ * Returns only files and directories (no symlinks or submodules).
36
+ */
37
+ listDirectory(path: string): Promise<StorageEntry[]>;
38
+ /**
39
+ * Delete a file.
40
+ * Requires sha for optimistic locking.
41
+ */
42
+ deleteFile(path: string, sha: string, message?: string): Promise<void>;
43
+ /**
44
+ * Get a public URL for a file, if supported.
45
+ * Returns null if the storage backend doesn't support public URLs.
46
+ */
47
+ getPublicUrl(path: string): string | null;
48
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Storage abstraction interface.
3
+ * Allows swapping between different storage backends (GitHub, Filesystem, Azure Blob, etc.)
4
+ */
5
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Result stats for sync operations
3
+ */
4
+ export interface SyncStats {
5
+ fetched: number;
6
+ written: number;
7
+ skipped: number;
8
+ errors: number;
9
+ }
10
+ /**
11
+ * Stats per fiscal year (more detailed)
12
+ */
13
+ export interface FiscalYearStats {
14
+ year: number;
15
+ fetched: number;
16
+ created: number;
17
+ updated: number;
18
+ unchanged: number;
19
+ failed: number;
20
+ }
21
+ /**
22
+ * Error entry for tracking issues
23
+ */
24
+ export interface SyncError {
25
+ year: number;
26
+ externalId: string;
27
+ message: string;
28
+ }
29
+ /**
30
+ * Complete sync result
31
+ */
32
+ export interface SyncJournalResult {
33
+ success: boolean;
34
+ fiscalYears: FiscalYearStats[];
35
+ totals: {
36
+ fetched: number;
37
+ created: number;
38
+ updated: number;
39
+ unchanged: number;
40
+ failed: number;
41
+ };
42
+ errors: SyncError[];
43
+ durationMs: number;
44
+ }
45
+ /**
46
+ * Fiscal year info for sync operations
47
+ */
48
+ export interface FiscalYearInfo {
49
+ /** External ID from provider (used for API calls) */
50
+ externalId: number;
51
+ /** Calendar year (used for file paths) */
52
+ year: number;
53
+ }
54
+ /**
55
+ * Fiscal year with dates (from database)
56
+ */
57
+ export interface FiscalYearWithDates {
58
+ name: string | null;
59
+ start_date: string;
60
+ end_date: string;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { CreateJournalEntryRequest, JournalEntry as BokioJournalEntry } from "@moatless/bokio-client";
2
+ import type { JournalEntry } from "../types";
3
+ /**
4
+ * Transform a Bokio journal entry to a JournalEntry
5
+ */
6
+ export declare function mapBokioEntryToJournalEntry(entry: BokioJournalEntry): JournalEntry;
7
+ /**
8
+ * Transform a local JournalEntry to a Bokio CreateJournalEntryRequest
9
+ */
10
+ export declare function mapJournalEntryToBokioRequest(entry: JournalEntry): CreateJournalEntryRequest;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Transform a Bokio journal entry to a JournalEntry
3
+ */
4
+ export function mapBokioEntryToJournalEntry(entry) {
5
+ // Parse voucher number and series from journalEntryNumber (e.g., "A123" -> series: "A", number: 123)
6
+ const voucherNumberMatch = entry.journalEntryNumber.match(/\d+/);
7
+ const voucherNumber = voucherNumberMatch
8
+ ? parseInt(voucherNumberMatch[0], 10)
9
+ : 0;
10
+ const seriesCode = entry.journalEntryNumber.replace(/\d+/g, "") || undefined;
11
+ const lines = entry.items.map((item, idx) => ({
12
+ lineNumber: idx + 1,
13
+ account: String(item.account),
14
+ debit: { amount: item.debit, currency: "SEK" },
15
+ credit: { amount: item.credit, currency: "SEK" },
16
+ }));
17
+ const totalDebit = entry.items.reduce((sum, item) => sum + item.debit, 0);
18
+ const totalCredit = entry.items.reduce((sum, item) => sum + item.credit, 0);
19
+ return {
20
+ series: seriesCode,
21
+ entryNumber: voucherNumber,
22
+ entryDate: entry.date,
23
+ description: entry.title,
24
+ status: "POSTED",
25
+ currency: "SEK",
26
+ externalId: entry.id,
27
+ totalDebit: { amount: totalDebit, currency: "SEK" },
28
+ totalCredit: { amount: totalCredit, currency: "SEK" },
29
+ lines,
30
+ sourceIntegration: "bokio",
31
+ sourceSyncedAt: new Date().toISOString(),
32
+ reversingEntryExternalId: entry.reversingJournalEntryId ?? undefined,
33
+ reversedByEntryExternalId: entry.reversedByJournalEntryId ?? undefined,
34
+ };
35
+ }
36
+ /**
37
+ * Helper to handle both plain number and MoneyAmount object formats.
38
+ * This provides backwards compatibility with YAML files that have plain numbers.
39
+ */
40
+ function getAmount(value) {
41
+ return typeof value === "number" ? value : value.amount;
42
+ }
43
+ /**
44
+ * Transform a local JournalEntry to a Bokio CreateJournalEntryRequest
45
+ */
46
+ export function mapJournalEntryToBokioRequest(entry) {
47
+ return {
48
+ title: entry.description,
49
+ date: entry.entryDate,
50
+ items: entry.lines.map((line) => ({
51
+ debit: getAmount(line.debit),
52
+ credit: getAmount(line.credit),
53
+ account: parseInt(line.account, 10),
54
+ })),
55
+ };
56
+ }
@@ -0,0 +1,6 @@
1
+ import type { Voucher } from "@moatless/fortnox-client";
2
+ import type { JournalEntry } from "../types";
3
+ /**
4
+ * Transform a Fortnox voucher to a JournalEntry
5
+ */
6
+ export declare function mapFortnoxVoucherToJournalEntry(voucher: Voucher, _fiscalYear?: number): JournalEntry;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Transform a Fortnox voucher to a JournalEntry
3
+ */
4
+ export function mapFortnoxVoucherToJournalEntry(voucher, _fiscalYear) {
5
+ const series = voucher.VoucherSeries;
6
+ const externalId = `${voucher.VoucherSeries}-${voucher.VoucherNumber}`;
7
+ // Calculate totals from voucher rows
8
+ let totalDebit = 0;
9
+ let totalCredit = 0;
10
+ const lines = (voucher.VoucherRows ?? []).map((row, idx) => {
11
+ totalDebit += row.Debit ?? 0;
12
+ totalCredit += row.Credit ?? 0;
13
+ const memo = row.TransactionInformation || row.Description || undefined;
14
+ return {
15
+ lineNumber: idx + 1,
16
+ account: String(row.Account),
17
+ debit: { amount: row.Debit ?? 0, currency: "SEK" },
18
+ credit: { amount: row.Credit ?? 0, currency: "SEK" },
19
+ memo: memo ? String(memo) : undefined,
20
+ costCenter: row.CostCenter ?? undefined,
21
+ };
22
+ });
23
+ return {
24
+ series,
25
+ entryNumber: voucher.VoucherNumber ?? 0,
26
+ entryDate: voucher.TransactionDate ?? voucher.VoucherDate ?? "",
27
+ description: voucher.Description ?? "",
28
+ status: "POSTED",
29
+ currency: "SEK",
30
+ externalId,
31
+ totalDebit: { amount: totalDebit, currency: "SEK" },
32
+ totalCredit: { amount: totalCredit, currency: "SEK" },
33
+ lines,
34
+ sourceIntegration: "fortnox",
35
+ sourceSyncedAt: new Date().toISOString(),
36
+ voucherNumber: voucher.VoucherNumber,
37
+ voucherSeriesCode: voucher.VoucherSeries,
38
+ };
39
+ }
@@ -0,0 +1,3 @@
1
+ export { mapFortnoxVoucherToJournalEntry } from "./fortnox";
2
+ export { mapBokioEntryToJournalEntry, mapJournalEntryToBokioRequest } from "./bokio";
3
+ export type { JournalEntry as BokioJournalEntry } from "@moatless/bokio-client";
@@ -0,0 +1,2 @@
1
+ export { mapFortnoxVoucherToJournalEntry } from "./fortnox";
2
+ export { mapBokioEntryToJournalEntry, mapJournalEntryToBokioRequest } from "./bokio";