kvitton-cli 0.4.2 → 0.4.5

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.
@@ -1,186 +0,0 @@
1
- import ora from "ora";
2
- import * as fs from "node:fs/promises";
3
- import * as path from "node:path";
4
- import { toYaml, sanitizeOrgName } from "sync";
5
- import { loadConfig } from "../lib/env";
6
- import { createBokioClient } from "../lib/client";
7
- /**
8
- * Slugify text for terminal-friendly filenames
9
- */
10
- function slugify(text, maxLength = 30) {
11
- return text
12
- .toLowerCase()
13
- .slice(0, maxLength)
14
- .replace(/[^a-z0-9\s-]/g, "")
15
- .replace(/\s+/g, "-")
16
- .replace(/-+/g, "-")
17
- .replace(/^-|-$/g, "");
18
- }
19
- export async function syncInboxCommand() {
20
- const cwd = process.cwd();
21
- // Load config
22
- const spinner = ora("Loading configuration...").start();
23
- let config;
24
- try {
25
- config = loadConfig(cwd);
26
- spinner.succeed(`Provider: ${config.provider}`);
27
- }
28
- catch (error) {
29
- spinner.fail(error instanceof Error ? error.message : "Unknown error");
30
- process.exit(1);
31
- }
32
- // Create client
33
- const client = createBokioClient(config);
34
- // Validate connection
35
- const validateSpinner = ora("Connecting to Bokio...").start();
36
- try {
37
- const companyInfo = await client.getCompanyInformation();
38
- validateSpinner.succeed(`Connected to: ${companyInfo.name}`);
39
- }
40
- catch (error) {
41
- validateSpinner.fail("Failed to connect to Bokio");
42
- console.error(error instanceof Error ? error.message : "Unknown error");
43
- process.exit(1);
44
- }
45
- // Fetch uploads
46
- const fetchSpinner = ora("Fetching inbox...").start();
47
- const allUploads = [];
48
- let page = 1;
49
- const pageSize = 100;
50
- while (true) {
51
- const response = await client.getUploads({ page, pageSize });
52
- allUploads.push(...response.data);
53
- if (!response.pagination.hasNextPage)
54
- break;
55
- page++;
56
- }
57
- // Filter to unprocessed items (not linked to journal entry)
58
- const unprocessedUploads = allUploads.filter((upload) => !upload.journalEntryId);
59
- fetchSpinner.succeed(`Found ${unprocessedUploads.length} unprocessed inbox items (${allUploads.length} total)`);
60
- if (unprocessedUploads.length === 0) {
61
- console.log("\n No new inbox items to download.\n");
62
- return;
63
- }
64
- // Ensure inbox directory exists
65
- const inboxDir = path.join(cwd, "inbox");
66
- await fs.mkdir(inboxDir, { recursive: true });
67
- // Download each unprocessed upload
68
- let newCount = 0;
69
- let existingCount = 0;
70
- const today = new Date().toISOString().slice(0, 10);
71
- // Track used directory names to handle duplicates
72
- const usedDirNames = new Set();
73
- // First, scan existing directories to avoid duplicates
74
- try {
75
- const existingDirs = await fs.readdir(inboxDir);
76
- for (const dir of existingDirs) {
77
- usedDirNames.add(dir);
78
- }
79
- }
80
- catch {
81
- // Inbox directory doesn't exist yet
82
- }
83
- for (const upload of unprocessedUploads) {
84
- // Generate directory name: {date}-{description-slugified}
85
- const slug = upload.description
86
- ? slugify(upload.description)
87
- : upload.id.slice(0, 8);
88
- let dirName = `${today}-${slug}`;
89
- // Check if this upload was already downloaded (by sourceId in existing document.yaml files)
90
- const alreadyDownloaded = await isAlreadyDownloaded(inboxDir, upload.id);
91
- if (alreadyDownloaded) {
92
- existingCount++;
93
- continue;
94
- }
95
- // Handle duplicate directory names by appending short ID
96
- if (usedDirNames.has(dirName)) {
97
- dirName = `${dirName}-${upload.id.slice(0, 8)}`;
98
- }
99
- usedDirNames.add(dirName);
100
- const uploadDir = path.join(inboxDir, dirName);
101
- process.stdout.write(`\r Downloading: ${newCount + existingCount + 1}/${unprocessedUploads.length}`);
102
- try {
103
- // Create upload directory
104
- await fs.mkdir(uploadDir, { recursive: true });
105
- // Download file
106
- const result = await client.downloadFile(upload.id);
107
- // Determine file extension from content type
108
- const ext = getExtensionFromContentType(upload.contentType || result.contentType);
109
- // Generate filename: {description-slugified}.ext or document.ext
110
- const filename = upload.description
111
- ? `${slugify(upload.description)}${ext}`
112
- : `document${ext}`;
113
- // Save file
114
- const filePath = path.join(uploadDir, filename);
115
- const buffer = Buffer.from(result.data);
116
- await fs.writeFile(filePath, buffer);
117
- // Write document.yaml (ExportedInboxDocument format)
118
- const document = {
119
- kind: "OTHER",
120
- status: "DRAFT",
121
- fileName: filename,
122
- mimeType: upload.contentType,
123
- description: upload.description ?? undefined,
124
- sourceIntegration: "bokio",
125
- sourceId: upload.id,
126
- uploadedAt: new Date().toISOString(),
127
- };
128
- const documentPath = path.join(uploadDir, "document.yaml");
129
- await fs.writeFile(documentPath, toYaml(document));
130
- newCount++;
131
- }
132
- catch (error) {
133
- console.error(`\n Failed to download ${upload.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
134
- // Clean up failed directory
135
- try {
136
- await fs.rm(uploadDir, { recursive: true });
137
- }
138
- catch {
139
- // Ignore cleanup errors
140
- }
141
- }
142
- }
143
- console.log(`\n
144
- Sync complete!
145
- - ${newCount} new inbox items downloaded
146
- - ${existingCount} already existed
147
- `);
148
- }
149
- function getExtensionFromContentType(contentType) {
150
- const map = {
151
- "application/pdf": ".pdf",
152
- "image/jpeg": ".jpg",
153
- "image/png": ".png",
154
- "image/gif": ".gif",
155
- "image/webp": ".webp",
156
- "image/tiff": ".tiff",
157
- "application/octet-stream": ".bin",
158
- };
159
- return map[contentType] || ".bin";
160
- }
161
- /**
162
- * Check if an upload was already downloaded by scanning existing document.yaml files
163
- */
164
- async function isAlreadyDownloaded(inboxDir, sourceId) {
165
- try {
166
- const dirs = await fs.readdir(inboxDir, { withFileTypes: true });
167
- for (const dir of dirs) {
168
- if (!dir.isDirectory())
169
- continue;
170
- const documentPath = path.join(inboxDir, dir.name, "document.yaml");
171
- try {
172
- const content = await fs.readFile(documentPath, "utf-8");
173
- if (content.includes(`sourceId: ${sourceId}`)) {
174
- return true;
175
- }
176
- }
177
- catch {
178
- // document.yaml doesn't exist or can't be read, skip
179
- }
180
- }
181
- }
182
- catch {
183
- // Inbox directory doesn't exist
184
- }
185
- return false;
186
- }
@@ -1,490 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import ora from "ora";
4
- import { FilesystemStorageService, mapBokioEntryToJournalEntry, mapJournalEntryToBokioRequest, journalEntryPath, journalEntryDirFromPath, toYaml, parseYaml, fiscalYearDirName, downloadFilesForEntry, } from "sync";
5
- import { loadConfig } from "../lib/env";
6
- import { createBokioClient } from "../lib/client";
7
- /** File extensions that should be uploaded as documents */
8
- const UPLOADABLE_EXTENSIONS = [".pdf", ".png", ".jpg", ".jpeg"];
9
- /** Map file extensions to MIME types */
10
- const MIME_TYPES = {
11
- ".pdf": "application/pdf",
12
- ".png": "image/png",
13
- ".jpg": "image/jpeg",
14
- ".jpeg": "image/jpeg",
15
- };
16
- export async function syncJournalCommand(options) {
17
- const cwd = process.cwd();
18
- // Load config
19
- const spinner = ora("Loading configuration...").start();
20
- let config;
21
- try {
22
- config = loadConfig(cwd);
23
- spinner.succeed(`Provider: ${config.provider}`);
24
- }
25
- catch (error) {
26
- spinner.fail(error instanceof Error ? error.message : "Unknown error");
27
- process.exit(1);
28
- }
29
- // Create client
30
- const client = createBokioClient(config);
31
- // Validate connection
32
- const validateSpinner = ora("Connecting to Bokio...").start();
33
- try {
34
- const companyInfo = await client.getCompanyInformation();
35
- validateSpinner.succeed(`Connected to: ${companyInfo.name}`);
36
- }
37
- catch (error) {
38
- validateSpinner.fail("Failed to connect to Bokio");
39
- console.error(error instanceof Error ? error.message : "Unknown error");
40
- process.exit(1);
41
- }
42
- // Determine which fiscal year(s) to sync
43
- const targetYear = options.year
44
- ? parseInt(options.year, 10)
45
- : options.all
46
- ? undefined
47
- : new Date().getFullYear();
48
- const yearLabel = targetYear ? `FY ${targetYear}` : "all years";
49
- console.log(` Syncing ${yearLabel}...\n`);
50
- // Storage for file operations
51
- const storage = new FilesystemStorageService(cwd);
52
- // Sync chart of accounts first
53
- const accountsSpinner = ora("Syncing chart of accounts...").start();
54
- const accountsCount = await syncChartOfAccounts(client, storage);
55
- accountsSpinner.succeed(`Synced ${accountsCount} accounts`);
56
- // Sync journal entries
57
- const shouldDownloadFiles = options.downloadFiles !== false;
58
- const result = await syncJournalEntries(client, cwd, targetYear, shouldDownloadFiles, (progress) => {
59
- process.stdout.write(`\r Syncing: ${progress.current}/${progress.total} entries`);
60
- });
61
- const filesDownloadedLine = result.entriesWithFilesDownloaded > 0
62
- ? `\n - Downloaded files for ${result.entriesWithFilesDownloaded}/${result.totalEntries} entries`
63
- : "";
64
- console.log(`\n
65
- Sync complete! (${yearLabel})
66
- - ${accountsCount} accounts
67
- - ${result.fiscalYearsCount} fiscal year(s)
68
- - Downloaded: ${result.totalEntries} entries (${result.newEntries} new, ${result.existingEntries} existing)
69
- - Uploaded: ${result.uploadedEntries} local entries${filesDownloadedLine}
70
- `);
71
- }
72
- /**
73
- * Create a FileDownloader adapter for BokioClient
74
- */
75
- function createBokioDownloader(client) {
76
- return {
77
- async getFilesForEntry(journalEntryId) {
78
- // Bokio API requires query format: journalEntryId==UUID
79
- const response = await client.getUploads({
80
- query: `journalEntryId==${journalEntryId}`,
81
- });
82
- return response.data.map((upload) => ({
83
- id: upload.id,
84
- contentType: upload.contentType,
85
- description: upload.description ?? undefined,
86
- }));
87
- },
88
- async downloadFile(id) {
89
- return client.downloadFile(id);
90
- },
91
- };
92
- }
93
- async function syncJournalEntries(client, repoPath, targetYear, downloadFiles, onProgress) {
94
- const storage = new FilesystemStorageService(repoPath);
95
- // 1. Fetch fiscal years
96
- const fiscalYearsResponse = await client.getFiscalYears();
97
- const allFiscalYears = fiscalYearsResponse.data;
98
- // Filter fiscal years if targeting a specific year
99
- const fiscalYears = targetYear
100
- ? allFiscalYears.filter((fy) => parseInt(fy.startDate.slice(0, 4), 10) === targetYear)
101
- : allFiscalYears;
102
- if (targetYear && fiscalYears.length === 0) {
103
- console.log(` No fiscal year found for ${targetYear}`);
104
- return {
105
- totalEntries: 0,
106
- newEntries: 0,
107
- existingEntries: 0,
108
- uploadedEntries: 0,
109
- fiscalYearsCount: 0,
110
- entriesWithFilesDownloaded: 0,
111
- };
112
- }
113
- // 2. Fetch all entries with pagination
114
- const allEntries = [];
115
- let page = 1;
116
- const pageSize = 100;
117
- // First fetch to get total count
118
- const firstPage = await client.getJournalEntries({ page: 1, pageSize: 1 });
119
- const totalFromApi = firstPage.pagination.totalItems;
120
- onProgress({ current: 0, total: totalFromApi });
121
- if (totalFromApi === 0) {
122
- await writeFiscalYearsMetadata(storage, fiscalYears);
123
- // Even with no entries from API, we might have local entries to upload
124
- const uploadedEntries = await uploadLocalEntries(client, storage, repoPath, fiscalYears);
125
- return {
126
- totalEntries: 0,
127
- newEntries: 0,
128
- existingEntries: 0,
129
- uploadedEntries,
130
- fiscalYearsCount: fiscalYears.length,
131
- entriesWithFilesDownloaded: 0,
132
- };
133
- }
134
- // Fetch all entries
135
- while (true) {
136
- const response = await client.getJournalEntries({ page, pageSize });
137
- allEntries.push(...response.data);
138
- onProgress({ current: allEntries.length, total: totalFromApi });
139
- if (!response.pagination.hasNextPage)
140
- break;
141
- page++;
142
- }
143
- // 3. Filter entries by target fiscal year(s)
144
- const entriesToSync = targetYear
145
- ? allEntries.filter((entry) => {
146
- const fy = findFiscalYear(entry.date, fiscalYears);
147
- return fy !== undefined;
148
- })
149
- : allEntries;
150
- // 4. Write each entry, tracking new vs existing, and download files
151
- let newEntries = 0;
152
- let existingEntries = 0;
153
- let entriesWithFilesDownloaded = 0;
154
- const downloader = createBokioDownloader(client);
155
- for (const entry of entriesToSync) {
156
- const fiscalYear = findFiscalYear(entry.date, allFiscalYears);
157
- if (!fiscalYear)
158
- continue;
159
- const fyYear = parseInt(fiscalYear.startDate.slice(0, 4), 10);
160
- const result = await writeJournalEntry(storage, fyYear, entry);
161
- if (result.isNew) {
162
- newEntries++;
163
- }
164
- else {
165
- existingEntries++;
166
- }
167
- // Download files for this entry if enabled
168
- if (downloadFiles && result.entryDir) {
169
- const filesDownloaded = await downloadFilesForEntry({
170
- storage,
171
- repoPath,
172
- entryDir: result.entryDir,
173
- journalEntryId: entry.id,
174
- downloader,
175
- sourceIntegration: "bokio",
176
- });
177
- if (filesDownloaded > 0) {
178
- entriesWithFilesDownloaded++;
179
- }
180
- }
181
- }
182
- // 5. Write fiscal year metadata
183
- await writeFiscalYearsMetadata(storage, fiscalYears);
184
- // 6. Upload local entries without externalId to Bokio
185
- const uploadedEntries = await uploadLocalEntries(client, storage, repoPath, fiscalYears);
186
- return {
187
- totalEntries: entriesToSync.length,
188
- newEntries,
189
- existingEntries,
190
- uploadedEntries,
191
- fiscalYearsCount: fiscalYears.length,
192
- entriesWithFilesDownloaded,
193
- };
194
- }
195
- function findFiscalYear(date, fiscalYears) {
196
- return fiscalYears.find((fy) => date >= fy.startDate && date <= fy.endDate);
197
- }
198
- async function writeJournalEntry(storage, fyYear, entry) {
199
- const journalEntry = mapBokioEntryToJournalEntry({
200
- id: entry.id,
201
- journalEntryNumber: entry.journalEntryNumber,
202
- date: entry.date,
203
- title: entry.title,
204
- items: entry.items.map((item) => ({
205
- account: item.account,
206
- debit: item.debit,
207
- credit: item.credit,
208
- })),
209
- });
210
- const entryPath = journalEntryPath(fyYear, journalEntry.series ?? null, journalEntry.entryNumber, journalEntry.entryDate, journalEntry.description);
211
- const entryDir = journalEntryDirFromPath(entryPath);
212
- // Check if entry already exists
213
- const exists = await storage.exists(entryPath);
214
- if (exists) {
215
- return { isNew: false, entryDir };
216
- }
217
- const yamlContent = toYaml(journalEntry);
218
- await storage.writeFile(entryPath, yamlContent);
219
- return { isNew: true, entryDir };
220
- }
221
- async function writeFiscalYearsMetadata(storage, fiscalYears) {
222
- for (const fy of fiscalYears) {
223
- const fyDir = fiscalYearDirName({ start_date: fy.startDate });
224
- const metadataPath = `journal-entries/${fyDir}/_fiscal-year.yaml`;
225
- const metadata = {
226
- id: fy.id,
227
- startDate: fy.startDate,
228
- endDate: fy.endDate,
229
- status: fy.status,
230
- };
231
- const yamlContent = toYaml(metadata);
232
- await storage.writeFile(metadataPath, yamlContent);
233
- }
234
- }
235
- /**
236
- * Parse journal entry number from Bokio (e.g., "A123" -> { series: "A", entryNumber: 123 })
237
- */
238
- function parseJournalEntryNumber(journalEntryNumber) {
239
- const numberMatch = journalEntryNumber.match(/\d+/);
240
- const entryNumber = numberMatch ? parseInt(numberMatch[0], 10) : 0;
241
- const series = journalEntryNumber.replace(/\d+/g, "") || undefined;
242
- return {
243
- series: series,
244
- entryNumber,
245
- };
246
- }
247
- /**
248
- * List local entries without externalId (not yet synced to provider)
249
- */
250
- async function listLocalEntriesWithoutExternalId(storage, fiscalYears) {
251
- const result = [];
252
- for (const fy of fiscalYears) {
253
- const fyYear = parseInt(fy.startDate.slice(0, 4), 10);
254
- const fyDir = `journal-entries/FY-${fyYear}`;
255
- try {
256
- const directories = await storage.listDirectory(fyDir);
257
- for (const dir of directories) {
258
- if (dir.type !== "dir" || dir.name.startsWith("_")) {
259
- continue;
260
- }
261
- const entryPath = `${dir.path}/entry.yaml`;
262
- try {
263
- const { content } = await storage.readFile(entryPath);
264
- const entry = parseYaml(content);
265
- // Only include entries without externalId
266
- if (!entry.externalId) {
267
- result.push({
268
- entry,
269
- directoryPath: dir.path,
270
- fiscalYear: fy,
271
- });
272
- }
273
- }
274
- catch {
275
- // Skip entries that can't be read
276
- }
277
- }
278
- }
279
- catch {
280
- // Fiscal year directory doesn't exist yet
281
- }
282
- }
283
- return result;
284
- }
285
- /**
286
- * Upload local entries without externalId to Bokio
287
- */
288
- async function uploadLocalEntries(client, storage, repoPath, fiscalYears) {
289
- const localEntries = await listLocalEntriesWithoutExternalId(storage, fiscalYears);
290
- let uploadedCount = 0;
291
- for (const { entry, directoryPath, fiscalYear } of localEntries) {
292
- try {
293
- // Upload to Bokio
294
- const request = mapJournalEntryToBokioRequest(entry);
295
- const bokioEntry = await client.createJournalEntry(request);
296
- // Parse series and number from response
297
- const parsed = parseJournalEntryNumber(bokioEntry.journalEntryNumber);
298
- // Upload any document files in the directory
299
- await uploadDocumentsForEntry(client, storage, path.join(repoPath, directoryPath), directoryPath, bokioEntry.id);
300
- // Update entry with Bokio metadata (remove legacy fields)
301
- const updatedEntry = {
302
- ...entry,
303
- externalId: bokioEntry.id,
304
- series: parsed.series,
305
- entryNumber: parsed.entryNumber,
306
- sourceIntegration: "bokio",
307
- sourceSyncedAt: new Date().toISOString(),
308
- // Remove legacy fields by setting to undefined
309
- voucherNumber: undefined,
310
- voucherSeriesCode: undefined,
311
- };
312
- // Calculate new directory path
313
- const fyYear = parseInt(fiscalYear.startDate.slice(0, 4), 10);
314
- const newEntryPath = journalEntryPath(fyYear, updatedEntry.series ?? null, updatedEntry.entryNumber, updatedEntry.entryDate, updatedEntry.description);
315
- const newDirPath = journalEntryDirFromPath(newEntryPath);
316
- // Write updated entry.yaml
317
- const yamlContent = toYaml(updatedEntry);
318
- await storage.writeFile(`${directoryPath}/entry.yaml`, yamlContent);
319
- // Rename directory if path changed
320
- if (directoryPath !== newDirPath) {
321
- // Check if target directory already exists
322
- const targetExists = await storage.exists(`${newDirPath}/entry.yaml`);
323
- if (targetExists) {
324
- console.warn(` Warning: Cannot rename ${directoryPath} -> ${newDirPath} (target exists)`);
325
- }
326
- else {
327
- await storage.renameDirectory(directoryPath, newDirPath);
328
- }
329
- }
330
- uploadedCount++;
331
- console.log(` Uploaded: ${entry.description} -> ${bokioEntry.journalEntryNumber}`);
332
- }
333
- catch (error) {
334
- console.error(` Failed to upload entry: ${entry.description}`, error instanceof Error ? error.message : error);
335
- }
336
- }
337
- // Also retry document uploads for entries that have externalId but failed document upload
338
- const retriedDocs = await retryFailedDocumentUploads(client, storage, repoPath, fiscalYears);
339
- return uploadedCount + retriedDocs;
340
- }
341
- /**
342
- * Upload documents for a journal entry and update document.yaml with sourceIntegration/sourceId
343
- */
344
- async function uploadDocumentsForEntry(client, storage, absoluteDirPath, relativeDirPath, journalEntryId) {
345
- const files = await fs.readdir(absoluteDirPath);
346
- for (const filename of files) {
347
- const ext = path.extname(filename).toLowerCase();
348
- if (!UPLOADABLE_EXTENSIONS.includes(ext)) {
349
- continue;
350
- }
351
- // Check if already uploaded via document.yaml
352
- const docYamlPath = `${relativeDirPath}/document.yaml`;
353
- let docMetadata = {};
354
- try {
355
- const { content } = await storage.readFile(docYamlPath);
356
- docMetadata = parseYaml(content);
357
- if (docMetadata.sourceIntegration === "bokio" && docMetadata.sourceId) {
358
- // Already uploaded to Bokio
359
- continue;
360
- }
361
- }
362
- catch {
363
- // No document.yaml, proceed with upload
364
- }
365
- try {
366
- const filePath = path.join(absoluteDirPath, filename);
367
- let fileBuffer = await fs.readFile(filePath);
368
- // Check if file is base64 encoded (stored as text, not binary)
369
- // Base64 PDF starts with "JVBERi" (%PDF- encoded)
370
- const firstBytes = fileBuffer.subarray(0, 10).toString("utf8");
371
- if (firstBytes.startsWith("JVBERi") || firstBytes.match(/^[A-Za-z0-9+/=]+$/)) {
372
- // Decode base64
373
- const base64Content = fileBuffer.toString("utf8");
374
- fileBuffer = Buffer.from(base64Content, "base64");
375
- }
376
- const mimeType = MIME_TYPES[ext] || "application/octet-stream";
377
- const file = new File([fileBuffer], filename, { type: mimeType });
378
- const upload = await client.uploadFile({
379
- file,
380
- filename,
381
- journalEntryId,
382
- });
383
- // Update document.yaml with Bokio source info
384
- const updatedDocMetadata = {
385
- ...docMetadata,
386
- sourceIntegration: "bokio",
387
- sourceId: upload.id,
388
- };
389
- await storage.writeFile(docYamlPath, toYaml(updatedDocMetadata));
390
- console.log(` Uploaded file: ${filename}`);
391
- }
392
- catch (fileError) {
393
- console.warn(`\n Warning: Failed to upload ${filename}`);
394
- if (fileError &&
395
- typeof fileError === "object" &&
396
- "statusCode" in fileError) {
397
- const apiErr = fileError;
398
- console.warn(` Status: ${apiErr.statusCode}`);
399
- if (apiErr.response) {
400
- console.warn(` Response: ${JSON.stringify(apiErr.response)}`);
401
- }
402
- else if (apiErr.message) {
403
- console.warn(` Message: ${apiErr.message}`);
404
- }
405
- }
406
- else if (fileError instanceof Error) {
407
- console.warn(` Error: ${fileError.message}`);
408
- }
409
- else {
410
- console.warn(` Error: ${fileError}`);
411
- }
412
- }
413
- }
414
- }
415
- /**
416
- * Retry document uploads for entries synced to Bokio but with documents not yet uploaded
417
- */
418
- async function retryFailedDocumentUploads(client, storage, repoPath, fiscalYears) {
419
- let retriedCount = 0;
420
- for (const fy of fiscalYears) {
421
- const fyYear = parseInt(fy.startDate.slice(0, 4), 10);
422
- const fyDir = `journal-entries/FY-${fyYear}`;
423
- try {
424
- const directories = await storage.listDirectory(fyDir);
425
- for (const dir of directories) {
426
- if (dir.type !== "dir" || dir.name.startsWith("_")) {
427
- continue;
428
- }
429
- const entryPath = `${dir.path}/entry.yaml`;
430
- const docPath = `${dir.path}/document.yaml`;
431
- try {
432
- // Check if entry has externalId (already synced to Bokio)
433
- const { content: entryContent } = await storage.readFile(entryPath);
434
- const entry = parseYaml(entryContent);
435
- if (!entry.externalId || entry.sourceIntegration !== "bokio") {
436
- continue; // Not synced to Bokio yet
437
- }
438
- // Check if document.yaml exists and needs upload
439
- try {
440
- const { content: docContent } = await storage.readFile(docPath);
441
- const docMetadata = parseYaml(docContent);
442
- if (docMetadata.sourceIntegration === "bokio" && docMetadata.sourceId) {
443
- continue; // Already uploaded to Bokio
444
- }
445
- // Has document.yaml but not uploaded to Bokio - retry upload
446
- console.log(`\n Retrying document upload for: ${entry.description}`);
447
- await uploadDocumentsForEntry(client, storage, path.join(repoPath, dir.path), dir.path, entry.externalId);
448
- retriedCount++;
449
- }
450
- catch {
451
- // No document.yaml, check for files to upload
452
- const absoluteDirPath = path.join(repoPath, dir.path);
453
- const files = await fs.readdir(absoluteDirPath);
454
- const hasUploadableFiles = files.some((f) => UPLOADABLE_EXTENSIONS.includes(path.extname(f).toLowerCase()));
455
- if (hasUploadableFiles) {
456
- console.log(`\n Uploading documents for: ${entry.description}`);
457
- await uploadDocumentsForEntry(client, storage, absoluteDirPath, dir.path, entry.externalId);
458
- retriedCount++;
459
- }
460
- }
461
- }
462
- catch {
463
- // Skip entries that can't be read
464
- }
465
- }
466
- }
467
- catch {
468
- // Fiscal year directory doesn't exist
469
- }
470
- }
471
- return retriedCount;
472
- }
473
- /**
474
- * Sync chart of accounts from Bokio to accounts.yaml
475
- */
476
- async function syncChartOfAccounts(client, storage) {
477
- const bokioAccounts = await client.getChartOfAccounts();
478
- // Sort by account number and transform to expected format
479
- const accounts = [...bokioAccounts]
480
- .sort((a, b) => a.account - b.account)
481
- .map((account) => ({
482
- code: account.account.toString(),
483
- name: account.name,
484
- description: account.name,
485
- }));
486
- // Write accounts.yaml
487
- const yamlContent = toYaml({ accounts });
488
- await storage.writeFile("accounts.yaml", yamlContent);
489
- return accounts.length;
490
- }
@@ -1,10 +0,0 @@
1
- import { BokioClient } from "integrations-bokio";
2
- export function createBokioClient(config) {
3
- if (config.provider !== "bokio" || !config.bokio) {
4
- throw new Error("Bokio configuration not found");
5
- }
6
- return new BokioClient({
7
- token: config.bokio.token,
8
- companyId: config.bokio.companyId,
9
- });
10
- }