create-kvitton 0.4.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.
@@ -0,0 +1,8 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ export async function createEnvFile(repoPath, variables) {
4
+ const lines = Object.entries(variables)
5
+ .map(([key, value]) => `${key}=${value}`)
6
+ .join("\n");
7
+ await fs.writeFile(path.join(repoPath, ".env"), `${lines}\n`);
8
+ }
@@ -0,0 +1,32 @@
1
+ import simpleGit from "simple-git";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ const GITIGNORE = `# Environment files with secrets
5
+ .env
6
+ .env.*
7
+
8
+ # CLI cache
9
+ .kvitton/
10
+
11
+ # OS files
12
+ .DS_Store
13
+ Thumbs.db
14
+
15
+ # Editor files
16
+ .vscode/
17
+ .idea/
18
+ *.swp
19
+ `;
20
+ export async function initGitRepo(repoPath) {
21
+ const git = simpleGit(repoPath);
22
+ await git.init();
23
+ await fs.writeFile(path.join(repoPath, ".gitignore"), GITIGNORE);
24
+ }
25
+ export async function commitAll(repoPath, message) {
26
+ const git = simpleGit(repoPath);
27
+ await git.add(".");
28
+ const status = await git.status();
29
+ if (status.files.length > 0) {
30
+ await git.commit(message);
31
+ }
32
+ }
@@ -0,0 +1,142 @@
1
+ import { FilesystemStorageService, mapBokioEntryToJournalEntry, journalEntryPath, journalEntryDirFromPath, toYaml, fiscalYearDirName, downloadFilesForEntry, } from "sync";
2
+ /**
3
+ * Create a FileDownloader adapter for BokioClient
4
+ */
5
+ function createBokioDownloader(client) {
6
+ return {
7
+ async getFilesForEntry(journalEntryId) {
8
+ // Bokio API requires query format: journalEntryId==UUID
9
+ const response = await client.getUploads({
10
+ query: `journalEntryId==${journalEntryId}`,
11
+ });
12
+ return response.data.map((upload) => ({
13
+ id: upload.id,
14
+ contentType: upload.contentType,
15
+ description: upload.description ?? undefined,
16
+ }));
17
+ },
18
+ async downloadFile(id) {
19
+ return client.downloadFile(id);
20
+ },
21
+ };
22
+ }
23
+ export async function syncJournalEntries(client, repoPath, options, onProgress) {
24
+ const storage = new FilesystemStorageService(repoPath);
25
+ // 1. Fetch fiscal years
26
+ const fiscalYearsResponse = await client.getFiscalYears();
27
+ const fiscalYears = fiscalYearsResponse.data;
28
+ // 2. Fetch total count first for progress display
29
+ const firstPage = await client.getJournalEntries({ page: 1, pageSize: 1 });
30
+ const totalEntries = firstPage.pagination.totalItems;
31
+ onProgress({ current: 0, total: totalEntries, message: "Starting sync..." });
32
+ if (totalEntries === 0) {
33
+ // Write fiscal years even if no entries
34
+ await writeFiscalYearsMetadata(storage, fiscalYears);
35
+ return {
36
+ entriesCount: 0,
37
+ fiscalYearsCount: fiscalYears.length,
38
+ entriesWithFilesDownloaded: 0,
39
+ };
40
+ }
41
+ // 3. Fetch all entries with pagination
42
+ const allEntries = [];
43
+ let page = 1;
44
+ const pageSize = 100;
45
+ while (true) {
46
+ const response = await client.getJournalEntries({ page, pageSize });
47
+ allEntries.push(...response.data);
48
+ onProgress({ current: allEntries.length, total: totalEntries });
49
+ if (!response.pagination.hasNextPage)
50
+ break;
51
+ page++;
52
+ }
53
+ // 4. Write each entry and download files
54
+ let entriesWithFilesDownloaded = 0;
55
+ const downloader = createBokioDownloader(client);
56
+ for (const entry of allEntries) {
57
+ const fiscalYear = findFiscalYear(entry.date, fiscalYears);
58
+ if (!fiscalYear)
59
+ continue;
60
+ const fyYear = parseInt(fiscalYear.startDate.slice(0, 4), 10);
61
+ const entryDir = await writeJournalEntry(storage, fyYear, entry);
62
+ // Download files for this entry if enabled
63
+ if (options.downloadFiles !== false && entryDir) {
64
+ const filesDownloaded = await downloadFilesForEntry({
65
+ storage,
66
+ repoPath,
67
+ entryDir,
68
+ journalEntryId: entry.id,
69
+ downloader,
70
+ sourceIntegration: "bokio",
71
+ });
72
+ if (filesDownloaded > 0) {
73
+ entriesWithFilesDownloaded++;
74
+ }
75
+ }
76
+ }
77
+ // 5. Write fiscal year metadata
78
+ await writeFiscalYearsMetadata(storage, fiscalYears);
79
+ return {
80
+ entriesCount: allEntries.length,
81
+ fiscalYearsCount: fiscalYears.length,
82
+ entriesWithFilesDownloaded,
83
+ };
84
+ }
85
+ function findFiscalYear(date, fiscalYears) {
86
+ return fiscalYears.find((fy) => date >= fy.startDate && date <= fy.endDate);
87
+ }
88
+ async function writeJournalEntry(storage, fyYear, entry) {
89
+ // Transform Bokio entry to unified format
90
+ const journalEntry = mapBokioEntryToJournalEntry({
91
+ id: entry.id,
92
+ journalEntryNumber: entry.journalEntryNumber,
93
+ date: entry.date,
94
+ title: entry.title,
95
+ items: entry.items.map((item) => ({
96
+ account: item.account,
97
+ debit: item.debit,
98
+ credit: item.credit,
99
+ })),
100
+ });
101
+ // Generate path
102
+ const entryPath = journalEntryPath(fyYear, journalEntry.series ?? null, journalEntry.entryNumber, journalEntry.entryDate, journalEntry.description);
103
+ // Write YAML
104
+ const yamlContent = toYaml(journalEntry);
105
+ await storage.writeFile(entryPath, yamlContent);
106
+ // Return directory path for file downloads
107
+ return journalEntryDirFromPath(entryPath);
108
+ }
109
+ async function writeFiscalYearsMetadata(storage, fiscalYears) {
110
+ for (const fy of fiscalYears) {
111
+ const fyDir = fiscalYearDirName({ start_date: fy.startDate });
112
+ const metadataPath = `journal-entries/${fyDir}/_fiscal-year.yaml`;
113
+ const metadata = {
114
+ id: fy.id,
115
+ startDate: fy.startDate,
116
+ endDate: fy.endDate,
117
+ status: fy.status,
118
+ };
119
+ const yamlContent = toYaml(metadata);
120
+ await storage.writeFile(metadataPath, yamlContent);
121
+ }
122
+ }
123
+ /**
124
+ * Sync chart of accounts from Bokio to accounts.yaml
125
+ */
126
+ export async function syncChartOfAccounts(client, repoPath) {
127
+ const storage = new FilesystemStorageService(repoPath);
128
+ // Fetch chart of accounts
129
+ const bokioAccounts = await client.getChartOfAccounts();
130
+ // Sort by account number and transform to expected format
131
+ const accounts = [...bokioAccounts]
132
+ .sort((a, b) => a.account - b.account)
133
+ .map((account) => ({
134
+ code: account.account.toString(),
135
+ name: account.name,
136
+ description: account.name,
137
+ }));
138
+ // Write accounts.yaml
139
+ const yamlContent = toYaml({ accounts });
140
+ await storage.writeFile("accounts.yaml", yamlContent);
141
+ return { accountsCount: accounts.length };
142
+ }
@@ -0,0 +1,95 @@
1
+ # AGENTS.md
2
+
3
+ This is an accounting data repository for {{COMPANY_NAME}} (Swedish company), storing financial records, journal entries, and documents. It integrates with {{PROVIDER}} (Swedish accounting software) for document management.
4
+
5
+ ## Repository Structure
6
+
7
+ ```
8
+ /
9
+ ├── accounts.yaml # Swedish chart of accounts (account codes 1000-9999)
10
+ ├── journal-entries/ # Main accounting data organized by fiscal year
11
+ │ └── FY-YYYY/ # Fiscal year folders (FY-2014 through FY-2026)
12
+ │ ├── _fiscal-year.yaml
13
+ │ └── [SERIES]-[NUM]-[DATE]-[DESC]/
14
+ │ ├── entry.yaml
15
+ │ └── *.pdf # Supporting documents
16
+
17
+ ├── inbox/ # Raw incoming documents (no entry yet)
18
+ │ └── [DATE]-[NAME]/
19
+ │ ├── document.yaml
20
+ │ └── *.pdf
21
+ └── drafts/ # Documents with entries, ready to post
22
+ └── [DATE] - [NAME]/ # e.g., "2025-12-24 - anthropic"
23
+ ├── document.yaml
24
+ ├── entry.yaml
25
+ └── *.pdf
26
+ ```
27
+
28
+ ## CLI Commands
29
+
30
+ Use the `kvitton` CLI to interact with this repository:
31
+
32
+ ```bash
33
+ kvitton sync-journal # Sync journal entries from accounting provider
34
+ kvitton sync-inbox # Download inbox files from accounting provider
35
+ kvitton company-info # Display company information
36
+ ```
37
+
38
+ ## Entities
39
+
40
+ ### Journal Entry (entry.yaml)
41
+
42
+ ```yaml
43
+ series: A # A=Admin, B=Customer invoices, C=Customer payments,
44
+ # D=Supplier invoices, E=Supplier payments, K=Salary
45
+ entryNumber: 1
46
+ entryDate: 2024-01-03
47
+ description: Transaction description
48
+ status: POSTED # or DRAFT
49
+ currency: SEK
50
+ lines:
51
+ - account: "1930" # Account code from accounts.yaml
52
+ debit: 1000.00 # or credit:
53
+ memo: Account description
54
+ ```
55
+
56
+ ### Document Metadata (document.yaml)
57
+
58
+ ```yaml
59
+ kind: RECEIPT # RECEIPT, INVOICE, BANK_FILE, etc.
60
+ status: DRAFT
61
+ fileName: document.pdf
62
+ sourceIntegration: {{PROVIDER_LOWER}}
63
+ sourceId: UUID
64
+ ```
65
+
66
+ ### Fiscal Year (_fiscal-year.yaml)
67
+
68
+ ```yaml
69
+ name: FY 2024
70
+ startDate: 2024-01-01
71
+ endDate: 2024-12-31
72
+ status: OPEN
73
+ externalId: "12" # Provider reference
74
+ ```
75
+
76
+ ## Account Codes
77
+
78
+ Swedish BAS account codes are organized by ranges:
79
+
80
+ - **1xxx** - Assets (Tillgångar)
81
+ - **2xxx** - Liabilities & Equity (Skulder och eget kapital)
82
+ - **3xxx** - Revenue (Intäkter)
83
+ - **4xxx** - Cost of goods sold (Inköp)
84
+ - **5xxx-6xxx** - Operating expenses (Kostnader)
85
+ - **7xxx** - Personnel costs (Personalkostnader)
86
+ - **8xxx** - Financial items (Finansiella poster)
87
+ - **9xxx** - Year-end allocations (Bokslutsdispositioner)
88
+
89
+ Common accounts:
90
+ - `1930` - Business bank account
91
+ - `2440` - Supplier payables
92
+ - `2610` - Outgoing VAT 25%
93
+ - `2640` - Incoming VAT
94
+ - `4000` - Cost of goods sold
95
+ - `6570` - Bank charges
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "create-kvitton",
3
+ "version": "0.4.0",
4
+ "description": "Create a new kvitton bookkeeping repository",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-kvitton": "./dist/index.js"
8
+ },
9
+ "files": ["dist"],
10
+ "scripts": {
11
+ "build": "bun scripts/build.ts",
12
+ "dev": "tsc --watch",
13
+ "type-check": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@inquirer/prompts": "^7.0.0",
17
+ "@supabase/supabase-js": "^2.49.1",
18
+ "commander": "^12.1.0",
19
+ "ora": "^8.1.1",
20
+ "simple-git": "^3.27.0",
21
+ "yaml": "^2.8.2",
22
+ "zod": "^3.24.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/bun": "latest",
26
+ "typescript": "^5.0.0",
27
+ "integrations-bokio": "workspace:*",
28
+ "sync": "workspace:*"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }