@moatless/bookkeeping 0.3.0 → 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.
@@ -1,10 +1,4 @@
1
1
  import type { IStorageService } from "../storage/interface";
2
- export interface DocumentMetadata {
3
- fileName: string;
4
- mimeType?: string;
5
- sourceIntegration: string;
6
- sourceId: string;
7
- }
8
2
  export interface DownloadableFile {
9
3
  id: string;
10
4
  contentType: string;
@@ -33,7 +27,7 @@ export interface DownloadFilesOptions {
33
27
  entryDir: string;
34
28
  journalEntryId: string;
35
29
  downloader: FileDownloader;
36
- sourceIntegration: string;
30
+ sourceIntegration: "fortnox" | "bokio";
37
31
  }
38
32
  /**
39
33
  * Download files for a journal entry and save them to documents.yaml
@@ -77,8 +77,10 @@ export async function downloadFilesForEntry(options) {
77
77
  const filePath = path.join(absoluteDir, filename);
78
78
  const buffer = Buffer.from(result.data);
79
79
  await fs.writeFile(filePath, buffer);
80
- // Add to documents list
80
+ // Add to documents list (downloaded docs are already POSTED in provider)
81
81
  existingDocs.push({
82
+ kind: "FILE",
83
+ status: "POSTED",
82
84
  fileName: filename,
83
85
  mimeType: file.contentType || result.contentType,
84
86
  sourceIntegration,
@@ -1,6 +1,6 @@
1
1
  export { JournalService } from "./journal.service";
2
2
  export { syncJournalEntries, type SyncJournalConfig, } from "./journal-sync";
3
- export { downloadFilesForEntry, getExtensionFromMimeType, type DocumentMetadata, type DownloadableFile, type FileDownloader, type DownloadFilesOptions, } from "./document-download";
3
+ export { downloadFilesForEntry, getExtensionFromMimeType, type DownloadableFile, type FileDownloader, type DownloadFilesOptions, } from "./document-download";
4
4
  export { syncFortnoxJournalEntries, syncFortnoxChartOfAccounts, type FortnoxSyncProgress, type FortnoxJournalSyncOptions, type FortnoxJournalSyncResult, } from "./fortnox-journal";
5
5
  export { syncFortnoxInbox, type FortnoxInboxSyncProgress, type FortnoxInboxSyncOptions, type FortnoxInboxSyncResult, } from "./fortnox-inbox";
6
6
  export { syncBokioJournalEntries, syncBokioChartOfAccounts, type BokioSyncProgress, type BokioJournalSyncOptions, type BokioJournalSyncResult, } from "./bokio-journal";
@@ -9,9 +9,18 @@ export class JournalService {
9
9
  constructor(storage) {
10
10
  this.storage = storage;
11
11
  }
12
- static getEntryKey(entry) {
13
- return (entry.externalId ??
14
- `${entry.series ?? ""}-${entry.entryNumber}`);
12
+ static getEntryKey(entry, entryDir) {
13
+ // If entry has externalId, use it (synced entries)
14
+ if (entry.externalId) {
15
+ return entry.externalId;
16
+ }
17
+ // For local entries without externalId, use directory path if available
18
+ // This prevents collisions when multiple entries have same entry number
19
+ if (entryDir) {
20
+ return entryDir;
21
+ }
22
+ // Fallback for backward compatibility
23
+ return `${entry.series ?? ""}-${entry.entryNumber}`;
15
24
  }
16
25
  /**
17
26
  * List all journal entries from a fiscal year
@@ -41,7 +50,7 @@ export class JournalService {
41
50
  try {
42
51
  const { content } = await this.storage.readFile(entryPath);
43
52
  const journalEntry = parseYaml(content);
44
- const key = JournalService.getEntryKey(journalEntry);
53
+ const key = JournalService.getEntryKey(journalEntry, dir.path);
45
54
  entriesMap.set(key, {
46
55
  entry: journalEntry,
47
56
  entryPath,
@@ -49,7 +58,14 @@ export class JournalService {
49
58
  });
50
59
  }
51
60
  catch (error) {
52
- console.error(`Failed to read ${entryPath}:`, error);
61
+ // Warn about missing/invalid entry.yaml but continue processing other entries
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ if (message.includes("ENOENT")) {
64
+ console.warn(` Warning: Skipping ${dir.path} - missing entry.yaml`);
65
+ }
66
+ else {
67
+ console.warn(` Warning: Skipping ${dir.path} - ${message}`);
68
+ }
53
69
  }
54
70
  }
55
71
  }
@@ -0,0 +1,2 @@
1
+ import type { Skill } from "./types";
2
+ export declare const CLI_USAGE_SKILL: Skill;
@@ -0,0 +1,283 @@
1
+ export const CLI_USAGE_SKILL = {
2
+ name: "cli-usage",
3
+ description: "Quick start guide for ledgit CLI - essential commands and workflow. Use when learning ledgit basics, creating entries, syncing data from accounting providers (Fortnox/Bokio), or managing inbox items. Triggers on tasks involving ledgit setup, repository structure, command usage, or git-based bookkeeping workflow.",
4
+ content: `# Ledgit CLI - Quick Start Guide
5
+
6
+ Get started with ledgit, a git-based bookkeeping system for Swedish companies integrating with Fortnox or Bokio.
7
+
8
+ ## What is Ledgit?
9
+
10
+ Ledgit stores accounting data as YAML files in a git repository, providing:
11
+ - Version control for all financial records
12
+ - Human-readable accounting entries
13
+ - Seamless integration with Swedish accounting providers
14
+ - Git workflow for review and approval
15
+
16
+ ## Repository Structure
17
+
18
+ \`\`\`
19
+ /
20
+ ├── accounts.yaml # Swedish chart of accounts (BAS codes 1000-9999)
21
+ ├── journal-entries/ # Main accounting data organized by fiscal year
22
+ │ └── FY-YYYY/ # Fiscal year folders (FY-2024, FY-2025, etc.)
23
+ │ ├── _fiscal-year.yaml
24
+ │ └── [SERIES]-[NUM]-[DATE]-[DESC]/
25
+ │ ├── entry.yaml # Journal entry data
26
+ │ └── *.pdf # Supporting documents
27
+
28
+ └── inbox/ # Raw incoming documents (no entry yet)
29
+ └── [DATE]-[NAME]/
30
+ ├── documents.yaml # Document metadata
31
+ └── *.pdf # Files to process
32
+ \`\`\`
33
+
34
+ ## Common Workflow
35
+
36
+ The typical bookkeeping workflow:
37
+
38
+ \`\`\`
39
+ inbox/ → (create-entry) → journal-entries/ → (git review) → (sync-journal) → Provider
40
+ \`\`\`
41
+
42
+ **Steps:**
43
+ 1. Run \`ledgit sync-inbox\` to download new documents from provider
44
+ 2. Review inbox item (read PDF, check \`documents.yaml\`)
45
+ 3. Run \`ledgit create-entry\` with appropriate flags
46
+ 4. Review changes with \`git diff\`, commit if correct
47
+ 5. Run \`ledgit sync-journal\` to post entry back to provider
48
+
49
+ **Branching:** When running \`create-entry\` on main branch, a new git branch is automatically created (e.g., \`book/V-189-2025-10-15-anthropic\`). Review, commit, and merge when ready.
50
+
51
+ ## Essential Commands
52
+
53
+ ### Syncing Data
54
+
55
+ \`\`\`bash
56
+ # Download new inbox documents from provider
57
+ ledgit sync-inbox
58
+
59
+ # Sync journal entries from provider to local repo
60
+ ledgit sync-journal
61
+
62
+ # Display company information
63
+ ledgit company-info
64
+ \`\`\`
65
+
66
+ ### Creating Entries
67
+
68
+ \`\`\`bash
69
+ # Create entry from inbox directory
70
+ ledgit create-entry --path <inbox-dir> \\
71
+ --tax-code <code> \\
72
+ --base-account <account> \\
73
+ --balancing-account <account> \\
74
+ --series <A-K>
75
+
76
+ # Example: Create entry for AWS invoice
77
+ ledgit create-entry --path inbox/2025-01-15-aws \\
78
+ --tax-code SE_VAT_25_PURCHASE_NON_EU_SERVICES_RC \\
79
+ --base-account 5422 \\
80
+ --balancing-account 2820 \\
81
+ --series D
82
+ \`\`\`
83
+
84
+ **Common flags:**
85
+ - \`--path\` - Path to inbox directory or raw file
86
+ - \`--tax-code\` - Swedish VAT code (see \`ledgit list-tax-codes\`)
87
+ - \`--base-account\` - Expense/revenue account (from \`accounts.yaml\`)
88
+ - \`--balancing-account\` - 2440 (payable), 2820 (credit card), or 1930 (bank)
89
+ - \`--series\` - A=Admin, D=Supplier invoices, E=Supplier payments, etc.
90
+
91
+ **For raw files** (not from inbox):
92
+ - \`--description "Vendor name"\`
93
+ - \`--document-date 2025-01-15\`
94
+ - \`--amount 1234.50\`
95
+
96
+ ### Currency Conversion
97
+
98
+ \`\`\`bash
99
+ # Convert foreign currency to SEK (automatic Riksbank rates)
100
+ ledgit convert-currency --amount 100 --currency USD --date 2025-01-15
101
+
102
+ # When creating entries with foreign currency
103
+ ledgit create-entry --path inbox/2025-01-15-stripe \\
104
+ --currency USD \\
105
+ --tax-code SE_VAT_25_PURCHASE_NON_EU_SERVICES_RC \\
106
+ --base-account 5422 \\
107
+ --balancing-account 2820 \\
108
+ --series D
109
+ \`\`\`
110
+
111
+ Amounts are automatically converted to SEK using Riksbank exchange rates for the entry date. Rates are cached locally for efficiency.
112
+
113
+ ### Managing Inbox
114
+
115
+ \`\`\`bash
116
+ # Discard unwanted inbox item (marks for deletion from provider)
117
+ ledgit discard inbox/2025-01-15-spam-document
118
+ \`\`\`
119
+
120
+ ### Reference Information
121
+
122
+ \`\`\`bash
123
+ # List all available Swedish tax codes
124
+ ledgit list-tax-codes
125
+ \`\`\`
126
+
127
+ ## Creating Your First Entry
128
+
129
+ Step-by-step walkthrough:
130
+
131
+ ### 1. Sync Inbox
132
+ \`\`\`bash
133
+ ledgit sync-inbox
134
+ \`\`\`
135
+ This downloads new documents to \`inbox/\` directories.
136
+
137
+ ### 2. Review Document
138
+ Read the PDF or check \`documents.yaml\` for metadata:
139
+ \`\`\`yaml
140
+ - fileName: invoice.pdf
141
+ description: Vendor Name
142
+ documentDate: 2025-01-15
143
+ totalAmount:
144
+ amount: "1250.00"
145
+ currency: SEK
146
+ \`\`\`
147
+
148
+ ### 3. Look for Similar Entries
149
+ Search \`journal-entries/\` for entries from the same vendor to learn which accounts and tax codes were used previously.
150
+
151
+ ### 4. Check Accounts
152
+ Review \`accounts.yaml\` for appropriate expense/revenue accounts. Common examples:
153
+ - 5422 - SaaS/Software
154
+ - 6540 - IT services
155
+ - 6530 - Accounting services
156
+ - 5800 - Travel expenses
157
+
158
+ ### 5. Determine Tax Code
159
+ Based on vendor location and transaction type. **For detailed tax code guidance, run:**
160
+ \`\`\`bash
161
+ ledgit skills swedish-bookkeeping
162
+ \`\`\`
163
+
164
+ Quick reference:
165
+ - Swedish supplier with VAT → \`SE_VAT_25_PURCHASE_DOMESTIC\`
166
+ - EU services → \`SE_VAT_25_PURCHASE_EU_SERVICES_RC\`
167
+ - Non-EU (US/UK) services → \`SE_VAT_25_PURCHASE_NON_EU_SERVICES_RC\`
168
+
169
+ ### 6. Create Entry
170
+ \`\`\`bash
171
+ ledgit create-entry --path inbox/2025-01-15-vendor \\
172
+ --tax-code SE_VAT_25_PURCHASE_NON_EU_SERVICES_RC \\
173
+ --base-account 5422 \\
174
+ --balancing-account 2820 \\
175
+ --series D
176
+ \`\`\`
177
+
178
+ ### 7. Review and Sync
179
+ \`\`\`bash
180
+ # Review changes
181
+ git diff
182
+
183
+ # Commit if correct
184
+ git add .
185
+ git commit -m "Add: Vendor Name invoice"
186
+
187
+ # Post to provider
188
+ ledgit sync-journal
189
+ \`\`\`
190
+
191
+ ## Skills System
192
+
193
+ Ledgit includes domain-specific skills with detailed guidance. **Always load the relevant skill before creating unfamiliar entries.**
194
+
195
+ \`\`\`bash
196
+ # List all available skills
197
+ ledgit skills
198
+
199
+ # Load specific skill
200
+ ledgit skills swedish-bookkeeping # VAT, tax codes, BAS accounts
201
+ ledgit skills travel-expenses # Hotels, flights, per diem
202
+ \`\`\`
203
+
204
+ **When to use skills:**
205
+ - \`swedish-bookkeeping\` - Creating any journal entry, handling VAT/moms, selecting accounts
206
+ - \`travel-expenses\` - Business travel, hotels, flights, traktamente (per diem), mileage
207
+
208
+ ## Entry File Format
209
+
210
+ Basic \`entry.yaml\` structure:
211
+
212
+ \`\`\`yaml
213
+ series: D # A=Admin, B=Customer invoices, C=Customer payments,
214
+ # D=Supplier invoices, E=Supplier payments, K=Salary
215
+ entryNumber: 189
216
+ entryDate: 2025-01-15
217
+ description: Vendor Name - Service description
218
+ status: POSTED # or DRAFT
219
+ currency: SEK
220
+ lines:
221
+ - account: "5422" # Expense account (from accounts.yaml)
222
+ debit: 1000.00
223
+ memo: SaaS subscription
224
+ - account: "2645" # Input VAT (if reverse charge)
225
+ debit: 250.00
226
+ memo: Input VAT reverse charge
227
+ - account: "2614" # Output VAT (if reverse charge)
228
+ credit: 250.00
229
+ memo: Output VAT reverse charge
230
+ - account: "2820" # Balancing account
231
+ credit: 1000.00
232
+ memo: Credit card payment
233
+ \`\`\`
234
+
235
+ **Entry must balance:** Total debits must equal total credits.
236
+
237
+ ## Series Codes
238
+
239
+ | Series | Swedish | Use |
240
+ |--------|---------|-----|
241
+ | A | Administration | Internal adjustments, corrections |
242
+ | B | Kundfakturor | Customer invoices (sales) |
243
+ | C | Kundbetalningar | Customer payments received |
244
+ | D | Leverantörsfakturor | Supplier invoices (purchases) |
245
+ | E | Leverantörsbetalningar | Supplier payments made |
246
+ | K | Löner | Salary and payroll |
247
+
248
+ Most common: **D** (supplier invoices) and **E** (supplier payments).
249
+
250
+ ## Account Codes Quick Reference
251
+
252
+ Swedish BAS account ranges:
253
+ - **1xxx** - Assets (bank accounts, receivables)
254
+ - **2xxx** - Liabilities (payables, VAT, credit cards)
255
+ - **3xxx** - Revenue
256
+ - **4xxx** - Cost of Goods Sold
257
+ - **5-6xxx** - Operating Expenses
258
+ - **7xxx** - Personnel costs
259
+ - **8xxx** - Financial items
260
+
261
+ See \`accounts.yaml\` in your repository for the company's full chart of accounts.
262
+
263
+ ## Common Balancing Accounts
264
+
265
+ | Account | Name | Use |
266
+ |---------|------|-----|
267
+ | 1930 | Business checking account | Direct debit/immediate payment |
268
+ | 2440 | Supplier payable | When paying later via bank transfer |
269
+ | 2820 | Credit card | Direct credit card charge |
270
+
271
+ ## Next Steps
272
+
273
+ 1. Run \`ledgit company-info\` to verify your setup
274
+ 2. Run \`ledgit sync-inbox\` to get documents
275
+ 3. Run \`ledgit skills swedish-bookkeeping\` for detailed tax code guidance
276
+ 4. Create your first entry following the workflow above
277
+ 5. Review with \`git diff\` and commit when ready
278
+
279
+ **Questions?** Load the appropriate skill for detailed guidance:
280
+ - General bookkeeping → \`ledgit skills swedish-bookkeeping\`
281
+ - Travel expenses → \`ledgit skills travel-expenses\`
282
+ `,
283
+ };
@@ -1,6 +1,7 @@
1
1
  export type { Skill } from "./types";
2
2
  export { SWEDISH_BOOKKEEPING_SKILL } from "./swedish-bookkeeping";
3
3
  export { TRAVEL_EXPENSES_SKILL } from "./travel-expenses";
4
+ export { CLI_USAGE_SKILL } from "./cli-usage";
4
5
  import type { Skill } from "./types";
5
6
  export declare const SKILLS: Skill[];
6
7
  export declare function listSkills(): Skill[];
@@ -1,5 +1,6 @@
1
1
  export { SWEDISH_BOOKKEEPING_SKILL } from "./swedish-bookkeeping";
2
2
  export { TRAVEL_EXPENSES_SKILL } from "./travel-expenses";
3
+ export { CLI_USAGE_SKILL } from "./cli-usage";
3
4
  import { SWEDISH_BOOKKEEPING_SKILL } from "./swedish-bookkeeping";
4
5
  import { TRAVEL_EXPENSES_SKILL } from "./travel-expenses";
5
6
  export const SKILLS = [
@@ -25,11 +25,11 @@ export interface TaxDetail {
25
25
  /**
26
26
  * Document kind/type
27
27
  */
28
- export type DocumentKind = "RECEIPT" | "SUPPLIER_INVOICE" | "CUSTOMER_INVOICE" | "TRAKTAMENTE" | "OTHER";
28
+ export type DocumentKind = "RECEIPT" | "SUPPLIER_INVOICE" | "CUSTOMER_INVOICE" | "TRAKTAMENTE" | "FILE" | "OTHER";
29
29
  /**
30
30
  * Document status in workflow
31
31
  */
32
- export type DocumentStatus = "DRAFT" | "REVIEWED" | "READY_TO_EXPORT" | "EXPORTED" | "ERROR" | "DISCARDED" | "POSTED";
32
+ export type DocumentStatus = "DRAFT" | "REVIEWED" | "READY_TO_EXPORT" | "EXPORTED" | "ERROR" | "DISCARDED" | "POSTED" | "PENDING_CONNECTION";
33
33
  /**
34
34
  * Complete exported document with parsed data
35
35
  *
@@ -60,4 +60,7 @@ export interface Document {
60
60
  sourceId?: string;
61
61
  uploadedAt?: string;
62
62
  interpretedAt?: string;
63
+ errorMessage?: string;
64
+ linkedToJournalEntry?: boolean;
65
+ previousSourceIds?: string[];
63
66
  }
@@ -66,7 +66,6 @@ export interface JournalEntry {
66
66
  lines: JournalLine[];
67
67
  postedAt?: string;
68
68
  errorMessage?: string;
69
- postingCheckpoint?: string;
70
69
  voucherNumber?: number;
71
70
  voucherSeriesCode?: string;
72
71
  fortnoxFileId?: string;
@@ -76,4 +75,6 @@ export interface JournalEntry {
76
75
  reversingEntryExternalId?: string;
77
76
  /** If this entry was reversed/cancelled, the external ID of the reversing entry */
78
77
  reversedByEntryExternalId?: string;
78
+ /** The external ID of the fiscal year this entry belongs to (e.g., Fortnox year ID) */
79
+ fiscalYearExternalId?: number;
79
80
  }
@@ -2,8 +2,13 @@
2
2
  * Initialize a new git repository with a .gitignore file
3
3
  */
4
4
  export declare function initGitRepo(repoPath: string): Promise<void>;
5
+ /**
6
+ * Check if a directory is a git repository
7
+ */
8
+ export declare function isGitRepo(repoPath: string): Promise<boolean>;
5
9
  /**
6
10
  * Stage all changes and commit with the given message.
7
11
  * Returns true if a commit was made, false if there were no changes to commit.
12
+ * Returns false if the directory is not a git repository.
8
13
  */
9
14
  export declare function commitAll(repoPath: string, message: string): Promise<boolean>;
package/dist/utils/git.js CHANGED
@@ -26,11 +26,29 @@ export async function initGitRepo(repoPath) {
26
26
  await git.init();
27
27
  await fs.writeFile(path.join(repoPath, ".gitignore"), DEFAULT_GITIGNORE);
28
28
  }
29
+ /**
30
+ * Check if a directory is a git repository
31
+ */
32
+ export async function isGitRepo(repoPath) {
33
+ try {
34
+ const git = simpleGit(repoPath);
35
+ await git.revparse(["--git-dir"]);
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
29
42
  /**
30
43
  * Stage all changes and commit with the given message.
31
44
  * Returns true if a commit was made, false if there were no changes to commit.
45
+ * Returns false if the directory is not a git repository.
32
46
  */
33
47
  export async function commitAll(repoPath, message) {
48
+ // Check if it's a git repository first
49
+ if (!(await isGitRepo(repoPath))) {
50
+ return false;
51
+ }
34
52
  const git = simpleGit(repoPath);
35
53
  await git.add(".");
36
54
  const status = await git.status();
@@ -1,6 +1,6 @@
1
1
  export { journalEntryDirName, journalEntryPath, journalEntryDirFromPath, fiscalYearDirName, sanitizeOrgName, } from "./file-namer";
2
2
  export { parseYaml, toYaml } from "./yaml";
3
3
  export { withRetry, isRateLimitError, type RetryOptions } from "./retry";
4
- export { initGitRepo, commitAll } from "./git";
4
+ export { initGitRepo, commitAll, isGitRepo } from "./git";
5
5
  export { getAgentsTemplate, renderAgentsTemplate, type RenderAgentsTemplateOptions, } from "./templates";
6
6
  export { LEDGIT_DIR, getLedgitDir, getTokensDir, getCacheDir, } from "./paths";
@@ -1,6 +1,6 @@
1
1
  export { journalEntryDirName, journalEntryPath, journalEntryDirFromPath, fiscalYearDirName, sanitizeOrgName, } from "./file-namer";
2
2
  export { parseYaml, toYaml } from "./yaml";
3
3
  export { withRetry, isRateLimitError } from "./retry";
4
- export { initGitRepo, commitAll } from "./git";
4
+ export { initGitRepo, commitAll, isGitRepo } from "./git";
5
5
  export { getAgentsTemplate, renderAgentsTemplate, } from "./templates";
6
6
  export { LEDGIT_DIR, getLedgitDir, getTokensDir, getCacheDir, } from "./paths";
@@ -8,16 +8,10 @@ import type { JournalLineInput } from "../accounting";
8
8
  * Read and parse entry.yaml file
9
9
  */
10
10
  export declare function readEntryYaml(filePath: string): Promise<JournalEntry>;
11
- interface DocumentMetadata {
12
- fileName: string;
13
- mimeType?: string;
14
- sourceIntegration?: string;
15
- sourceId?: string;
16
- }
17
11
  /**
18
12
  * Read and parse documents.yaml file (list format)
19
13
  */
20
- export declare function readDocumentsYaml(filePath: string): Promise<DocumentMetadata[]>;
14
+ export declare function readDocumentsYaml(filePath: string): Promise<Document[]>;
21
15
  /**
22
16
  * @deprecated Use readDocumentsYaml instead
23
17
  * Read and parse document.yaml file (legacy single object format)
@@ -54,4 +48,3 @@ export declare function getAccountName(accountCode: string, accounts: Array<{
54
48
  code: string;
55
49
  name: string;
56
50
  }>): string | undefined;
57
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moatless/bookkeeping",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@moatless/api-client": "^0.1.2",
26
- "@moatless/bookkeeping-types": "^0.2.0",
26
+ "@moatless/bookkeeping-types": "^0.3.0",
27
27
  "@moatless/fortnox-client": "^0.1.2",
28
28
  "@moatless/bokio-client": "^0.1.2",
29
29
  "cli-progress": "^3.12.0",