@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,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error information from a failed deletion attempt
|
|
3
|
+
*/
|
|
4
|
+
export interface DeletionError {
|
|
5
|
+
/** When the deletion was attempted */
|
|
6
|
+
attemptedAt: string;
|
|
7
|
+
/** HTTP status code from the API */
|
|
8
|
+
statusCode?: number;
|
|
9
|
+
/** Error code from the integration (e.g., Fortnox error code) */
|
|
10
|
+
errorCode?: number | string;
|
|
11
|
+
/** Human-readable error message */
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Represents a discarded inbox item that should be removed from the source integration.
|
|
16
|
+
* Stored in inbox/discarded.json in the organization's storage.
|
|
17
|
+
*/
|
|
18
|
+
export interface DiscardedItem {
|
|
19
|
+
/** Directory name in the inbox folder */
|
|
20
|
+
dir: string;
|
|
21
|
+
/** ID of the item in the source integration (e.g., Fortnox file ID) */
|
|
22
|
+
sourceId: string;
|
|
23
|
+
/** Which integration the item came from */
|
|
24
|
+
sourceIntegration: "fortnox" | "bokio";
|
|
25
|
+
/** Error from the last failed deletion attempt (if any) */
|
|
26
|
+
lastDeletionError?: DeletionError;
|
|
27
|
+
/** Number of failed deletion attempts */
|
|
28
|
+
deletionAttempts?: number;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported document format - optimized for Git-based workflow
|
|
3
|
+
* Using camelCase for TypeScript compatibility
|
|
4
|
+
*
|
|
5
|
+
* This format is designed for human-readable YAML exports stored in Git repositories.
|
|
6
|
+
* It removes database-specific metadata (UUIDs, timestamps, sync data) and focuses
|
|
7
|
+
* on the document content and parsed data. Git provides version history and timestamps.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Monetary amount with currency
|
|
11
|
+
* Amount is stored as a string to preserve precision
|
|
12
|
+
*/
|
|
13
|
+
export interface DocumentMoneyAmount {
|
|
14
|
+
amount: string;
|
|
15
|
+
currency: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Tax detail line item
|
|
19
|
+
*/
|
|
20
|
+
export interface TaxDetail {
|
|
21
|
+
rate: number;
|
|
22
|
+
amount: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Document kind/type
|
|
27
|
+
*/
|
|
28
|
+
export type DocumentKind = "RECEIPT" | "SUPPLIER_INVOICE" | "CUSTOMER_INVOICE" | "TRAKTAMENTE" | "OTHER";
|
|
29
|
+
/**
|
|
30
|
+
* Document status in workflow
|
|
31
|
+
*/
|
|
32
|
+
export type DocumentStatus = "DRAFT" | "REVIEWED" | "READY_TO_EXPORT" | "EXPORTED" | "ERROR" | "DISCARDED" | "POSTED";
|
|
33
|
+
/**
|
|
34
|
+
* Complete exported document with parsed data
|
|
35
|
+
*
|
|
36
|
+
* Git-based workflow assumptions:
|
|
37
|
+
* - Creation/update timestamps: Use git log
|
|
38
|
+
* - Author information: Use git commit author
|
|
39
|
+
* - File location: Same directory as YAML file
|
|
40
|
+
* - Organization context: Repository structure
|
|
41
|
+
*/
|
|
42
|
+
export interface Document {
|
|
43
|
+
kind: DocumentKind;
|
|
44
|
+
status: DocumentStatus;
|
|
45
|
+
fileName: string;
|
|
46
|
+
mimeType: string;
|
|
47
|
+
documentDate?: string;
|
|
48
|
+
documentNumber?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
counterparty?: string;
|
|
51
|
+
totalAmount?: DocumentMoneyAmount;
|
|
52
|
+
totalTaxAmount?: DocumentMoneyAmount;
|
|
53
|
+
subTotal?: DocumentMoneyAmount;
|
|
54
|
+
taxDetails?: TaxDetail[];
|
|
55
|
+
dueDate?: string;
|
|
56
|
+
currencyRate?: number;
|
|
57
|
+
vendorTaxId?: string;
|
|
58
|
+
interpretationError?: string;
|
|
59
|
+
sourceIntegration?: "fortnox" | "bokio";
|
|
60
|
+
sourceId?: string;
|
|
61
|
+
uploadedAt?: string;
|
|
62
|
+
interpretedAt?: string;
|
|
63
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported document format - optimized for Git-based workflow
|
|
3
|
+
* Using camelCase for TypeScript compatibility
|
|
4
|
+
*
|
|
5
|
+
* This format is designed for human-readable YAML exports stored in Git repositories.
|
|
6
|
+
* It removes database-specific metadata (UUIDs, timestamps, sync data) and focuses
|
|
7
|
+
* on the document content and parsed data. Git provides version history and timestamps.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported document format - optimized for Git-based workflow
|
|
3
|
+
* Using camelCase for TypeScript compatibility
|
|
4
|
+
*
|
|
5
|
+
* This format is designed for human-readable YAML exports stored in Git repositories.
|
|
6
|
+
* It removes database-specific metadata (UUIDs, timestamps, sync data) and focuses
|
|
7
|
+
* on the document content and parsed data. Git provides version history and timestamps.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Monetary amount with currency
|
|
11
|
+
* Amount is stored as a string to preserve precision
|
|
12
|
+
*/
|
|
13
|
+
export interface ExportedMoneyAmount {
|
|
14
|
+
amount: string;
|
|
15
|
+
currency: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Tax detail line item
|
|
19
|
+
*/
|
|
20
|
+
export interface ExportedTaxDetail {
|
|
21
|
+
rate: number;
|
|
22
|
+
amount: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Document kind/type
|
|
27
|
+
*/
|
|
28
|
+
export type DocumentKind = "RECEIPT" | "SUPPLIER_INVOICE" | "CUSTOMER_INVOICE" | "TRAKTAMENTE" | "OTHER";
|
|
29
|
+
/**
|
|
30
|
+
* Document status in workflow
|
|
31
|
+
*/
|
|
32
|
+
export type DocumentStatus = "DRAFT" | "REVIEWED" | "READY_TO_EXPORT" | "EXPORTED" | "ERROR" | "DISCARDED" | "POSTED";
|
|
33
|
+
/**
|
|
34
|
+
* Complete exported document with parsed data
|
|
35
|
+
*
|
|
36
|
+
* Git-based workflow assumptions:
|
|
37
|
+
* - Creation/update timestamps: Use git log
|
|
38
|
+
* - Author information: Use git commit author
|
|
39
|
+
* - File location: Same directory as YAML file
|
|
40
|
+
* - Organization context: Repository structure
|
|
41
|
+
*/
|
|
42
|
+
export interface ExportedDocument {
|
|
43
|
+
kind: DocumentKind;
|
|
44
|
+
status: DocumentStatus;
|
|
45
|
+
fileName: string;
|
|
46
|
+
mimeType: string;
|
|
47
|
+
documentDate?: string;
|
|
48
|
+
documentNumber?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
counterparty?: string;
|
|
51
|
+
totalAmount?: ExportedMoneyAmount;
|
|
52
|
+
totalTaxAmount?: ExportedMoneyAmount;
|
|
53
|
+
subTotal?: ExportedMoneyAmount;
|
|
54
|
+
taxDetails?: ExportedTaxDetail[];
|
|
55
|
+
dueDate?: string;
|
|
56
|
+
currencyRate?: number;
|
|
57
|
+
vendorTaxId?: string;
|
|
58
|
+
interpretationError?: string;
|
|
59
|
+
sourceIntegration?: "fortnox" | "bokio";
|
|
60
|
+
sourceId?: string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported document format - optimized for Git-based workflow
|
|
3
|
+
* Using camelCase for TypeScript compatibility
|
|
4
|
+
*
|
|
5
|
+
* This format is designed for human-readable YAML exports stored in Git repositories.
|
|
6
|
+
* It removes database-specific metadata (UUIDs, timestamps, sync data) and focuses
|
|
7
|
+
* on the document content and parsed data. Git provides version history and timestamps.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported inbox document format - extends ExportedDocument for Git workflow
|
|
3
|
+
*
|
|
4
|
+
* Inbox documents are documents that haven't been fully processed yet:
|
|
5
|
+
* - Not yet linked to a journal entry (journal_entry_id IS NULL), OR
|
|
6
|
+
* - Linked to a DRAFT journal entry (not yet posted)
|
|
7
|
+
*
|
|
8
|
+
* This format includes source tracking and optional suggested journal entries.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExportedDocument } from "./exported-document";
|
|
11
|
+
export interface ExportedInboxDocument extends ExportedDocument {
|
|
12
|
+
uploadedAt?: string;
|
|
13
|
+
interpretedAt?: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported inbox document format - extends ExportedDocument for Git workflow
|
|
3
|
+
*
|
|
4
|
+
* Inbox documents are documents that haven't been fully processed yet:
|
|
5
|
+
* - Not yet linked to a journal entry (journal_entry_id IS NULL), OR
|
|
6
|
+
* - Linked to a DRAFT journal entry (not yet posted)
|
|
7
|
+
*
|
|
8
|
+
* This format includes source tracking and optional suggested journal entries.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ApiResponse = {
|
|
2
|
+
message: string;
|
|
3
|
+
success: true;
|
|
4
|
+
};
|
|
5
|
+
export * from "./journal-entry";
|
|
6
|
+
export * from "./document";
|
|
7
|
+
export type { Document as InboxDocument } from "./document";
|
|
8
|
+
export * from "./fiscal-year";
|
|
9
|
+
export * from "./ledger-account";
|
|
10
|
+
export * from "./discarded-item";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Journal entry types (used across Git YAML, API, and client)
|
|
2
|
+
export * from "./journal-entry";
|
|
3
|
+
// Document types
|
|
4
|
+
export * from "./document";
|
|
5
|
+
// Fiscal year
|
|
6
|
+
export * from "./fiscal-year";
|
|
7
|
+
// Ledger account
|
|
8
|
+
export * from "./ledger-account";
|
|
9
|
+
// Discarded items
|
|
10
|
+
export * from "./discarded-item";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Journal Entry types
|
|
3
|
+
*
|
|
4
|
+
* These types are used throughout the stack:
|
|
5
|
+
* - Git YAML storage (entry.yaml files)
|
|
6
|
+
* - Server API responses
|
|
7
|
+
* - Client state
|
|
8
|
+
*
|
|
9
|
+
* All monetary amounts use NUMBER type (not strings).
|
|
10
|
+
* All field names use camelCase.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Monetary amount with currency
|
|
14
|
+
*/
|
|
15
|
+
export interface MoneyAmount {
|
|
16
|
+
amount: number;
|
|
17
|
+
currency: string;
|
|
18
|
+
originalAmount?: number;
|
|
19
|
+
originalCurrency?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Single line in a journal entry
|
|
23
|
+
*/
|
|
24
|
+
export interface JournalLine {
|
|
25
|
+
lineNumber: number;
|
|
26
|
+
account: string;
|
|
27
|
+
debit: MoneyAmount;
|
|
28
|
+
credit: MoneyAmount;
|
|
29
|
+
memo?: string;
|
|
30
|
+
taxCode?: string;
|
|
31
|
+
costCenter?: string;
|
|
32
|
+
/** Populated by API when account details are available */
|
|
33
|
+
ledgerAccountName?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Journal entry series
|
|
37
|
+
* Different series are used for different transaction types:
|
|
38
|
+
* - A: General/miscellaneous transactions
|
|
39
|
+
* - B: Customer invoices (sales)
|
|
40
|
+
* - C: Customer payments (collections)
|
|
41
|
+
* - D: Supplier invoices (purchases)
|
|
42
|
+
* - E: Supplier payments (disbursements)
|
|
43
|
+
* - I: Period-end accruals and adjustments
|
|
44
|
+
* - M: Monthly/quarterly tax reporting
|
|
45
|
+
*/
|
|
46
|
+
export type JournalEntrySeries = "A" | "B" | "C" | "D" | "E" | "I" | "M";
|
|
47
|
+
/**
|
|
48
|
+
* Journal entry status
|
|
49
|
+
*/
|
|
50
|
+
export type JournalEntryStatus = "DRAFT" | "POSTED";
|
|
51
|
+
/**
|
|
52
|
+
* Complete journal entry
|
|
53
|
+
*/
|
|
54
|
+
export interface JournalEntry {
|
|
55
|
+
/** Populated by API (derived from directory name), not stored in YAML */
|
|
56
|
+
id?: string;
|
|
57
|
+
series?: JournalEntrySeries;
|
|
58
|
+
entryNumber: number;
|
|
59
|
+
entryDate: string;
|
|
60
|
+
description: string;
|
|
61
|
+
status: JournalEntryStatus;
|
|
62
|
+
currency: string;
|
|
63
|
+
externalId?: string;
|
|
64
|
+
totalDebit: MoneyAmount;
|
|
65
|
+
totalCredit: MoneyAmount;
|
|
66
|
+
lines: JournalLine[];
|
|
67
|
+
postedAt?: string;
|
|
68
|
+
errorMessage?: string;
|
|
69
|
+
postingCheckpoint?: string;
|
|
70
|
+
voucherNumber?: number;
|
|
71
|
+
voucherSeriesCode?: string;
|
|
72
|
+
fortnoxFileId?: string;
|
|
73
|
+
sourceIntegration?: "fortnox" | "bokio";
|
|
74
|
+
sourceSyncedAt?: string;
|
|
75
|
+
/** If this entry is a reversal, the external ID of the entry it reverses */
|
|
76
|
+
reversingEntryExternalId?: string;
|
|
77
|
+
/** If this entry was reversed/cancelled, the external ID of the reversing entry */
|
|
78
|
+
reversedByEntryExternalId?: string;
|
|
79
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Journal Entry types
|
|
3
|
+
*
|
|
4
|
+
* These types are used throughout the stack:
|
|
5
|
+
* - Git YAML storage (entry.yaml files)
|
|
6
|
+
* - Server API responses
|
|
7
|
+
* - Client state
|
|
8
|
+
*
|
|
9
|
+
* All monetary amounts use NUMBER type (not strings).
|
|
10
|
+
* All field names use camelCase.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File naming utilities for journal entries and fiscal years
|
|
3
|
+
*/
|
|
4
|
+
interface JournalEntryInput {
|
|
5
|
+
voucher_series_code: string | null;
|
|
6
|
+
voucher_number: number | null;
|
|
7
|
+
entry_date: string;
|
|
8
|
+
description: string | null;
|
|
9
|
+
id?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Generate directory name for a journal entry
|
|
13
|
+
* Format: {series}-{voucher-number}-{entry-date}-{description-truncated}
|
|
14
|
+
* Examples:
|
|
15
|
+
* - A-042-2024-01-15-invoice-from-acme-corp
|
|
16
|
+
* - B-001-2024-12-31-year-end-adjustment
|
|
17
|
+
* - 042-2024-01-15-purchase-equipment (no series)
|
|
18
|
+
*/
|
|
19
|
+
export declare function journalEntryDirName(entry: JournalEntryInput): string;
|
|
20
|
+
/**
|
|
21
|
+
* Generate file path for a journal entry
|
|
22
|
+
* Format: journal-entries/FY-{year}/{entry-dir}/entry.yaml
|
|
23
|
+
*/
|
|
24
|
+
export declare function journalEntryPath(fiscalYear: number, series: string | null, entryNumber: number, entryDate: string, description: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Get directory path from entry file path
|
|
27
|
+
* e.g., "journal-entries/FY-2024/A-042-2024-01-15-description/entry.yaml"
|
|
28
|
+
* -> "journal-entries/FY-2024/A-042-2024-01-15-description"
|
|
29
|
+
*/
|
|
30
|
+
export declare function journalEntryDirFromPath(entryPath: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Generate directory name for a fiscal year
|
|
33
|
+
* Format: FY-{year}
|
|
34
|
+
* Examples:
|
|
35
|
+
* - FY-2024
|
|
36
|
+
* - FY-2025
|
|
37
|
+
*/
|
|
38
|
+
export declare function fiscalYearDirName(year: number | {
|
|
39
|
+
start_date: string;
|
|
40
|
+
}): string;
|
|
41
|
+
/**
|
|
42
|
+
* Sanitize organization name for use as directory name
|
|
43
|
+
* Examples:
|
|
44
|
+
* - "Acme Corp AB" → "acme-corp-ab"
|
|
45
|
+
* - "Widgets & Gadgets Inc." → "widgets-gadgets-inc"
|
|
46
|
+
*/
|
|
47
|
+
export declare function sanitizeOrgName(name: string): string;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File naming utilities for journal entries and fiscal years
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generate directory name for a journal entry
|
|
6
|
+
* Format: {series}-{voucher-number}-{entry-date}-{description-truncated}
|
|
7
|
+
* Examples:
|
|
8
|
+
* - A-042-2024-01-15-invoice-from-acme-corp
|
|
9
|
+
* - B-001-2024-12-31-year-end-adjustment
|
|
10
|
+
* - 042-2024-01-15-purchase-equipment (no series)
|
|
11
|
+
*/
|
|
12
|
+
export function journalEntryDirName(entry) {
|
|
13
|
+
const series = entry.voucher_series_code || "";
|
|
14
|
+
const voucher = String(entry.voucher_number || "000").padStart(3, "0");
|
|
15
|
+
const date = entry.entry_date;
|
|
16
|
+
const description = slugify(entry.description || entry.id?.slice(0, 8) || "entry");
|
|
17
|
+
// Build parts array and filter out empty strings
|
|
18
|
+
const parts = [series, voucher, date, description].filter(Boolean);
|
|
19
|
+
return parts.join("-");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate file path for a journal entry
|
|
23
|
+
* Format: journal-entries/FY-{year}/{entry-dir}/entry.yaml
|
|
24
|
+
*/
|
|
25
|
+
export function journalEntryPath(fiscalYear, series, entryNumber, entryDate, description) {
|
|
26
|
+
const fyDir = fiscalYearDirName(fiscalYear);
|
|
27
|
+
const entryDir = journalEntryDirName({
|
|
28
|
+
voucher_series_code: series,
|
|
29
|
+
voucher_number: entryNumber,
|
|
30
|
+
entry_date: entryDate,
|
|
31
|
+
description,
|
|
32
|
+
});
|
|
33
|
+
return `journal-entries/${fyDir}/${entryDir}/entry.yaml`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get directory path from entry file path
|
|
37
|
+
* e.g., "journal-entries/FY-2024/A-042-2024-01-15-description/entry.yaml"
|
|
38
|
+
* -> "journal-entries/FY-2024/A-042-2024-01-15-description"
|
|
39
|
+
*/
|
|
40
|
+
export function journalEntryDirFromPath(entryPath) {
|
|
41
|
+
return entryPath.replace(/\/entry\.yaml$/, "");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generate directory name for a fiscal year
|
|
45
|
+
* Format: FY-{year}
|
|
46
|
+
* Examples:
|
|
47
|
+
* - FY-2024
|
|
48
|
+
* - FY-2025
|
|
49
|
+
*/
|
|
50
|
+
export function fiscalYearDirName(year) {
|
|
51
|
+
if (typeof year === "number") {
|
|
52
|
+
return `FY-${year}`;
|
|
53
|
+
}
|
|
54
|
+
return `FY-${year.start_date.slice(0, 4)}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert text to a URL-friendly slug
|
|
58
|
+
* - Converts to lowercase
|
|
59
|
+
* - Removes special characters
|
|
60
|
+
* - Replaces spaces with hyphens
|
|
61
|
+
* - Truncates to maxLength
|
|
62
|
+
*/
|
|
63
|
+
function slugify(text, maxLength = 30) {
|
|
64
|
+
return text
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.slice(0, maxLength)
|
|
67
|
+
.replace(/[^a-z0-9\s-]/g, "") // Remove special chars
|
|
68
|
+
.replace(/\s+/g, "-") // Spaces to hyphens
|
|
69
|
+
.replace(/-+/g, "-") // Collapse multiple hyphens
|
|
70
|
+
.replace(/^-|-$/g, ""); // Trim hyphens
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sanitize organization name for use as directory name
|
|
74
|
+
* Examples:
|
|
75
|
+
* - "Acme Corp AB" → "acme-corp-ab"
|
|
76
|
+
* - "Widgets & Gadgets Inc." → "widgets-gadgets-inc"
|
|
77
|
+
*/
|
|
78
|
+
export function sanitizeOrgName(name) {
|
|
79
|
+
return slugify(name, 50);
|
|
80
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize a new git repository with a .gitignore file
|
|
3
|
+
*/
|
|
4
|
+
export declare function initGitRepo(repoPath: string): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Stage all changes and commit with the given message.
|
|
7
|
+
* Returns true if a commit was made, false if there were no changes to commit.
|
|
8
|
+
*/
|
|
9
|
+
export declare function commitAll(repoPath: string, message: string): Promise<boolean>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import simpleGit from "simple-git";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const DEFAULT_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
|
+
/**
|
|
21
|
+
* Initialize a new git repository with a .gitignore file
|
|
22
|
+
*/
|
|
23
|
+
export async function initGitRepo(repoPath) {
|
|
24
|
+
const git = simpleGit(repoPath);
|
|
25
|
+
await git.init();
|
|
26
|
+
await fs.writeFile(path.join(repoPath, ".gitignore"), DEFAULT_GITIGNORE);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stage all changes and commit with the given message.
|
|
30
|
+
* Returns true if a commit was made, false if there were no changes to commit.
|
|
31
|
+
*/
|
|
32
|
+
export async function commitAll(repoPath, message) {
|
|
33
|
+
const git = simpleGit(repoPath);
|
|
34
|
+
await git.add(".");
|
|
35
|
+
const status = await git.status();
|
|
36
|
+
if (status.files.length > 0) {
|
|
37
|
+
await git.commit(message);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { journalEntryDirName, journalEntryPath, journalEntryDirFromPath, fiscalYearDirName, sanitizeOrgName, } from "./file-namer";
|
|
2
|
+
export { parseYaml, toYaml } from "./yaml";
|
|
3
|
+
export { withRetry, isRateLimitError, type RetryOptions } from "./retry";
|
|
4
|
+
export { initGitRepo, commitAll } from "./git";
|
|
5
|
+
export { getAgentsTemplate, renderAgentsTemplate, type RenderAgentsTemplateOptions, } from "./templates";
|
|
6
|
+
export { KVITTON_DIR, getKvittonDir, getTokensDir, getCacheDir, } from "./paths";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { journalEntryDirName, journalEntryPath, journalEntryDirFromPath, fiscalYearDirName, sanitizeOrgName, } from "./file-namer";
|
|
2
|
+
export { parseYaml, toYaml } from "./yaml";
|
|
3
|
+
export { withRetry, isRateLimitError } from "./retry";
|
|
4
|
+
export { initGitRepo, commitAll } from "./git";
|
|
5
|
+
export { getAgentsTemplate, renderAgentsTemplate, } from "./templates";
|
|
6
|
+
export { KVITTON_DIR, getKvittonDir, getTokensDir, getCacheDir, } from "./paths";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hidden directory used for internal storage (tokens, cache, etc.)
|
|
3
|
+
* inside the accounting repository.
|
|
4
|
+
*/
|
|
5
|
+
export declare const KVITTON_DIR = ".kvitton";
|
|
6
|
+
/**
|
|
7
|
+
* Get the base .kvitton directory path for a given working directory.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getKvittonDir(cwd: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Get the tokens directory path for storing OAuth tokens.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getTokensDir(cwd: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Get the cache directory path for storing cached data (e.g., currency rates).
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCacheDir(cwd: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* The hidden directory used for internal storage (tokens, cache, etc.)
|
|
4
|
+
* inside the accounting repository.
|
|
5
|
+
*/
|
|
6
|
+
export const KVITTON_DIR = ".kvitton";
|
|
7
|
+
/**
|
|
8
|
+
* Get the base .kvitton directory path for a given working directory.
|
|
9
|
+
*/
|
|
10
|
+
export function getKvittonDir(cwd) {
|
|
11
|
+
return path.join(cwd, KVITTON_DIR);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get the tokens directory path for storing OAuth tokens.
|
|
15
|
+
*/
|
|
16
|
+
export function getTokensDir(cwd) {
|
|
17
|
+
return path.join(cwd, KVITTON_DIR, "tokens");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get the cache directory path for storing cached data (e.g., currency rates).
|
|
21
|
+
*/
|
|
22
|
+
export function getCacheDir(cwd) {
|
|
23
|
+
return path.join(cwd, KVITTON_DIR, "cache");
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff
|
|
3
|
+
*/
|
|
4
|
+
export interface RetryOptions {
|
|
5
|
+
maxRetries?: number;
|
|
6
|
+
baseDelayMs?: number;
|
|
7
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
8
|
+
onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Default check for rate limit errors
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRateLimitError(error: unknown): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Execute a function with retry logic and exponential backoff
|
|
16
|
+
*/
|
|
17
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Default check for rate limit errors
|
|
6
|
+
*/
|
|
7
|
+
export function isRateLimitError(error) {
|
|
8
|
+
if (error && typeof error === "object") {
|
|
9
|
+
const err = error;
|
|
10
|
+
if (err.code === "RATE_LIMIT_EXCEEDED")
|
|
11
|
+
return true;
|
|
12
|
+
if (err.statusCode === 429)
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Execute a function with retry logic and exponential backoff
|
|
19
|
+
*/
|
|
20
|
+
export async function withRetry(fn, options = {}) {
|
|
21
|
+
const { maxRetries = 3, baseDelayMs = 2000, shouldRetry = isRateLimitError, onRetry, } = options;
|
|
22
|
+
let lastError;
|
|
23
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
return await fn();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
lastError = err;
|
|
29
|
+
// Check if we should retry
|
|
30
|
+
if (attempt < maxRetries && shouldRetry(err)) {
|
|
31
|
+
// Exponential backoff: 2s, 4s, 8s, ...
|
|
32
|
+
const delayMs = baseDelayMs * 2 ** attempt;
|
|
33
|
+
onRetry?.(attempt + 1, err, delayMs);
|
|
34
|
+
await sleep(delayMs);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Don't retry - rethrow
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw lastError;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sleep for a given number of milliseconds
|
|
45
|
+
*/
|
|
46
|
+
function sleep(ms) {
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
}
|