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.
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "kvitton-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.5",
4
4
  "description": "CLI for kvitton bookkeeping repositories",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "kvitton": "./dist/index.js"
8
8
  },
9
- "files": ["dist"],
9
+ "files": [
10
+ "dist"
11
+ ],
10
12
  "scripts": {
11
13
  "build": "bun scripts/build.ts",
12
14
  "dev": "tsc --watch",
13
15
  "type-check": "tsc --noEmit"
14
16
  },
15
17
  "dependencies": {
18
+ "@inquirer/prompts": "^8.1.0",
16
19
  "@supabase/supabase-js": "^2.49.1",
17
20
  "commander": "^12.1.0",
18
21
  "dotenv": "^16.0.0",
@@ -27,9 +30,11 @@
27
30
  "@types/pdf-parse": "^1.1.5",
28
31
  "typescript": "^5.0.0",
29
32
  "integrations-bokio": "workspace:*",
33
+ "integrations-core": "workspace:*",
34
+ "integrations-fortnox": "workspace:*",
30
35
  "integrations-riksbank": "workspace:*",
31
36
  "shared": "workspace:*",
32
- "sync": "workspace:*"
37
+ "accounting": "workspace:*"
33
38
  },
34
39
  "engines": {
35
40
  "node": ">=18"
@@ -1,48 +0,0 @@
1
- import ora from "ora";
2
- import { loadConfig } from "../lib/env";
3
- import { createBokioClient } from "../lib/client";
4
- export async function companyInfoCommand() {
5
- const cwd = process.cwd();
6
- // Load config
7
- const spinner = ora("Loading configuration...").start();
8
- let config;
9
- try {
10
- config = loadConfig(cwd);
11
- spinner.succeed(`Provider: ${config.provider}`);
12
- }
13
- catch (error) {
14
- spinner.fail(error instanceof Error ? error.message : "Unknown error");
15
- process.exit(1);
16
- }
17
- // Create client
18
- const client = createBokioClient(config);
19
- // Fetch company info
20
- const infoSpinner = ora("Fetching company information...").start();
21
- try {
22
- const companyInfo = await client.getCompanyInformation();
23
- infoSpinner.stop();
24
- console.log(`
25
- Company Information
26
- -------------------
27
- Name: ${companyInfo.name}
28
- Org Number: ${companyInfo.organizationNumber ?? "N/A"}
29
- VAT Number: ${companyInfo.vatNumber ?? "N/A"}
30
- Address: ${formatAddress(companyInfo.address)}
31
- `);
32
- }
33
- catch (error) {
34
- infoSpinner.fail("Failed to fetch company information");
35
- console.error(error instanceof Error ? error.message : "Unknown error");
36
- process.exit(1);
37
- }
38
- }
39
- function formatAddress(address) {
40
- if (!address)
41
- return "N/A";
42
- const parts = [
43
- address.street,
44
- [address.zipCode, address.city].filter(Boolean).join(" "),
45
- address.country,
46
- ].filter(Boolean);
47
- return parts.length > 0 ? parts.join(", ") : "N/A";
48
- }
@@ -1,76 +0,0 @@
1
- import { RiksbankClient } from "integrations-riksbank";
2
- import { getCachedRate, setCachedRate } from "../lib/currency-cache";
3
- function getToday() {
4
- return new Date().toISOString().slice(0, 10);
5
- }
6
- export async function convertCurrencyCommand(options) {
7
- const cwd = process.cwd();
8
- const amount = parseFloat(options.amount);
9
- const currency = options.currency.toUpperCase();
10
- const date = options.date ?? getToday();
11
- // Validate amount
12
- if (Number.isNaN(amount) || amount <= 0) {
13
- console.error("Error: --amount must be a positive number");
14
- process.exit(1);
15
- }
16
- // Validate date format
17
- if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
18
- console.error("Error: --date must be in YYYY-MM-DD format");
19
- process.exit(1);
20
- }
21
- // Handle SEK (no conversion needed)
22
- if (currency === "SEK") {
23
- console.log("Currency Conversion");
24
- console.log("===================");
25
- console.log(`Amount: ${amount.toFixed(2)} SEK`);
26
- console.log(`Date: ${date}`);
27
- console.log("");
28
- console.log("No conversion needed (already SEK)");
29
- console.log("");
30
- console.log(`Result: ${amount.toFixed(2)} SEK`);
31
- return;
32
- }
33
- // Initialize Riksbank client
34
- const riksbank = new RiksbankClient();
35
- // Validate currency is supported
36
- if (!riksbank.isSupportedCurrency(currency)) {
37
- console.error(`Error: Unsupported currency: ${currency}`);
38
- console.error(`Supported currencies: ${riksbank.getSupportedCurrencies().join(", ")}`);
39
- process.exit(1);
40
- }
41
- console.log("Currency Conversion");
42
- console.log("===================");
43
- console.log(`Amount: ${amount.toFixed(2)} ${currency}`);
44
- console.log(`Date: ${date}`);
45
- console.log("");
46
- // Check cache first
47
- const cached = getCachedRate(cwd, currency, date);
48
- let rate;
49
- let rateDate;
50
- let source;
51
- if (cached) {
52
- rate = cached.rate;
53
- rateDate = cached.rateDate;
54
- source = "CACHE";
55
- }
56
- else {
57
- // Fetch from Riksbank
58
- const result = await riksbank.getRate(currency, date);
59
- rate = result.value;
60
- rateDate = result.date;
61
- source = "RIKSBANK";
62
- // Save to cache
63
- setCachedRate(cwd, currency, date, rate, rateDate);
64
- }
65
- const convertedAmount = amount * rate;
66
- console.log(`Exchange Rate: ${rate.toFixed(4)} SEK/${currency}`);
67
- if (rateDate !== date) {
68
- console.log(`Rate Date: ${rateDate} (closest available)`);
69
- }
70
- else {
71
- console.log(`Rate Date: ${rateDate}`);
72
- }
73
- console.log(`Source: ${source}`);
74
- console.log("");
75
- console.log(`Converted Amount: ${convertedAmount.toFixed(2)} SEK`);
76
- }
@@ -1,198 +0,0 @@
1
- /**
2
- * Create a complete journal entry from an inbox document
3
- * Reads document.yaml, generates lines, and creates entry.yaml
4
- */
5
- import { RiksbankClient } from "integrations-riksbank";
6
- import { resolveTaxCode, createJournalLinesFromTaxCode, getAllTaxCodes, } from "shared/dist/accounting";
7
- import { readAccountsYaml, getAccountName, toYaml } from "shared/dist/yaml";
8
- import * as path from "node:path";
9
- import * as fs from "node:fs/promises";
10
- import * as yaml from "yaml";
11
- export async function createEntryCommand(options) {
12
- // Handle --list-tax-codes flag
13
- if (options.listTaxCodes) {
14
- console.log("Available Tax Codes");
15
- console.log("===================");
16
- for (const tc of getAllTaxCodes()) {
17
- console.log(` ${tc.code}`);
18
- console.log(` ${tc.description}`);
19
- console.log(` Direction: ${tc.direction} | Territory: ${tc.territory} | Rate: ${tc.ratePercent}%`);
20
- console.log("");
21
- }
22
- return;
23
- }
24
- const { inboxDir, taxCode: taxCodeStr, baseAccount: baseAccountCode, balancingAccount: balancingAccountCode, series: seriesInput, entryNumber: entryNumberStr, memo, dryRun = false, } = options;
25
- const series = seriesInput.toUpperCase();
26
- const entryNumber = Number.parseInt(entryNumberStr, 10);
27
- if (Number.isNaN(entryNumber) || entryNumber < 1) {
28
- console.error("Error: --entry-number must be a positive integer");
29
- process.exit(1);
30
- }
31
- // File paths
32
- const documentPath = path.join(inboxDir, "document.yaml");
33
- const accountsPath = path.join(path.dirname(inboxDir), "accounts.yaml");
34
- // Read document.yaml
35
- let documentContent;
36
- try {
37
- documentContent = await fs.readFile(documentPath, "utf-8");
38
- }
39
- catch {
40
- console.error(`Error: document.yaml not found at ${documentPath}`);
41
- process.exit(1);
42
- }
43
- const document = yaml.parse(documentContent);
44
- // Extract data from document
45
- const description = document.description || "Unknown vendor";
46
- const documentDate = document.documentDate;
47
- const totalAmount = document.totalAmount;
48
- const subTotal = document.subTotal;
49
- if (!documentDate) {
50
- console.error("Error: documentDate not found in document.yaml");
51
- process.exit(1);
52
- }
53
- // Determine base amount - use subTotal (net) if available, otherwise totalAmount
54
- const amountSource = subTotal || totalAmount;
55
- if (!amountSource) {
56
- console.error("Error: No amount found in document.yaml (need totalAmount or subTotal)");
57
- process.exit(1);
58
- }
59
- const baseAmount = Number.parseFloat(amountSource.amount);
60
- const currency = amountSource.currency || "SEK";
61
- if (Number.isNaN(baseAmount) || baseAmount <= 0) {
62
- console.error("Error: Invalid amount in document.yaml");
63
- process.exit(1);
64
- }
65
- console.log("Creating Journal Entry");
66
- console.log("======================");
67
- console.log(`Document: ${document.fileName}`);
68
- console.log(`Vendor: ${description}`);
69
- console.log(`Date: ${documentDate}`);
70
- console.log(`Amount: ${baseAmount.toFixed(2)} ${currency}`);
71
- console.log("");
72
- // Resolve tax code
73
- const taxCode = resolveTaxCode(taxCodeStr);
74
- if (!taxCode) {
75
- console.error(`Error: Invalid tax code: ${taxCodeStr}`);
76
- console.error("\nRun with --list-tax-codes to see available codes");
77
- process.exit(1);
78
- }
79
- console.log(`Tax Code: ${taxCode.code}`);
80
- console.log(` ${taxCode.description}`);
81
- console.log(` VAT Rate: ${taxCode.ratePercent}%`);
82
- console.log("");
83
- // Currency conversion if needed
84
- let amountInSEK = baseAmount;
85
- if (currency !== "SEK") {
86
- console.log(`Converting ${baseAmount.toFixed(2)} ${currency} to SEK...`);
87
- const riksbank = new RiksbankClient();
88
- if (!riksbank.isSupportedCurrency(currency)) {
89
- console.error(`Error: Unsupported currency: ${currency}`);
90
- process.exit(1);
91
- }
92
- const rate = await riksbank.getRate(currency, documentDate);
93
- amountInSEK = baseAmount * rate.value;
94
- console.log(` Rate: ${rate.value.toFixed(4)} SEK/${currency} (${rate.date})`);
95
- console.log(` Converted: ${amountInSEK.toFixed(2)} SEK`);
96
- console.log("");
97
- }
98
- // Load accounts for memo lookup
99
- let accounts = [];
100
- try {
101
- accounts = await readAccountsYaml(accountsPath);
102
- }
103
- catch {
104
- // accounts.yaml not found, continue without account names
105
- }
106
- // Generate journal lines
107
- const result = createJournalLinesFromTaxCode({
108
- taxCode,
109
- baseAmount: amountInSEK,
110
- baseAccountCode,
111
- balancingAccountCode,
112
- memo,
113
- startingLineNumber: 1,
114
- });
115
- // Build entry.yaml content
116
- const entryLines = result.lines.map((line) => ({
117
- lineNumber: line.line_number,
118
- account: line.ledger_account_code,
119
- debit: {
120
- amount: line.debit ?? 0,
121
- currency: "SEK",
122
- },
123
- credit: {
124
- amount: line.credit ?? 0,
125
- currency: "SEK",
126
- },
127
- memo: line.memo ||
128
- getAccountName(line.ledger_account_code, accounts) ||
129
- undefined,
130
- }));
131
- const totalDebit = result.lines.reduce((sum, l) => sum + (l.debit ?? 0), 0);
132
- const totalCredit = result.lines.reduce((sum, l) => sum + (l.credit ?? 0), 0);
133
- const entry = {
134
- series,
135
- entryNumber,
136
- entryDate: documentDate,
137
- description,
138
- status: "DRAFT",
139
- currency: "SEK",
140
- totalDebit: {
141
- amount: Math.round(totalDebit * 100) / 100,
142
- currency: "SEK",
143
- },
144
- totalCredit: {
145
- amount: Math.round(totalCredit * 100) / 100,
146
- currency: "SEK",
147
- },
148
- lines: entryLines,
149
- };
150
- // Output the entry
151
- console.log("Generated Entry:");
152
- console.log("-".repeat(40));
153
- const entryYaml = toYaml(entry);
154
- console.log(entryYaml);
155
- console.log("-".repeat(40));
156
- console.log(`Total Debit: ${totalDebit.toFixed(2)} SEK`);
157
- console.log(`Total Credit: ${totalCredit.toFixed(2)} SEK`);
158
- console.log(`Balanced: ${Math.abs(totalDebit - totalCredit) < 0.01 ? "✓" : "✗"}`);
159
- // Calculate drafts directory name: {date} - {title}
160
- const titleSlug = slugify(description, 30);
161
- const draftsDirName = `${documentDate} - ${titleSlug}`;
162
- const draftsDir = path.join(path.dirname(inboxDir), "drafts", draftsDirName);
163
- // Write entry.yaml and move to drafts unless dry-run
164
- if (dryRun) {
165
- console.log("\n[Dry run - files not moved]");
166
- console.log(`Would move to: ${draftsDir}`);
167
- }
168
- else {
169
- // Create drafts directory
170
- await fs.mkdir(draftsDir, { recursive: true });
171
- // Copy all files from inbox to drafts
172
- const inboxFiles = await fs.readdir(inboxDir);
173
- for (const file of inboxFiles) {
174
- const srcPath = path.join(inboxDir, file);
175
- const destPath = path.join(draftsDir, file);
176
- await fs.copyFile(srcPath, destPath);
177
- }
178
- // Write entry.yaml to drafts
179
- const entryPath = path.join(draftsDir, "entry.yaml");
180
- await fs.writeFile(entryPath, entryYaml, "utf-8");
181
- // Delete inbox directory
182
- await fs.rm(inboxDir, { recursive: true });
183
- console.log(`\n✓ Moved to ${draftsDir}`);
184
- console.log(`✓ Entry written to ${entryPath}`);
185
- }
186
- }
187
- /**
188
- * Convert text to a URL-friendly slug
189
- */
190
- function slugify(text, maxLength = 30) {
191
- return text
192
- .toLowerCase()
193
- .slice(0, maxLength)
194
- .replace(/[^a-z0-9\s-]/g, "")
195
- .replace(/\s+/g, "-")
196
- .replace(/-+/g, "-")
197
- .replace(/^-|-$/g, "");
198
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * Generate journal entry lines from tax codes
3
- * Appends lines to an existing entry.yaml
4
- */
5
- import { createClient } from "@supabase/supabase-js";
6
- import { resolveTaxCode, createJournalLinesFromTaxCode, convertToSEK, calculateLineTotals, getAllTaxCodes, } from "shared/dist/accounting";
7
- import { readEntryYaml, readDocumentYaml, appendLinesToEntry, readAccountsYaml, validateAccountExists, getAccountName, } from "shared/dist/yaml";
8
- import * as path from "node:path";
9
- import * as fs from "node:fs/promises";
10
- export async function generateLinesCommand(options) {
11
- const { inboxDir, taxCode: taxCodeStr, baseAmount: baseAmountStr, baseAccount: baseAccountCode, balancingAccount: balancingAccountCode, memo, amountCurrency = "SEK", } = options;
12
- const baseAmount = Number.parseFloat(baseAmountStr);
13
- // Validate base amount
14
- if (Number.isNaN(baseAmount) || baseAmount <= 0) {
15
- console.error("Error: --base-amount must be a positive number");
16
- process.exit(1);
17
- }
18
- // File paths
19
- const entryPath = path.join(inboxDir, "entry.yaml");
20
- const documentPath = path.join(inboxDir, "document.yaml");
21
- const accountsPath = path.join(path.dirname(path.dirname(inboxDir)), "accounts.yaml");
22
- // Check if files exist
23
- try {
24
- await fs.access(entryPath);
25
- }
26
- catch {
27
- console.error(`Error: entry.yaml not found at ${entryPath}`);
28
- process.exit(1);
29
- }
30
- try {
31
- await fs.access(documentPath);
32
- }
33
- catch {
34
- console.error(`Error: document.yaml not found at ${documentPath}`);
35
- process.exit(1);
36
- }
37
- // Read entry and document
38
- console.log("Reading entry and document files...");
39
- const entry = await readEntryYaml(entryPath);
40
- // Document is read to validate it exists but not directly used
41
- await readDocumentYaml(documentPath);
42
- console.log(`Entry date: ${entry.entryDate}`);
43
- console.log(`Description: ${entry.description}`);
44
- // Resolve tax code
45
- console.log(`\nResolving tax code: ${taxCodeStr}`);
46
- const taxCode = resolveTaxCode(taxCodeStr);
47
- if (!taxCode) {
48
- console.error(`Error: Invalid tax code: ${taxCodeStr}`);
49
- console.error("\nAvailable tax codes:");
50
- for (const tc of getAllTaxCodes()) {
51
- console.error(` ${tc.code} - ${tc.description}`);
52
- }
53
- process.exit(1);
54
- }
55
- console.log(`✓ Tax code resolved: ${taxCode.description}`);
56
- console.log(` Direction: ${taxCode.direction}`);
57
- console.log(` Territory: ${taxCode.territory}`);
58
- console.log(` VAT Rate: ${taxCode.ratePercent}%`);
59
- // Currency conversion if needed
60
- let amountInSEK = baseAmount;
61
- if (amountCurrency !== "SEK") {
62
- console.log(`\nCurrency conversion: ${baseAmount} ${amountCurrency} → SEK`);
63
- // Initialize Supabase client
64
- const supabaseUrl = process.env.SUPABASE_URL;
65
- const supabaseKey = process.env.SUPABASE_SECRET_KEY;
66
- if (!supabaseUrl || !supabaseKey) {
67
- console.error("Error: Missing Supabase credentials");
68
- console.error("Ensure SUPABASE_URL and SUPABASE_SECRET_KEY are set in environment");
69
- process.exit(1);
70
- }
71
- const supabase = createClient(supabaseUrl, supabaseKey);
72
- const converted = await convertToSEK(supabase, baseAmount, amountCurrency, entry.entryDate);
73
- if (converted === null) {
74
- console.error(`Error: No currency rate found for ${amountCurrency} on ${entry.entryDate}`);
75
- console.error("Please sync currency rates from Riksbank or provide amount already in SEK");
76
- process.exit(1);
77
- }
78
- amountInSEK = converted;
79
- console.log(`✓ Converted to SEK: ${amountInSEK.toFixed(2)}`);
80
- }
81
- // Validate accounts exist (if accounts.yaml is available)
82
- let accounts = [];
83
- try {
84
- await fs.access(accountsPath);
85
- accounts = await readAccountsYaml(accountsPath);
86
- console.log(`\nValidating accounts against ${accountsPath}`);
87
- if (!validateAccountExists(baseAccountCode, accounts)) {
88
- console.error(`Error: Base account ${baseAccountCode} not found`);
89
- console.error("Run export-accounts first to create accounts.yaml");
90
- process.exit(1);
91
- }
92
- if (!validateAccountExists(balancingAccountCode, accounts)) {
93
- console.error(`Error: Balancing account ${balancingAccountCode} not found`);
94
- console.error("Run export-accounts first to create accounts.yaml");
95
- process.exit(1);
96
- }
97
- console.log(`✓ Base account: ${baseAccountCode} - ${getAccountName(baseAccountCode, accounts)}`);
98
- console.log(`✓ Balancing account: ${balancingAccountCode} - ${getAccountName(balancingAccountCode, accounts)}`);
99
- }
100
- catch {
101
- console.warn(`\nWarning: accounts.yaml not found at ${accountsPath}, skipping account validation`);
102
- }
103
- // Generate lines
104
- console.log("\nGenerating journal lines...");
105
- const result = createJournalLinesFromTaxCode({
106
- taxCode,
107
- baseAmount: amountInSEK,
108
- baseAccountCode,
109
- balancingAccountCode,
110
- memo,
111
- startingLineNumber: (entry.lines?.length || 0) + 1,
112
- });
113
- console.log(`✓ Generated ${result.lines.length} lines:`);
114
- console.log(` Net amount: ${result.totals.net.toFixed(2)} SEK`);
115
- console.log(` VAT amount: ${result.totals.vat.toFixed(2)} SEK`);
116
- console.log(` Gross amount: ${result.totals.gross.toFixed(2)} SEK`);
117
- for (const line of result.lines) {
118
- const accountName = getAccountName(line.ledger_account_code, accounts);
119
- const debit = line.debit ? `${line.debit.toFixed(2)} SEK` : "-";
120
- const credit = line.credit ? `${line.credit.toFixed(2)} SEK` : "-";
121
- console.log(` Line ${line.line_number}: ${line.ledger_account_code} ${accountName ? `(${accountName})` : ""} | Debit: ${debit} | Credit: ${credit}`);
122
- }
123
- // Calculate total debits and credits across ALL lines (existing + new)
124
- const allLinesForTotal = [
125
- ...(entry.lines || []).map((l) => ({
126
- line_number: l.lineNumber,
127
- ledger_account_code: l.account,
128
- debit: l.debit.amount,
129
- credit: l.credit.amount,
130
- })),
131
- ...result.lines,
132
- ];
133
- const totals = calculateLineTotals(allLinesForTotal);
134
- console.log("\nUpdating entry.yaml...");
135
- await appendLinesToEntry(entryPath, result.lines, totals);
136
- console.log(`✓ Successfully appended lines to ${entryPath} and updated totals`);
137
- console.log(` Total Debit: ${totals.totalDebit.toFixed(2)} SEK`);
138
- console.log(` Total Credit: ${totals.totalCredit.toFixed(2)} SEK`);
139
- console.log(` Balanced: ${totals.totalDebit === totals.totalCredit ? "✓" : "✗"}`);
140
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Extract text from a PDF file
3
- */
4
- import pdf from "pdf-parse";
5
- import { readFile } from "node:fs/promises";
6
- export async function parsePdfCommand(options) {
7
- const { file: filePath } = options;
8
- try {
9
- const buffer = await readFile(filePath);
10
- const data = await pdf(buffer);
11
- console.log(data.text);
12
- }
13
- catch (err) {
14
- if (err instanceof Error && "code" in err && err.code === "ENOENT") {
15
- console.error(`Error: File not found: ${filePath}`);
16
- process.exit(1);
17
- }
18
- throw err;
19
- }
20
- }