@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.
- package/dist/accounting/index.d.ts +9 -0
- package/dist/accounting/index.js +14 -0
- package/dist/accounting/line-generator.d.ts +34 -0
- package/dist/accounting/line-generator.js +136 -0
- package/dist/accounting/tax-codes.d.ts +32 -0
- package/dist/accounting/tax-codes.js +279 -0
- package/dist/accounting/traktamente-rates.d.ts +48 -0
- package/dist/accounting/traktamente-rates.js +325 -0
- package/dist/accounting/types.d.ts +69 -0
- package/dist/accounting/types.js +5 -0
- package/dist/accounting/validation.d.ts +41 -0
- package/dist/accounting/validation.js +118 -0
- package/dist/auth/fortnox-login.d.ts +15 -0
- package/dist/auth/fortnox-login.js +170 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth/prompts.d.ts +6 -0
- package/dist/auth/prompts.js +56 -0
- package/dist/auth/token-store.d.ts +19 -0
- package/dist/auth/token-store.js +54 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +21 -0
- package/dist/progress/index.d.ts +1 -0
- package/dist/progress/index.js +1 -0
- package/dist/progress/sync-progress.d.ts +31 -0
- package/dist/progress/sync-progress.js +65 -0
- package/dist/services/bokio-journal.d.ts +29 -0
- package/dist/services/bokio-journal.js +175 -0
- package/dist/services/document-download.d.ts +46 -0
- package/dist/services/document-download.js +105 -0
- package/dist/services/fortnox-inbox.d.ts +18 -0
- package/dist/services/fortnox-inbox.js +150 -0
- package/dist/services/fortnox-journal.d.ts +22 -0
- package/dist/services/fortnox-journal.js +166 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +6 -0
- package/dist/services/journal-sync.d.ts +23 -0
- package/dist/services/journal-sync.js +124 -0
- package/dist/services/journal.service.d.ts +45 -0
- package/dist/services/journal.service.js +204 -0
- package/dist/storage/filesystem.d.ts +49 -0
- package/dist/storage/filesystem.js +122 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/interface.d.ts +48 -0
- package/dist/storage/interface.js +5 -0
- package/dist/sync-types.d.ts +61 -0
- package/dist/sync-types.js +1 -0
- package/dist/transformers/bokio.d.ts +10 -0
- package/dist/transformers/bokio.js +56 -0
- package/dist/transformers/fortnox.d.ts +6 -0
- package/dist/transformers/fortnox.js +39 -0
- package/dist/transformers/index.d.ts +3 -0
- package/dist/transformers/index.js +2 -0
- package/dist/types/discarded-item.d.ts +29 -0
- package/dist/types/discarded-item.js +1 -0
- package/dist/types/document.d.ts +63 -0
- package/dist/types/document.js +9 -0
- package/dist/types/exported-document.d.ts +61 -0
- package/dist/types/exported-document.js +9 -0
- package/dist/types/exported-fiscal-year.d.ts +10 -0
- package/dist/types/exported-fiscal-year.js +1 -0
- package/dist/types/exported-inbox-document.d.ts +14 -0
- package/dist/types/exported-inbox-document.js +10 -0
- package/dist/types/fiscal-year.d.ts +10 -0
- package/dist/types/fiscal-year.js +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +10 -0
- package/dist/types/journal-entry.d.ts +79 -0
- package/dist/types/journal-entry.js +12 -0
- package/dist/types/ledger-account.d.ts +5 -0
- package/dist/types/ledger-account.js +1 -0
- package/dist/utils/file-namer.d.ts +48 -0
- package/dist/utils/file-namer.js +80 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/git.js +41 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/paths.d.ts +17 -0
- package/dist/utils/paths.js +24 -0
- package/dist/utils/retry.d.ts +17 -0
- package/dist/utils/retry.js +48 -0
- package/dist/utils/templates.d.ts +12 -0
- package/dist/utils/templates.js +222 -0
- package/dist/utils/yaml.d.ts +12 -0
- package/dist/utils/yaml.js +47 -0
- package/dist/yaml/entry-helpers.d.ts +57 -0
- package/dist/yaml/entry-helpers.js +125 -0
- package/dist/yaml/index.d.ts +2 -0
- package/dist/yaml/index.js +2 -0
- package/dist/yaml/yaml-serializer.d.ts +21 -0
- package/dist/yaml/yaml-serializer.js +60 -0
- package/package.json +37 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the raw AGENTS.md template with placeholders
|
|
3
|
+
*/
|
|
4
|
+
export declare function getAgentsTemplate(): string;
|
|
5
|
+
export interface RenderAgentsTemplateOptions {
|
|
6
|
+
companyName: string;
|
|
7
|
+
provider: "Bokio" | "Fortnox";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Render the AGENTS.md template with variable substitution
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderAgentsTemplate(options: RenderAgentsTemplateOptions): string;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Template inlined as string to ensure it's bundled properly
|
|
2
|
+
const AGENTS_TEMPLATE = `# AGENTS.md
|
|
3
|
+
|
|
4
|
+
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.
|
|
5
|
+
|
|
6
|
+
## Repository Structure
|
|
7
|
+
|
|
8
|
+
\`\`\`
|
|
9
|
+
/
|
|
10
|
+
├── accounts.yaml # Swedish chart of accounts (account codes 1000-9999)
|
|
11
|
+
├── journal-entries/ # Main accounting data organized by fiscal year
|
|
12
|
+
│ └── FY-YYYY/ # Fiscal year folders (FY-2014 through FY-2026)
|
|
13
|
+
│ ├── _fiscal-year.yaml
|
|
14
|
+
│ └── [SERIES]-[NUM]-[DATE]-[DESC]/
|
|
15
|
+
│ ├── entry.yaml
|
|
16
|
+
│ └── *.pdf # Supporting documents
|
|
17
|
+
│
|
|
18
|
+
├── inbox/ # Raw incoming documents (no entry yet)
|
|
19
|
+
│ └── [DATE]-[NAME]/
|
|
20
|
+
│ ├── documents.yaml
|
|
21
|
+
│ └── *.pdf
|
|
22
|
+
└── drafts/ # Documents with entries, ready to post
|
|
23
|
+
└── [DATE]-[NAME]/ # e.g., "2025-12-24-anthropic"
|
|
24
|
+
├── documents.yaml
|
|
25
|
+
├── entry.yaml
|
|
26
|
+
└── *.pdf
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
## CLI Commands
|
|
30
|
+
|
|
31
|
+
Use the \`npx ledgit-cli\` CLI to interact with this repository:
|
|
32
|
+
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
# Sync data from provider
|
|
35
|
+
npx ledgit-cli sync-journal # Sync journal entries from accounting provider
|
|
36
|
+
npx ledgit-cli sync-inbox # Download inbox files from accounting provider
|
|
37
|
+
npx ledgit-cli company-info # Display company information
|
|
38
|
+
|
|
39
|
+
# Create journal entries
|
|
40
|
+
npx ledgit-cli create-entry --path <inbox-dir-or-file> \\
|
|
41
|
+
--tax-code <code> --base-account <acc> --balancing-account <acc> \\
|
|
42
|
+
--series <A-K> --entry-number <num>
|
|
43
|
+
|
|
44
|
+
# For raw files (not from inbox), also provide:
|
|
45
|
+
# --description "Vendor name" --document-date 2025-01-15 --amount 1234.50
|
|
46
|
+
|
|
47
|
+
# Manage inbox
|
|
48
|
+
npx ledgit-cli discard <inbox-directory> # Remove item, mark for deletion from provider
|
|
49
|
+
npx ledgit-cli parse-pdf -f <file.pdf> # Extract text from PDF (for analysis)
|
|
50
|
+
|
|
51
|
+
# Currency conversion
|
|
52
|
+
npx ledgit-cli convert-currency --amount <num> --currency <code> [--date YYYY-MM-DD]
|
|
53
|
+
# Converts foreign currency to SEK using Riksbank exchange rates
|
|
54
|
+
# Caches rates locally for efficiency
|
|
55
|
+
|
|
56
|
+
# Generate journal entry lines (alternative to create-entry for adding lines)
|
|
57
|
+
npx ledgit-cli generate-lines --inbox-dir <path> \\
|
|
58
|
+
--tax-code <code> --base-amount <num> --base-account <acc> \\
|
|
59
|
+
--balancing-account <acc> [--memo "description"] [--currency USD]
|
|
60
|
+
|
|
61
|
+
# List available tax codes
|
|
62
|
+
npx ledgit-cli list-tax-codes
|
|
63
|
+
|
|
64
|
+
# Update instructions
|
|
65
|
+
npx ledgit-cli update # Update AGENTS.md to latest version
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
**Currency Conversion:** Both \`create-entry\` and \`generate-lines\` automatically convert to SEK when \`--currency\` is different from SEK, using Riksbank exchange rates for the entry date.
|
|
69
|
+
|
|
70
|
+
## Workflow Overview
|
|
71
|
+
|
|
72
|
+
\`\`\`
|
|
73
|
+
inbox/ → (create-entry) → drafts/ → (user confirms) → journal-entries/ → (sync-journal) → Provider
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
## Bookkeeping New Entries
|
|
77
|
+
|
|
78
|
+
**Important:** Always ask the user for clarification if anything is unclear about the transaction before creating an entry.
|
|
79
|
+
|
|
80
|
+
To create a journal entry from an inbox document:
|
|
81
|
+
|
|
82
|
+
1. **Read the document** - Use \`npx ledgit-cli parse-pdf\` for PDFs to understand the content
|
|
83
|
+
2. **Update documents.yaml** - After parsing, populate missing metadata in \`documents.yaml\`:
|
|
84
|
+
- \`documentDate\`: Invoice/receipt date (YYYY-MM-DD)
|
|
85
|
+
- \`description\`: Vendor or document name
|
|
86
|
+
- \`totalAmount\`: With \`amount\` (string) and \`currency\` (e.g., EUR, SEK)
|
|
87
|
+
3. **Look for similar entries** - Search \`journal-entries/\` and \`drafts/\` for entries from the same vendor or similar transaction types to learn which accounts and tax codes were used previously
|
|
88
|
+
4. **Check accounts** - Review \`accounts.yaml\` for appropriate expense/revenue accounts
|
|
89
|
+
5. **Determine tax code** - Based on vendor location and transaction type (see Tax Codes below). Explain your reasoning and describe the tax code meaning to the user
|
|
90
|
+
6. **Ask for confirmation** - Before creating, confirm with the user: vendor, amount, accounts (with names), and tax code (with description)
|
|
91
|
+
7. **Create entry** - Run \`npx ledgit-cli create-entry\` with the appropriate parameters
|
|
92
|
+
8. **Files moved to drafts** - The inbox item is automatically moved to \`drafts/\` with \`entry.yaml\`
|
|
93
|
+
|
|
94
|
+
To discard unwanted inbox items:
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
npx ledgit-cli discard inbox/2025-01-15-spam-document
|
|
97
|
+
\`\`\`
|
|
98
|
+
This removes the directory and marks the item for deletion from the provider on next sync.
|
|
99
|
+
|
|
100
|
+
## Posting Entries
|
|
101
|
+
|
|
102
|
+
**Important:** Moving entries to \`journal-entries/\` posts them to the real ledger. Always confirm with the user before posting.
|
|
103
|
+
|
|
104
|
+
When the user confirms draft entries are ready to post:
|
|
105
|
+
|
|
106
|
+
1. **Determine entry number** - Check existing entries in \`journal-entries/FY-YYYY/\` for the series to find the next available number
|
|
107
|
+
2. **Move to journal-entries** - Rename the draft directory to the proper format:
|
|
108
|
+
\`\`\`
|
|
109
|
+
drafts/2025-01-15-anthropic/
|
|
110
|
+
→ journal-entries/FY-2025/A-042-2025-01-15-anthropic/
|
|
111
|
+
\`\`\`
|
|
112
|
+
Format: \`{SERIES}-{NUM}-{DATE}-{DESC}/\`
|
|
113
|
+
3. **Update entry.yaml** - Ensure \`entryNumber\` matches the directory and \`status\` is set appropriately
|
|
114
|
+
4. **Sync to provider** - Run \`npx ledgit-cli sync-journal\` to post the entries to {{PROVIDER}}
|
|
115
|
+
|
|
116
|
+
## Tax Codes
|
|
117
|
+
|
|
118
|
+
### Common Tax Code Patterns
|
|
119
|
+
|
|
120
|
+
| Scenario | Tax Code | Accounts |
|
|
121
|
+
|----------|----------|----------|
|
|
122
|
+
| Domestic purchase 25% | \`SE_VAT_25_PURCHASE_DOMESTIC\` | Expense + 2641 + 2440 |
|
|
123
|
+
| Domestic purchase 12% | \`SE_VAT_12_PURCHASE_DOMESTIC\` | Expense + 2641 + 2440 |
|
|
124
|
+
| Non-EU services (US SaaS) | \`SE_VAT_25_PURCHASE_NON_EU_SERVICES_RC\` | 5422 + 2645 + 2614 + 2820 |
|
|
125
|
+
| EU services | \`SE_VAT_25_PURCHASE_EU_SERVICES_RC\` | Expense + 2645 + 2614 + 2440 |
|
|
126
|
+
| Exempt purchase | \`SE_VAT_EXEMPT_PURCHASE\` | Expense + balancing |
|
|
127
|
+
|
|
128
|
+
**Notes:**
|
|
129
|
+
- \`RC\` = Reverse Charge (buyer reports VAT instead of seller)
|
|
130
|
+
- Use \`--list-tax-codes\` to see all available codes with descriptions
|
|
131
|
+
- Account 2641 = Incoming VAT (domestic), 2645 = Incoming VAT (reverse charge)
|
|
132
|
+
- Account 2614 = Outgoing VAT (reverse charge)
|
|
133
|
+
|
|
134
|
+
## Entities
|
|
135
|
+
|
|
136
|
+
### Journal Entry (entry.yaml)
|
|
137
|
+
|
|
138
|
+
\`\`\`yaml
|
|
139
|
+
series: A # A=Admin, B=Customer invoices, C=Customer payments,
|
|
140
|
+
# D=Supplier invoices, E=Supplier payments, K=Salary
|
|
141
|
+
entryNumber: 1
|
|
142
|
+
entryDate: 2024-01-03
|
|
143
|
+
description: Transaction description
|
|
144
|
+
status: POSTED # or DRAFT
|
|
145
|
+
currency: SEK
|
|
146
|
+
lines:
|
|
147
|
+
- account: "1930" # Account code from accounts.yaml
|
|
148
|
+
debit: 1000.00 # or credit:
|
|
149
|
+
memo: Account description
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
### Document Metadata (documents.yaml)
|
|
153
|
+
|
|
154
|
+
One entry can have multiple supporting documents (e.g., invoice + receipt, contract + amendment).
|
|
155
|
+
|
|
156
|
+
\`\`\`yaml
|
|
157
|
+
- fileName: invoice.pdf
|
|
158
|
+
mimeType: application/pdf
|
|
159
|
+
sourceIntegration: {{PROVIDER_LOWER}}
|
|
160
|
+
sourceId: UUID-1
|
|
161
|
+
description: Vendor invoice
|
|
162
|
+
documentDate: 2024-01-15
|
|
163
|
+
totalAmount:
|
|
164
|
+
amount: "1250.00"
|
|
165
|
+
currency: SEK
|
|
166
|
+
- fileName: receipt.pdf
|
|
167
|
+
mimeType: application/pdf
|
|
168
|
+
sourceIntegration: {{PROVIDER_LOWER}}
|
|
169
|
+
sourceId: UUID-2
|
|
170
|
+
description: Payment receipt
|
|
171
|
+
documentDate: 2024-01-16
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
### Fiscal Year (_fiscal-year.yaml)
|
|
175
|
+
|
|
176
|
+
\`\`\`yaml
|
|
177
|
+
name: FY 2024
|
|
178
|
+
startDate: 2024-01-01
|
|
179
|
+
endDate: 2024-12-31
|
|
180
|
+
status: OPEN
|
|
181
|
+
externalId: "12" # Provider reference
|
|
182
|
+
\`\`\`
|
|
183
|
+
|
|
184
|
+
## Account Codes
|
|
185
|
+
|
|
186
|
+
Swedish BAS account codes are organized by ranges:
|
|
187
|
+
|
|
188
|
+
- **1xxx** - Assets (Tillgångar)
|
|
189
|
+
- **2xxx** - Liabilities & Equity (Skulder och eget kapital)
|
|
190
|
+
- **3xxx** - Revenue (Intäkter)
|
|
191
|
+
- **4xxx** - Cost of goods sold (Inköp)
|
|
192
|
+
- **5xxx-6xxx** - Operating expenses (Kostnader)
|
|
193
|
+
- **7xxx** - Personnel costs (Personalkostnader)
|
|
194
|
+
- **8xxx** - Financial items (Finansiella poster)
|
|
195
|
+
- **9xxx** - Year-end allocations (Bokslutsdispositioner)
|
|
196
|
+
|
|
197
|
+
Common accounts:
|
|
198
|
+
- \`1930\` - Business bank account
|
|
199
|
+
- \`2440\` - Supplier payables
|
|
200
|
+
- \`2610\` - Outgoing VAT 25%
|
|
201
|
+
- \`2640\` - Incoming VAT
|
|
202
|
+
- \`2820\` - Short-term liabilities (credit card, etc.)
|
|
203
|
+
- \`4000\` - Cost of goods sold
|
|
204
|
+
- \`5420\` - Software and IT services
|
|
205
|
+
- \`6570\` - Bank charges
|
|
206
|
+
`;
|
|
207
|
+
/**
|
|
208
|
+
* Get the raw AGENTS.md template with placeholders
|
|
209
|
+
*/
|
|
210
|
+
export function getAgentsTemplate() {
|
|
211
|
+
return AGENTS_TEMPLATE;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Render the AGENTS.md template with variable substitution
|
|
215
|
+
*/
|
|
216
|
+
export function renderAgentsTemplate(options) {
|
|
217
|
+
const template = getAgentsTemplate();
|
|
218
|
+
return template
|
|
219
|
+
.replace(/\{\{COMPANY_NAME\}\}/g, options.companyName)
|
|
220
|
+
.replace(/\{\{PROVIDER\}\}/g, options.provider)
|
|
221
|
+
.replace(/\{\{PROVIDER_LOWER\}\}/g, options.provider.toLowerCase());
|
|
222
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse YAML string to object
|
|
3
|
+
*/
|
|
4
|
+
export declare function parseYaml<T = unknown>(yaml: string): T;
|
|
5
|
+
/**
|
|
6
|
+
* Convert data to YAML format with special handling for:
|
|
7
|
+
* - Monetary values (Json type: {amount, currency})
|
|
8
|
+
* - Enums (direct string values)
|
|
9
|
+
* - Nulls (omitted for cleaner output)
|
|
10
|
+
* - Arrays and nested objects
|
|
11
|
+
*/
|
|
12
|
+
export declare function toYaml(data: unknown): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Parse YAML string to object
|
|
4
|
+
*/
|
|
5
|
+
export function parseYaml(yaml) {
|
|
6
|
+
return YAML.parse(yaml);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Convert data to YAML format with special handling for:
|
|
10
|
+
* - Monetary values (Json type: {amount, currency})
|
|
11
|
+
* - Enums (direct string values)
|
|
12
|
+
* - Nulls (omitted for cleaner output)
|
|
13
|
+
* - Arrays and nested objects
|
|
14
|
+
*/
|
|
15
|
+
export function toYaml(data) {
|
|
16
|
+
// Remove null values for cleaner output
|
|
17
|
+
const cleanData = removeNulls(data);
|
|
18
|
+
return YAML.stringify(cleanData, {
|
|
19
|
+
indent: 2,
|
|
20
|
+
lineWidth: 120,
|
|
21
|
+
minContentWidth: 0,
|
|
22
|
+
doubleQuotedAsJSON: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Recursively remove null values from objects and arrays
|
|
27
|
+
* This produces cleaner YAML output by omitting null fields
|
|
28
|
+
*/
|
|
29
|
+
function removeNulls(obj) {
|
|
30
|
+
if (obj === null || obj === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(obj)) {
|
|
34
|
+
return obj.map(removeNulls).filter((item) => item !== undefined);
|
|
35
|
+
}
|
|
36
|
+
if (typeof obj === "object" && obj !== null) {
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
39
|
+
const cleanValue = removeNulls(value);
|
|
40
|
+
if (cleanValue !== undefined) {
|
|
41
|
+
result[key] = cleanValue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return obj;
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML helper functions for reading, updating, and validating journal entry files
|
|
3
|
+
* Used by CLI tools
|
|
4
|
+
*/
|
|
5
|
+
import type { JournalEntry, Document } from "../types";
|
|
6
|
+
import type { JournalLineInput } from "../accounting";
|
|
7
|
+
/**
|
|
8
|
+
* Read and parse entry.yaml file
|
|
9
|
+
*/
|
|
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
|
+
/**
|
|
18
|
+
* Read and parse documents.yaml file (list format)
|
|
19
|
+
*/
|
|
20
|
+
export declare function readDocumentsYaml(filePath: string): Promise<DocumentMetadata[]>;
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Use readDocumentsYaml instead
|
|
23
|
+
* Read and parse document.yaml file (legacy single object format)
|
|
24
|
+
*/
|
|
25
|
+
export declare function readDocumentYaml(filePath: string): Promise<Document>;
|
|
26
|
+
/**
|
|
27
|
+
* Write entry to entry.yaml file
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeEntryYaml(filePath: string, entry: JournalEntry): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Update entry with new lines (appending and renumbering)
|
|
32
|
+
*/
|
|
33
|
+
export declare function appendLinesToEntry(entryPath: string, newLines: JournalLineInput[], totals: {
|
|
34
|
+
totalDebit: number;
|
|
35
|
+
totalCredit: number;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Read and parse accounts.yaml file
|
|
39
|
+
*/
|
|
40
|
+
export declare function readAccountsYaml(filePath: string): Promise<{
|
|
41
|
+
code: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Validate that account code exists in accounts list
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateAccountExists(accountCode: string, accounts: Array<{
|
|
48
|
+
code: string;
|
|
49
|
+
}>): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Get account name by code
|
|
52
|
+
*/
|
|
53
|
+
export declare function getAccountName(accountCode: string, accounts: Array<{
|
|
54
|
+
code: string;
|
|
55
|
+
name: string;
|
|
56
|
+
}>): string | undefined;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML helper functions for reading, updating, and validating journal entry files
|
|
3
|
+
* Used by CLI tools
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import { parseYaml, toYaml } from "./yaml-serializer";
|
|
7
|
+
/**
|
|
8
|
+
* Read and parse entry.yaml file
|
|
9
|
+
*/
|
|
10
|
+
export async function readEntryYaml(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
13
|
+
return parseYaml(content);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new Error(`Failed to read entry.yaml at ${filePath}: ${err}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read and parse documents.yaml file (list format)
|
|
21
|
+
*/
|
|
22
|
+
export async function readDocumentsYaml(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
25
|
+
return parseYaml(content) || [];
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
throw new Error(`Failed to read documents.yaml at ${filePath}: ${err}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @deprecated Use readDocumentsYaml instead
|
|
33
|
+
* Read and parse document.yaml file (legacy single object format)
|
|
34
|
+
*/
|
|
35
|
+
export async function readDocumentYaml(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
38
|
+
return parseYaml(content);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new Error(`Failed to read document.yaml at ${filePath}: ${err}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Write entry to entry.yaml file
|
|
46
|
+
*/
|
|
47
|
+
export async function writeEntryYaml(filePath, entry) {
|
|
48
|
+
try {
|
|
49
|
+
const yaml = toYaml(entry);
|
|
50
|
+
await fs.writeFile(filePath, yaml, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw new Error(`Failed to write entry.yaml at ${filePath}: ${err}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Update entry with new lines (appending and renumbering)
|
|
58
|
+
*/
|
|
59
|
+
export async function appendLinesToEntry(entryPath, newLines, totals) {
|
|
60
|
+
// Read existing entry
|
|
61
|
+
const entry = await readEntryYaml(entryPath);
|
|
62
|
+
// Combine existing and new lines
|
|
63
|
+
const allLines = [...(entry.lines || [])];
|
|
64
|
+
// Append new lines with proper formatting
|
|
65
|
+
for (const newLine of newLines) {
|
|
66
|
+
allLines.push({
|
|
67
|
+
lineNumber: newLine.line_number,
|
|
68
|
+
account: newLine.ledger_account_code,
|
|
69
|
+
debit: {
|
|
70
|
+
amount: newLine.debit || 0,
|
|
71
|
+
currency: "SEK",
|
|
72
|
+
},
|
|
73
|
+
credit: {
|
|
74
|
+
amount: newLine.credit || 0,
|
|
75
|
+
currency: "SEK",
|
|
76
|
+
},
|
|
77
|
+
...(newLine.memo && { memo: newLine.memo }),
|
|
78
|
+
...(newLine.tax_code && { taxCode: newLine.tax_code }),
|
|
79
|
+
...(newLine.cost_center_code && {
|
|
80
|
+
costCenter: newLine.cost_center_code,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Renumber all lines sequentially
|
|
85
|
+
allLines.forEach((line, index) => {
|
|
86
|
+
line.lineNumber = index + 1;
|
|
87
|
+
});
|
|
88
|
+
// Update entry
|
|
89
|
+
entry.lines = allLines;
|
|
90
|
+
entry.totalDebit = {
|
|
91
|
+
amount: totals.totalDebit,
|
|
92
|
+
currency: "SEK",
|
|
93
|
+
};
|
|
94
|
+
entry.totalCredit = {
|
|
95
|
+
amount: totals.totalCredit,
|
|
96
|
+
currency: "SEK",
|
|
97
|
+
};
|
|
98
|
+
// Write back
|
|
99
|
+
await writeEntryYaml(entryPath, entry);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Read and parse accounts.yaml file
|
|
103
|
+
*/
|
|
104
|
+
export async function readAccountsYaml(filePath) {
|
|
105
|
+
try {
|
|
106
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
107
|
+
const data = parseYaml(content);
|
|
108
|
+
return data.accounts || [];
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new Error(`Failed to read accounts.yaml at ${filePath}: ${err}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Validate that account code exists in accounts list
|
|
116
|
+
*/
|
|
117
|
+
export function validateAccountExists(accountCode, accounts) {
|
|
118
|
+
return accounts.some((account) => account.code === accountCode);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get account name by code
|
|
122
|
+
*/
|
|
123
|
+
export function getAccountName(accountCode, accounts) {
|
|
124
|
+
return accounts.find((account) => account.code === accountCode)?.name;
|
|
125
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse YAML string to object
|
|
3
|
+
*/
|
|
4
|
+
export declare function parseYaml<T = unknown>(yaml: string): T;
|
|
5
|
+
/**
|
|
6
|
+
* Convert data to YAML format with special handling for:
|
|
7
|
+
* - Monetary values (Json type: {amount, currency})
|
|
8
|
+
* - Enums (direct string values)
|
|
9
|
+
* - Nulls (omitted for cleaner output)
|
|
10
|
+
* - Arrays and nested objects
|
|
11
|
+
*/
|
|
12
|
+
export declare function toYaml(data: unknown): string;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a single object to YAML
|
|
15
|
+
*/
|
|
16
|
+
export declare function objectToYaml(obj: Record<string, unknown>): string;
|
|
17
|
+
/**
|
|
18
|
+
* Convert an array of objects to YAML
|
|
19
|
+
* The output will be a YAML array
|
|
20
|
+
*/
|
|
21
|
+
export declare function arrayToYaml(arr: unknown[]): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Parse YAML string to object
|
|
4
|
+
*/
|
|
5
|
+
export function parseYaml(yaml) {
|
|
6
|
+
return YAML.parse(yaml);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Convert data to YAML format with special handling for:
|
|
10
|
+
* - Monetary values (Json type: {amount, currency})
|
|
11
|
+
* - Enums (direct string values)
|
|
12
|
+
* - Nulls (omitted for cleaner output)
|
|
13
|
+
* - Arrays and nested objects
|
|
14
|
+
*/
|
|
15
|
+
export function toYaml(data) {
|
|
16
|
+
// Remove null values for cleaner output
|
|
17
|
+
const cleanData = removeNulls(data);
|
|
18
|
+
return YAML.stringify(cleanData, {
|
|
19
|
+
indent: 2,
|
|
20
|
+
lineWidth: 120,
|
|
21
|
+
minContentWidth: 0,
|
|
22
|
+
doubleQuotedAsJSON: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Recursively remove null values from objects and arrays
|
|
27
|
+
* This produces cleaner YAML output by omitting null fields
|
|
28
|
+
*/
|
|
29
|
+
function removeNulls(obj) {
|
|
30
|
+
if (obj === null || obj === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(obj)) {
|
|
34
|
+
return obj.map(removeNulls).filter((item) => item !== undefined);
|
|
35
|
+
}
|
|
36
|
+
if (typeof obj === "object" && obj !== null) {
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
39
|
+
const cleanValue = removeNulls(value);
|
|
40
|
+
if (cleanValue !== undefined) {
|
|
41
|
+
result[key] = cleanValue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return obj;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convert a single object to YAML
|
|
50
|
+
*/
|
|
51
|
+
export function objectToYaml(obj) {
|
|
52
|
+
return toYaml(obj);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Convert an array of objects to YAML
|
|
56
|
+
* The output will be a YAML array
|
|
57
|
+
*/
|
|
58
|
+
export function arrayToYaml(arr) {
|
|
59
|
+
return toYaml(arr);
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moatless/bookkeeping",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/aorwall/ledgit.git",
|
|
10
|
+
"directory": "packages/bookkeeping"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"files": ["dist"],
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"type-check": "tsc --noEmit",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"test:coverage": "bun test --coverage"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@moatless/api-client": "workspace:*",
|
|
24
|
+
"@moatless/fortnox-client": "workspace:*",
|
|
25
|
+
"@moatless/bokio-client": "workspace:*",
|
|
26
|
+
"cli-progress": "^3.12.0",
|
|
27
|
+
"open": "^10.1.0",
|
|
28
|
+
"simple-git": "^3.22.0",
|
|
29
|
+
"yaml": "^2.7.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/cli-progress": "^3.11.6",
|
|
33
|
+
"@types/node": "^25.0.3",
|
|
34
|
+
"bun-types": "latest",
|
|
35
|
+
"typescript": "^5.8.3"
|
|
36
|
+
}
|
|
37
|
+
}
|