e-arveldaja-mcp 0.3.2 → 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.
- package/dist/account-validation.d.ts +6 -0
- package/dist/account-validation.js +22 -0
- package/dist/api/purchase-invoices.api.d.ts +2 -2
- package/dist/api/purchase-invoices.api.js +5 -3
- package/dist/api/transactions.api.js +2 -2
- package/dist/cache.js +3 -1
- package/dist/config.js +1 -13
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +34 -0
- package/dist/file-validation.js +1 -17
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +77 -43
- package/dist/index.js +5 -3
- package/dist/money.js +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +15 -0
- package/dist/tools/bank-reconciliation.js +4 -2
- package/dist/tools/crud-tools.d.ts +3 -0
- package/dist/tools/crud-tools.js +14 -11
- package/dist/tools/estonian-tax.js +14 -18
- package/dist/tools/financial-statements.js +8 -8
- package/dist/tools/lightyear-investments.js +15 -71
- package/dist/tools/pdf-workflow.js +2 -1
- package/dist/tools/purchase-vat-defaults.d.ts +1 -0
- package/dist/tools/purchase-vat-defaults.js +1 -1
- package/dist/tools/wise-import.js +1 -26
- package/dist/types/api.d.ts +3 -0
- package/package.json +7 -3
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function validateAccounts(accounts, targets) {
|
|
2
|
+
const accountMap = new Map(accounts.map(account => [account.id, account]));
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const errors = [];
|
|
5
|
+
for (const target of targets) {
|
|
6
|
+
const key = `${target.id}:${target.label}`;
|
|
7
|
+
if (seen.has(key))
|
|
8
|
+
continue;
|
|
9
|
+
seen.add(key);
|
|
10
|
+
const account = accountMap.get(target.id);
|
|
11
|
+
if (!account) {
|
|
12
|
+
errors.push(`${target.label} ${target.id} not found in chart of accounts. ` +
|
|
13
|
+
`Activate it in e-arveldaja: Seaded → Kontoplaan → find account ${target.id} and enable it.`);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (!account.is_valid) {
|
|
17
|
+
errors.push(`${target.label} ${target.id} (${account.name_est}) is inactive. ` +
|
|
18
|
+
`Activate it in e-arveldaja: Seaded → Kontoplaan → ${account.name_est} → mark as active.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return errors;
|
|
22
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from "../http-client.js";
|
|
2
|
-
import type { PurchaseInvoice, ApiResponse, ApiFile } from "../types/api.js";
|
|
2
|
+
import type { PurchaseInvoice, CreatePurchaseInvoiceData, ApiResponse, ApiFile } from "../types/api.js";
|
|
3
3
|
import { BaseResource } from "./base-resource.js";
|
|
4
4
|
export declare class PurchaseInvoicesApi extends BaseResource<PurchaseInvoice> {
|
|
5
5
|
constructor(client: HttpClient);
|
|
@@ -13,7 +13,7 @@ export declare class PurchaseInvoicesApi extends BaseResource<PurchaseInvoice> {
|
|
|
13
13
|
* because input VAT is not deductible. gross_price is still set to actual payable amount.
|
|
14
14
|
* Items get project_no_vat_gross_price set for VAT tracking.
|
|
15
15
|
*/
|
|
16
|
-
createAndSetTotals(data:
|
|
16
|
+
createAndSetTotals(data: CreatePurchaseInvoiceData, vatPrice?: number, grossPrice?: number, isVatRegistered?: boolean): Promise<PurchaseInvoice>;
|
|
17
17
|
/**
|
|
18
18
|
* Confirm a purchase invoice. Automatically fixes vat_price/gross_price if missing or inconsistent.
|
|
19
19
|
* For non-VAT companies: only fixes gross_price, leaves vat_price at 0.
|
|
@@ -66,7 +66,7 @@ export class PurchaseInvoicesApi extends BaseResource {
|
|
|
66
66
|
const gross = grossPrice !== undefined
|
|
67
67
|
? grossPrice
|
|
68
68
|
: roundMoney(itemNet + itemVat);
|
|
69
|
-
if (vat
|
|
69
|
+
if (vat !== undefined || gross !== undefined) {
|
|
70
70
|
await this.update(id, { vat_price: vat, gross_price: gross, items: invoice.items });
|
|
71
71
|
this.invalidateCache();
|
|
72
72
|
}
|
|
@@ -85,8 +85,10 @@ export class PurchaseInvoicesApi extends BaseResource {
|
|
|
85
85
|
const vat = isVatRegistered ? itemVat : 0;
|
|
86
86
|
const gross = roundMoney(net + itemVat);
|
|
87
87
|
const currentGross = invoice.gross_price;
|
|
88
|
-
const
|
|
89
|
-
|
|
88
|
+
const currentVat = invoice.vat_price;
|
|
89
|
+
const grossNeedsRepair = !currentGross || Math.abs(currentGross - gross) > 0.02;
|
|
90
|
+
const vatNeedsRepair = isVatRegistered && (currentVat === undefined || currentVat === null || Math.abs(currentVat - vat) > 0.02);
|
|
91
|
+
if (grossNeedsRepair || vatNeedsRepair) {
|
|
90
92
|
await this.update(id, { vat_price: vat, gross_price: gross, items });
|
|
91
93
|
}
|
|
92
94
|
}
|
|
@@ -26,10 +26,10 @@ export class TransactionsApi extends BaseResource {
|
|
|
26
26
|
const inv = await this.client.get(`/sale_invoices/${dist.related_id}`);
|
|
27
27
|
clientsId = inv?.clients_id;
|
|
28
28
|
}
|
|
29
|
-
if (clientsId)
|
|
29
|
+
if (clientsId !== undefined)
|
|
30
30
|
break;
|
|
31
31
|
}
|
|
32
|
-
if (clientsId) {
|
|
32
|
+
if (clientsId !== undefined) {
|
|
33
33
|
await this.update(id, { clients_id: clientsId });
|
|
34
34
|
}
|
|
35
35
|
}
|
package/dist/cache.js
CHANGED
|
@@ -20,13 +20,15 @@ export class Cache {
|
|
|
20
20
|
return entry.data;
|
|
21
21
|
}
|
|
22
22
|
set(key, data, ttlSeconds) {
|
|
23
|
+
if (ttlSeconds === 0)
|
|
24
|
+
return; // Zero TTL = don't cache
|
|
23
25
|
// Evict oldest entries if at capacity
|
|
24
26
|
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
25
27
|
const firstKey = this.store.keys().next().value;
|
|
26
28
|
if (firstKey)
|
|
27
29
|
this.store.delete(firstKey);
|
|
28
30
|
}
|
|
29
|
-
const ttlMs = ttlSeconds ? ttlSeconds * 1000 : this.defaultTtlMs;
|
|
31
|
+
const ttlMs = ttlSeconds !== undefined ? ttlSeconds * 1000 : this.defaultTtlMs;
|
|
30
32
|
this.store.set(key, { data, expiresAt: Date.now() + ttlMs });
|
|
31
33
|
}
|
|
32
34
|
invalidate(pattern) {
|
package/dist/config.js
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import dotenv from "dotenv";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { readFileSync, existsSync, statSync, readdirSync, realpathSync } from "fs";
|
|
4
|
+
import { getProjectRoot } from "./paths.js";
|
|
4
5
|
const SERVERS = {
|
|
5
6
|
live: "https://rmp-api.rik.ee/v1",
|
|
6
7
|
demo: "https://demo-rmp-api.rik.ee/v1",
|
|
7
8
|
};
|
|
8
|
-
/** Find project root by walking up from import.meta.dirname to package.json. */
|
|
9
|
-
function getProjectRoot() {
|
|
10
|
-
let dir = import.meta.dirname;
|
|
11
|
-
for (let i = 0; i < 5; i++) {
|
|
12
|
-
if (existsSync(resolve(dir, "package.json")))
|
|
13
|
-
return dir;
|
|
14
|
-
const parent = resolve(dir, "..");
|
|
15
|
-
if (parent === dir)
|
|
16
|
-
break;
|
|
17
|
-
dir = parent;
|
|
18
|
-
}
|
|
19
|
-
return process.cwd();
|
|
20
|
-
}
|
|
21
9
|
const PROJECT_ROOT = getProjectRoot();
|
|
22
10
|
dotenv.config({ path: resolve(PROJECT_ROOT, ".env") });
|
|
23
11
|
function getBaseUrl() {
|
package/dist/csv.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseCSVLine(line: string): string[];
|
package/dist/csv.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function parseCSVLine(line) {
|
|
2
|
+
const fields = [];
|
|
3
|
+
let current = "";
|
|
4
|
+
let inQuotes = false;
|
|
5
|
+
for (let i = 0; i < line.length; i++) {
|
|
6
|
+
const ch = line[i];
|
|
7
|
+
if (inQuotes) {
|
|
8
|
+
if (ch === '"') {
|
|
9
|
+
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
10
|
+
current += '"';
|
|
11
|
+
i++;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
inQuotes = false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
current += ch;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (ch === '"') {
|
|
22
|
+
inQuotes = true;
|
|
23
|
+
}
|
|
24
|
+
else if (ch === ",") {
|
|
25
|
+
fields.push(current);
|
|
26
|
+
current = "";
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
current += ch;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
fields.push(current);
|
|
33
|
+
return fields;
|
|
34
|
+
}
|
package/dist/file-validation.js
CHANGED
|
@@ -2,6 +2,7 @@ import { stat, realpath } from "fs/promises";
|
|
|
2
2
|
import { resolve, extname, isAbsolute } from "path";
|
|
3
3
|
import { existsSync, realpathSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
+
import { getProjectRoot } from "./paths.js";
|
|
5
6
|
/**
|
|
6
7
|
* Allowed root directories for file reads. Configurable via EARVELDAJA_ALLOWED_PATHS
|
|
7
8
|
* (colon-separated list). Defaults to $HOME and /tmp.
|
|
@@ -21,23 +22,6 @@ function getAllowedRoots() {
|
|
|
21
22
|
}
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Get the project root (directory containing package.json).
|
|
26
|
-
* Falls back to process.cwd().
|
|
27
|
-
*/
|
|
28
|
-
function getProjectRoot() {
|
|
29
|
-
// When running from dist/ or src/, go up until we find package.json
|
|
30
|
-
let dir = import.meta.dirname;
|
|
31
|
-
for (let i = 0; i < 5; i++) {
|
|
32
|
-
if (existsSync(resolve(dir, "package.json")))
|
|
33
|
-
return dir;
|
|
34
|
-
const parent = resolve(dir, "..");
|
|
35
|
-
if (parent === dir)
|
|
36
|
-
break;
|
|
37
|
-
dir = parent;
|
|
38
|
-
}
|
|
39
|
-
return process.cwd();
|
|
40
|
-
}
|
|
41
25
|
/**
|
|
42
26
|
* Resolve a file path. For relative paths, tries:
|
|
43
27
|
* 1. Parent of project root (where accounting documents typically live)
|
package/dist/http-client.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare class HttpClient {
|
|
|
13
13
|
private readonly minIntervalMs;
|
|
14
14
|
constructor(config: Config, cacheNamespace?: string);
|
|
15
15
|
private waitForRateLimitTurn;
|
|
16
|
+
private static sleep;
|
|
17
|
+
private static shouldRetryStatus;
|
|
18
|
+
private static isRetryableError;
|
|
19
|
+
private static formatNetworkError;
|
|
16
20
|
request<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
17
21
|
private static getPublicIp;
|
|
18
22
|
get<T = unknown>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
package/dist/http-client.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createAuthHeaders } from "./auth.js";
|
|
2
|
+
const MAX_RETRIES = 3;
|
|
3
|
+
const INITIAL_RETRY_DELAY_MS = 1_000;
|
|
2
4
|
export class HttpClient {
|
|
3
5
|
config;
|
|
4
6
|
cacheNamespace;
|
|
@@ -22,8 +24,22 @@ export class HttpClient {
|
|
|
22
24
|
this.lastRequest = waitTurn;
|
|
23
25
|
await waitTurn;
|
|
24
26
|
}
|
|
27
|
+
static async sleep(ms) {
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
29
|
+
}
|
|
30
|
+
static shouldRetryStatus(status) {
|
|
31
|
+
return status === 429 || status >= 500;
|
|
32
|
+
}
|
|
33
|
+
static isRetryableError(error) {
|
|
34
|
+
return error instanceof Error && (error.name === "AbortError" ||
|
|
35
|
+
error.name === "TypeError" ||
|
|
36
|
+
/fetch failed|network/i.test(error.message));
|
|
37
|
+
}
|
|
38
|
+
static formatNetworkError(method, path, error) {
|
|
39
|
+
const suffix = error instanceof Error && error.message ? `: ${error.message}` : "";
|
|
40
|
+
return new Error(`API request failed: ${method} ${path} → network error${suffix}`);
|
|
41
|
+
}
|
|
25
42
|
async request(path, options = {}) {
|
|
26
|
-
await this.waitForRateLimitTurn();
|
|
27
43
|
const { method = "GET", body, params } = options;
|
|
28
44
|
// Build full URL: baseUrl already includes /v1
|
|
29
45
|
const fullUrl = `${this.config.baseUrl}${path}`;
|
|
@@ -45,56 +61,74 @@ export class HttpClient {
|
|
|
45
61
|
if (body !== undefined) {
|
|
46
62
|
headers["Content-Type"] = "application/json";
|
|
47
63
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
55
|
-
signal: controller.signal,
|
|
56
|
-
});
|
|
57
|
-
if (!response.ok) {
|
|
58
|
-
// Parse structured error if available, but don't expose raw upstream details
|
|
59
|
-
let errorMessage = `API request failed: ${method} ${path} → ${response.status}`;
|
|
64
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
65
|
+
await this.waitForRateLimitTurn();
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeoutId = setTimeout(() => controller.abort(), 60_000);
|
|
68
|
+
try {
|
|
69
|
+
let response;
|
|
60
70
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
response = await fetch(url.toString(), {
|
|
72
|
+
method,
|
|
73
|
+
headers,
|
|
74
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (HttpClient.isRetryableError(error) && attempt < MAX_RETRIES) {
|
|
80
|
+
await HttpClient.sleep(INITIAL_RETRY_DELAY_MS * (2 ** attempt));
|
|
81
|
+
continue;
|
|
64
82
|
}
|
|
83
|
+
throw HttpClient.formatNetworkError(method, path, error);
|
|
65
84
|
}
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
if (HttpClient.shouldRetryStatus(response.status) && attempt < MAX_RETRIES) {
|
|
87
|
+
await HttpClient.sleep(INITIAL_RETRY_DELAY_MS * (2 ** attempt));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Parse structured error if available, but don't expose raw upstream details
|
|
91
|
+
let errorMessage = `API request failed: ${method} ${path} → ${response.status}`;
|
|
92
|
+
try {
|
|
93
|
+
const body = await response.json();
|
|
94
|
+
if (body.messages && Array.isArray(body.messages)) {
|
|
95
|
+
errorMessage += `: ${body.messages.join("; ")}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Non-JSON error body — don't expose raw text
|
|
100
|
+
}
|
|
101
|
+
if (response.status === 401) {
|
|
102
|
+
const publicIp = await HttpClient.getPublicIp();
|
|
103
|
+
errorMessage += `\n\nTroubleshooting 401 Unauthorized:\n` +
|
|
104
|
+
` 1. Is the API key downloaded and configured? Check apikey*.txt or environment variables.\n` +
|
|
105
|
+
` 2. Is this machine's IP address allowed in e-arveldaja API settings?\n` +
|
|
106
|
+
` Your public IP: ${publicIp}\n` +
|
|
107
|
+
` 3. Add the IP above to: e-arveldaja → Seaded → API võtmed → Lubatud IP-aadressid\n` +
|
|
108
|
+
` Multiple IP addresses can be added, separated by ;`;
|
|
109
|
+
}
|
|
110
|
+
throw new Error(errorMessage);
|
|
68
111
|
}
|
|
69
|
-
if (response.status ===
|
|
70
|
-
|
|
71
|
-
errorMessage += `\n\nTroubleshooting 401 Unauthorized:\n` +
|
|
72
|
-
` 1. Is the API key downloaded and configured? Check apikey*.txt or environment variables.\n` +
|
|
73
|
-
` 2. Is this machine's IP address allowed in e-arveldaja API settings?\n` +
|
|
74
|
-
` Your public IP: ${publicIp}\n` +
|
|
75
|
-
` 3. Add the IP above to: e-arveldaja → Seaded → API võtmed → Lubatud IP-aadressid\n` +
|
|
76
|
-
` Multiple IP addresses can be added, separated by ;`;
|
|
112
|
+
if (response.status === 204) {
|
|
113
|
+
return undefined;
|
|
77
114
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
116
|
+
if (contentType.includes("application/json")) {
|
|
117
|
+
return response.json();
|
|
118
|
+
}
|
|
119
|
+
// Binary response (e.g. PDF document download) — return as ApiFile-compatible object
|
|
120
|
+
const arrayBuf = await response.arrayBuffer();
|
|
121
|
+
const base64 = Buffer.from(arrayBuf).toString("base64");
|
|
122
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
123
|
+
const nameMatch = disposition.match(/filename="?([^";\n]+)"?/);
|
|
124
|
+
const name = nameMatch?.[1] ?? "document";
|
|
125
|
+
return { name, contents: base64 };
|
|
82
126
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return response.json();
|
|
127
|
+
finally {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
86
129
|
}
|
|
87
|
-
// Binary response (e.g. PDF document download) — return as ApiFile-compatible object
|
|
88
|
-
const arrayBuf = await response.arrayBuffer();
|
|
89
|
-
const base64 = Buffer.from(arrayBuf).toString("base64");
|
|
90
|
-
const disposition = response.headers.get("content-disposition") ?? "";
|
|
91
|
-
const nameMatch = disposition.match(/filename="?([^";\n]+)"?/);
|
|
92
|
-
const name = nameMatch?.[1] ?? "document";
|
|
93
|
-
return { name, contents: base64 };
|
|
94
|
-
}
|
|
95
|
-
finally {
|
|
96
|
-
clearTimeout(timeoutId);
|
|
97
130
|
}
|
|
131
|
+
throw new Error(`API request failed: ${method} ${path} → retries exhausted`);
|
|
98
132
|
}
|
|
99
133
|
static async getPublicIp() {
|
|
100
134
|
try {
|
package/dist/index.js
CHANGED
|
@@ -93,7 +93,7 @@ async function main() {
|
|
|
93
93
|
const api = createScopedApiContext(connectionState, connectionContexts, invocationStorage);
|
|
94
94
|
const server = new McpServer({
|
|
95
95
|
name: "e-arveldaja",
|
|
96
|
-
version: "0.
|
|
96
|
+
version: "0.4.0",
|
|
97
97
|
description: "EXPERIMENTAL, UNOFFICIAL MCP server for the Estonian e-arveldaja (e-Financials) API. " +
|
|
98
98
|
"NOT affiliated with or endorsed by RIK. Use entirely at your own risk — " +
|
|
99
99
|
"this software interacts with live financial data and can create, modify, and delete accounting records. " +
|
|
@@ -167,9 +167,10 @@ Reporting:
|
|
|
167
167
|
}
|
|
168
168
|
const target = allConfigs[index];
|
|
169
169
|
const previousIndex = connectionState.activeIndex;
|
|
170
|
-
clearAllCaches(previousIndex);
|
|
171
|
-
connectionState.activeIndex = index;
|
|
172
170
|
connectionState.generation += 1;
|
|
171
|
+
connectionState.activeIndex = index;
|
|
172
|
+
clearAllCaches(previousIndex);
|
|
173
|
+
clearAllCaches(index);
|
|
173
174
|
const snapshot = captureSnapshot(connectionState);
|
|
174
175
|
return {
|
|
175
176
|
content: [{
|
|
@@ -201,6 +202,7 @@ Reporting:
|
|
|
201
202
|
});
|
|
202
203
|
}
|
|
203
204
|
catch (error) {
|
|
205
|
+
process.stderr.write(`Tool handler error: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
204
206
|
return toolError(error);
|
|
205
207
|
}
|
|
206
208
|
});
|
package/dist/money.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Round to 2 decimal places (cents). Use for all monetary arithmetic. */
|
|
2
|
-
export const roundMoney = (v) => Math.round(v * 100) / 100;
|
|
2
|
+
export const roundMoney = (v) => Math.sign(v) * Math.round((Math.abs(v) + Number.EPSILON) * 100) / 100;
|
package/dist/paths.d.ts
ADDED
package/dist/paths.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
/** Find project root by walking up from import.meta.dirname to package.json. */
|
|
4
|
+
export function getProjectRoot() {
|
|
5
|
+
let dir = import.meta.dirname;
|
|
6
|
+
for (let i = 0; i < 5; i++) {
|
|
7
|
+
if (existsSync(resolve(dir, "package.json")))
|
|
8
|
+
return dir;
|
|
9
|
+
const parent = resolve(dir, "..");
|
|
10
|
+
if (parent === dir)
|
|
11
|
+
break;
|
|
12
|
+
dir = parent;
|
|
13
|
+
}
|
|
14
|
+
return process.cwd();
|
|
15
|
+
}
|
|
@@ -189,9 +189,10 @@ export function registerBankReconciliationTools(server, api) {
|
|
|
189
189
|
}
|
|
190
190
|
// Only auto-confirm if exactly one high-confidence match
|
|
191
191
|
if (candidates.length === 1) {
|
|
192
|
-
consumedInvoiceKeys.add(`${candidates[0].type.replace("_invoice", "")}:${candidates[0].id}`);
|
|
193
192
|
const match = candidates[0];
|
|
193
|
+
const invoiceKey = `${match.type.replace("_invoice", "")}:${match.id}`;
|
|
194
194
|
if (dryRun) {
|
|
195
|
+
consumedInvoiceKeys.add(invoiceKey);
|
|
195
196
|
confirmed.push({
|
|
196
197
|
transaction_id: tx.id,
|
|
197
198
|
amount: tx.amount,
|
|
@@ -207,6 +208,7 @@ export function registerBankReconciliationTools(server, api) {
|
|
|
207
208
|
related_id: match.id,
|
|
208
209
|
amount: tx.amount,
|
|
209
210
|
}]);
|
|
211
|
+
consumedInvoiceKeys.add(invoiceKey);
|
|
210
212
|
confirmed.push({
|
|
211
213
|
transaction_id: tx.id,
|
|
212
214
|
amount: tx.amount,
|
|
@@ -215,7 +217,7 @@ export function registerBankReconciliationTools(server, api) {
|
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
219
|
catch (err) {
|
|
218
|
-
skipped.push({ transaction_id: tx.id, reason: err.message });
|
|
220
|
+
skipped.push({ transaction_id: tx.id, reason: err instanceof Error ? err.message : String(err) });
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
}
|
|
@@ -20,5 +20,8 @@ export interface ApiContext {
|
|
|
20
20
|
export declare function isCompanyVatRegistered(api: ApiContext): Promise<boolean>;
|
|
21
21
|
export declare const MAX_JSON_INPUT_SIZE: number;
|
|
22
22
|
export declare function safeJsonParse(input: string, label: string): unknown;
|
|
23
|
+
export declare function parseJsonObject(input: string, label: string): Record<string, unknown>;
|
|
24
|
+
export declare function parseJsonObjectArray(input: string, label: string): Record<string, unknown>[];
|
|
25
|
+
export declare function requireFields(items: Record<string, unknown>[], label: string, fields: string[]): void;
|
|
23
26
|
export declare function parsePurchaseInvoiceItems(input: string): PurchaseInvoiceItem[];
|
|
24
27
|
export declare function registerCrudTools(server: McpServer, api: ApiContext): void;
|
package/dist/tools/crud-tools.js
CHANGED
|
@@ -21,14 +21,14 @@ export function safeJsonParse(input, label) {
|
|
|
21
21
|
function isRecord(value) {
|
|
22
22
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
23
|
}
|
|
24
|
-
function parseJsonObject(input, label) {
|
|
24
|
+
export function parseJsonObject(input, label) {
|
|
25
25
|
const parsed = safeJsonParse(input, label);
|
|
26
26
|
if (!isRecord(parsed)) {
|
|
27
27
|
throw new Error(`"${label}" must be a JSON object`);
|
|
28
28
|
}
|
|
29
29
|
return parsed;
|
|
30
30
|
}
|
|
31
|
-
function parseJsonObjectArray(input, label) {
|
|
31
|
+
export function parseJsonObjectArray(input, label) {
|
|
32
32
|
const parsed = safeJsonParse(input, label);
|
|
33
33
|
if (!Array.isArray(parsed)) {
|
|
34
34
|
throw new Error(`"${label}" must be a JSON array`);
|
|
@@ -40,7 +40,7 @@ function parseJsonObjectArray(input, label) {
|
|
|
40
40
|
});
|
|
41
41
|
return parsed;
|
|
42
42
|
}
|
|
43
|
-
function requireFields(items, label, fields) {
|
|
43
|
+
export function requireFields(items, label, fields) {
|
|
44
44
|
items.forEach((item, index) => {
|
|
45
45
|
for (const field of fields) {
|
|
46
46
|
if (!(field in item) || item[field] === null || item[field] === undefined || item[field] === "") {
|
|
@@ -79,6 +79,8 @@ const pageParam = z.object({
|
|
|
79
79
|
modified_since: z.string().optional().describe("Return only objects modified since this timestamp (ISO 8601)"),
|
|
80
80
|
});
|
|
81
81
|
const idParam = z.object({ id: z.number().describe("Object ID") });
|
|
82
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
83
|
+
const isoDateString = (description) => z.string().regex(isoDateRegex, "Expected YYYY-MM-DD").describe(description);
|
|
82
84
|
export function registerCrudTools(server, api) {
|
|
83
85
|
// =====================
|
|
84
86
|
// CLIENTS
|
|
@@ -192,7 +194,7 @@ export function registerCrudTools(server, api) {
|
|
|
192
194
|
});
|
|
193
195
|
server.tool("create_journal", "Create a journal entry with postings", {
|
|
194
196
|
title: z.string().optional().describe("Journal entry title"),
|
|
195
|
-
effective_date:
|
|
197
|
+
effective_date: isoDateString("Entry date (YYYY-MM-DD)"),
|
|
196
198
|
clients_id: z.number().optional().describe("Related client ID"),
|
|
197
199
|
document_number: z.string().optional().describe("Document number"),
|
|
198
200
|
cl_currencies_id: z.string().optional().describe("Currency (default EUR)"),
|
|
@@ -240,7 +242,7 @@ export function registerCrudTools(server, api) {
|
|
|
240
242
|
type: z.string().describe("Transaction type: D (incoming) or C (outgoing)"),
|
|
241
243
|
amount: z.number().describe("Transaction amount"),
|
|
242
244
|
cl_currencies_id: z.string().optional().describe("Currency (default EUR)"),
|
|
243
|
-
date:
|
|
245
|
+
date: isoDateString("Transaction date (YYYY-MM-DD)"),
|
|
244
246
|
description: z.string().optional().describe("Description"),
|
|
245
247
|
clients_id: z.number().optional().describe("Related client ID"),
|
|
246
248
|
bank_account_name: z.string().optional().describe("Remitter/beneficiary name"),
|
|
@@ -283,8 +285,8 @@ export function registerCrudTools(server, api) {
|
|
|
283
285
|
clients_id: z.number().describe("Buyer client ID"),
|
|
284
286
|
cl_templates_id: z.number().describe("Invoice template ID"),
|
|
285
287
|
number_suffix: z.string().optional().describe("Invoice number suffix (omit or empty string for auto-assign from invoice series)"),
|
|
286
|
-
create_date:
|
|
287
|
-
journal_date:
|
|
288
|
+
create_date: isoDateString("Invoice date (YYYY-MM-DD)"),
|
|
289
|
+
journal_date: isoDateString("Turnover date (YYYY-MM-DD)"),
|
|
288
290
|
term_days: z.number().describe("Payment term in days"),
|
|
289
291
|
cl_currencies_id: z.string().optional().describe("Currency (default EUR)"),
|
|
290
292
|
cl_countries_id: z.string().optional().describe("Country (default EST)"),
|
|
@@ -353,8 +355,8 @@ export function registerCrudTools(server, api) {
|
|
|
353
355
|
clients_id: z.number().describe("Supplier client ID"),
|
|
354
356
|
client_name: z.string().describe("Supplier name"),
|
|
355
357
|
number: z.string().describe("Invoice number"),
|
|
356
|
-
create_date:
|
|
357
|
-
journal_date:
|
|
358
|
+
create_date: isoDateString("Invoice date (YYYY-MM-DD)"),
|
|
359
|
+
journal_date: isoDateString("Turnover date (YYYY-MM-DD)"),
|
|
358
360
|
term_days: z.number().describe("Payment term in days"),
|
|
359
361
|
vat_price: z.number().optional().describe("Total VAT amount from original invoice (EXACT, for payment matching)"),
|
|
360
362
|
gross_price: z.number().optional().describe("Total gross amount from original invoice (EXACT, for payment matching)"),
|
|
@@ -369,7 +371,7 @@ export function registerCrudTools(server, api) {
|
|
|
369
371
|
const purchaseArticles = await getPurchaseArticlesWithVat(api);
|
|
370
372
|
const rawItems = parsePurchaseInvoiceItems(params.items);
|
|
371
373
|
const items = rawItems.map(item => applyPurchaseVatDefaults(purchaseArticles, item, isVatReg));
|
|
372
|
-
const
|
|
374
|
+
const invoiceData = {
|
|
373
375
|
clients_id: params.clients_id,
|
|
374
376
|
client_name: params.client_name,
|
|
375
377
|
number: params.number,
|
|
@@ -382,7 +384,8 @@ export function registerCrudTools(server, api) {
|
|
|
382
384
|
bank_account_no: params.bank_account_no,
|
|
383
385
|
notes: params.notes,
|
|
384
386
|
items,
|
|
385
|
-
}
|
|
387
|
+
};
|
|
388
|
+
const result = await api.purchaseInvoices.createAndSetTotals(invoiceData, params.vat_price, params.gross_price, isVatReg);
|
|
386
389
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
387
390
|
});
|
|
388
391
|
server.tool("update_purchase_invoice", "Update a purchase invoice", {
|
|
@@ -3,22 +3,7 @@ import { isCompanyVatRegistered } from "./crud-tools.js";
|
|
|
3
3
|
import { computeAllBalances } from "./financial-statements.js";
|
|
4
4
|
import { roundMoney } from "../money.js";
|
|
5
5
|
import { create } from "../annotations.js";
|
|
6
|
-
|
|
7
|
-
const accounts = await api.readonly.getAccounts();
|
|
8
|
-
const accountMap = new Map(accounts.map(account => [account.id, account]));
|
|
9
|
-
const errors = [];
|
|
10
|
-
for (const id of new Set(accountIds)) {
|
|
11
|
-
const account = accountMap.get(id);
|
|
12
|
-
if (!account) {
|
|
13
|
-
errors.push(`Account ${id} not found in chart of accounts`);
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (!account.is_valid) {
|
|
17
|
-
errors.push(`Account ${id} (${account.name_est}) is inactive. Activate in e-arveldaja: Seaded → Kontoplaan → ${account.name_est} → mark as active.`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return errors;
|
|
21
|
-
}
|
|
6
|
+
import { validateAccounts } from "../account-validation.js";
|
|
22
7
|
async function computeRetainedEarningsBalance(api, accountId, asOfDate) {
|
|
23
8
|
const allJournals = await api.journals.listAllWithPostings();
|
|
24
9
|
let debit = 0;
|
|
@@ -63,7 +48,13 @@ export function registerEstonianTaxTools(server, api) {
|
|
|
63
48
|
const taxAccount = tax_payable_account ?? 2540;
|
|
64
49
|
const shareCapitalAccount = share_capital_account ?? 3000;
|
|
65
50
|
// Validate all accounts exist in chart of accounts
|
|
66
|
-
const
|
|
51
|
+
const accounts = await api.readonly.getAccounts();
|
|
52
|
+
const accountErrors = validateAccounts(accounts, [
|
|
53
|
+
{ id: retainedAccount, label: "Retained earnings account" },
|
|
54
|
+
{ id: payableAccount, label: "Dividend payable account" },
|
|
55
|
+
{ id: taxAccount, label: "Tax payable account" },
|
|
56
|
+
{ id: shareCapitalAccount, label: "Share capital account" },
|
|
57
|
+
]);
|
|
67
58
|
if (accountErrors.length > 0) {
|
|
68
59
|
return {
|
|
69
60
|
content: [{
|
|
@@ -189,7 +180,12 @@ export function registerEstonianTaxTools(server, api) {
|
|
|
189
180
|
const vat = vat_amount ?? roundMoney(net_amount * vat_rate);
|
|
190
181
|
if (vat > 0 && vatRegistered)
|
|
191
182
|
accountsToCheck.push(vatAcc);
|
|
192
|
-
const
|
|
183
|
+
const accounts = await api.readonly.getAccounts();
|
|
184
|
+
const accountErrors = validateAccounts(accounts, [
|
|
185
|
+
{ id: expense_account, label: "Expense account" },
|
|
186
|
+
...(vat > 0 && vatRegistered ? [{ id: vatAcc, label: "VAT account" }] : []),
|
|
187
|
+
{ id: payAcc, label: "Payable account" },
|
|
188
|
+
]);
|
|
193
189
|
if (accountErrors.length > 0) {
|
|
194
190
|
return {
|
|
195
191
|
content: [{
|
|
@@ -75,6 +75,7 @@ function getMonthLastDay(month) {
|
|
|
75
75
|
const [year, monthNumber] = month.split("-").map(Number);
|
|
76
76
|
return new Date(Date.UTC(year, monthNumber, 0)).getUTCDate();
|
|
77
77
|
}
|
|
78
|
+
const monthRegex = /^\d{4}-\d{2}$/;
|
|
78
79
|
export function registerFinancialStatementTools(server, api) {
|
|
79
80
|
server.tool("compute_trial_balance", "Compute trial balance (käibeandmik/proovibilanss) from journal postings. " +
|
|
80
81
|
"Shows debit/credit totals and balance for each account.", {
|
|
@@ -186,24 +187,23 @@ export function registerFinancialStatementTools(server, api) {
|
|
|
186
187
|
});
|
|
187
188
|
server.tool("month_end_close_checklist", "Generate month-end close checklist: unconfirmed journals/invoices, " +
|
|
188
189
|
"unreconciled bank transactions, overdue receivables/payables.", {
|
|
189
|
-
month: z.string().describe("Month to check (YYYY-MM, e.g. 2026-02)"),
|
|
190
|
+
month: z.string().regex(monthRegex, "Expected YYYY-MM").describe("Month to check (YYYY-MM, e.g. 2026-02)"),
|
|
190
191
|
}, { ...readOnly, title: "Month-End Close Checklist" }, async ({ month }) => {
|
|
191
192
|
const dateFrom = `${month}-01`;
|
|
192
193
|
const lastDay = getMonthLastDay(month);
|
|
193
194
|
const dateTo = `${month}-${String(lastDay).padStart(2, "0")}`;
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
const [allJournals, allTx, allSales, allPurchases] = await Promise.all([
|
|
196
|
+
api.journals.listAll(),
|
|
197
|
+
api.transactions.listAll(),
|
|
198
|
+
api.saleInvoices.listAll(),
|
|
199
|
+
api.purchaseInvoices.listAll(),
|
|
200
|
+
]);
|
|
196
201
|
const unconfirmedJournals = allJournals.filter(j => !j.is_deleted && !j.registered &&
|
|
197
202
|
j.effective_date >= dateFrom && j.effective_date <= dateTo);
|
|
198
|
-
// Unconfirmed transactions
|
|
199
|
-
const allTx = await api.transactions.listAll();
|
|
200
203
|
const unconfirmedTx = allTx.filter(tx => !tx.is_deleted && tx.status !== "CONFIRMED" &&
|
|
201
204
|
tx.date >= dateFrom && tx.date <= dateTo);
|
|
202
|
-
// Unconfirmed invoices
|
|
203
|
-
const allSales = await api.saleInvoices.listAll();
|
|
204
205
|
const unconfirmedSales = allSales.filter((inv) => inv.status === "PROJECT" &&
|
|
205
206
|
inv.journal_date >= dateFrom && inv.journal_date <= dateTo);
|
|
206
|
-
const allPurchases = await api.purchaseInvoices.listAll();
|
|
207
207
|
const unconfirmedPurchases = allPurchases.filter((inv) => inv.status === "PROJECT" &&
|
|
208
208
|
inv.journal_date >= dateFrom && inv.journal_date <= dateTo);
|
|
209
209
|
// Overdue receivables (compare to month-end date for reproducibility)
|
|
@@ -4,6 +4,8 @@ import { validateFilePath } from "../file-validation.js";
|
|
|
4
4
|
import { roundMoney } from "../money.js";
|
|
5
5
|
import { readOnly, batch } from "../annotations.js";
|
|
6
6
|
import { reportProgress } from "../progress.js";
|
|
7
|
+
import { parseCSVLine } from "../csv.js";
|
|
8
|
+
import { validateAccounts } from "../account-validation.js";
|
|
7
9
|
const MAX_CSV_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
8
10
|
// BRICEKSP is Lightyear's money market cash fund - not a real investment
|
|
9
11
|
const CASH_FUND_TICKER = "BRICEKSP";
|
|
@@ -22,42 +24,6 @@ function parseLightyearDate(d) {
|
|
|
22
24
|
}
|
|
23
25
|
return d;
|
|
24
26
|
}
|
|
25
|
-
function parseCSVLine(line) {
|
|
26
|
-
const fields = [];
|
|
27
|
-
let current = "";
|
|
28
|
-
let inQuotes = false;
|
|
29
|
-
for (let i = 0; i < line.length; i++) {
|
|
30
|
-
const ch = line[i];
|
|
31
|
-
if (inQuotes) {
|
|
32
|
-
if (ch === '"') {
|
|
33
|
-
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
34
|
-
current += '"';
|
|
35
|
-
i++;
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
inQuotes = false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
current += ch;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
if (ch === '"') {
|
|
47
|
-
inQuotes = true;
|
|
48
|
-
}
|
|
49
|
-
else if (ch === ",") {
|
|
50
|
-
fields.push(current);
|
|
51
|
-
current = "";
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
current += ch;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
fields.push(current);
|
|
59
|
-
return fields;
|
|
60
|
-
}
|
|
61
27
|
function validateHeaders(actual, expected, label) {
|
|
62
28
|
if (actual.length < expected.length) {
|
|
63
29
|
throw new Error(`${label}: expected ${expected.length} columns, got ${actual.length}. ` +
|
|
@@ -431,25 +397,13 @@ export function registerLightyearTools(server, api) {
|
|
|
431
397
|
const skipSet = new Set((skip_tickers ?? CASH_FUND_TICKER).split(",").map(t => t.trim()));
|
|
432
398
|
// Validate accounts exist and are active
|
|
433
399
|
const accounts = await api.readonly.getAccounts();
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
else if (!acc.is_valid) {
|
|
442
|
-
errors.push(`${label} ${id} (${acc.name_est}) is inactive. Activate it in e-arveldaja: Seaded → Kontoplaan → ${acc.name_est} → mark as active.`);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
checkAccount(investment_account, "Investment account");
|
|
446
|
-
checkAccount(broker_account, "Broker account");
|
|
447
|
-
if (fee_account)
|
|
448
|
-
checkAccount(fee_account, "Fee account");
|
|
449
|
-
if (gain_loss_account)
|
|
450
|
-
checkAccount(gain_loss_account, "Gain/loss account");
|
|
451
|
-
if (loss_account)
|
|
452
|
-
checkAccount(loss_account, "Loss account");
|
|
400
|
+
const errors = validateAccounts(accounts, [
|
|
401
|
+
{ id: investment_account, label: "Investment account" },
|
|
402
|
+
{ id: broker_account, label: "Broker account" },
|
|
403
|
+
...(fee_account ? [{ id: fee_account, label: "Fee account" }] : []),
|
|
404
|
+
...(gain_loss_account ? [{ id: gain_loss_account, label: "Gain/loss account" }] : []),
|
|
405
|
+
...(loss_account ? [{ id: loss_account, label: "Loss account" }] : []),
|
|
406
|
+
]);
|
|
453
407
|
if (errors.length > 0) {
|
|
454
408
|
return {
|
|
455
409
|
content: [{
|
|
@@ -674,22 +628,12 @@ export function registerLightyearTools(server, api) {
|
|
|
674
628
|
const fee_account = fee_account_param ?? 8610;
|
|
675
629
|
// Validate accounts exist and are active
|
|
676
630
|
const accounts = await api.readonly.getAccounts();
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
else if (!acc.is_valid) {
|
|
685
|
-
errors.push(`${label} ${id} (${acc.name_est}) is inactive. Activate it in e-arveldaja: Seaded → Kontoplaan → ${acc.name_est} → mark as active.`);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
checkAccount(broker_account, "Broker account");
|
|
689
|
-
checkAccount(income_account, "Income account");
|
|
690
|
-
if (tax_account)
|
|
691
|
-
checkAccount(tax_account, "Tax account");
|
|
692
|
-
checkAccount(fee_account, "Fee account");
|
|
631
|
+
const errors = validateAccounts(accounts, [
|
|
632
|
+
{ id: broker_account, label: "Broker account" },
|
|
633
|
+
{ id: income_account, label: "Income account" },
|
|
634
|
+
...(tax_account ? [{ id: tax_account, label: "Tax account" }] : []),
|
|
635
|
+
{ id: fee_account, label: "Fee account" },
|
|
636
|
+
]);
|
|
693
637
|
if (errors.length > 0) {
|
|
694
638
|
return {
|
|
695
639
|
content: [{
|
|
@@ -371,6 +371,7 @@ export function registerPdfWorkflowTools(server, api) {
|
|
|
371
371
|
notes: z.string().optional().describe("Notes (e.g. PDF filename)"),
|
|
372
372
|
ref_number: z.string().optional().describe("Reference number"),
|
|
373
373
|
bank_account_no: z.string().optional().describe("Supplier bank account"),
|
|
374
|
+
currency: z.string().optional().describe("Currency code (default EUR)"),
|
|
374
375
|
}, { ...create, title: "Create Purchase Invoice from PDF" }, async (params) => {
|
|
375
376
|
const supplier = await api.clients.get(params.supplier_client_id);
|
|
376
377
|
const isVatReg = await isCompanyVatRegistered(api);
|
|
@@ -384,7 +385,7 @@ export function registerPdfWorkflowTools(server, api) {
|
|
|
384
385
|
create_date: params.invoice_date,
|
|
385
386
|
journal_date: params.journal_date,
|
|
386
387
|
term_days: params.term_days,
|
|
387
|
-
cl_currencies_id: "EUR",
|
|
388
|
+
cl_currencies_id: params.currency ?? "EUR",
|
|
388
389
|
liability_accounts_id: params.liability_accounts_id ?? 2310,
|
|
389
390
|
bank_ref_number: params.ref_number,
|
|
390
391
|
bank_account_no: params.bank_account_no,
|
|
@@ -6,6 +6,7 @@ type PurchaseArticleWithVat = PurchaseArticle & {
|
|
|
6
6
|
vat_rate_dropdown?: string | null;
|
|
7
7
|
vat_rate?: number | null;
|
|
8
8
|
};
|
|
9
|
+
export declare function normalizeVatRate(value: unknown): string | undefined;
|
|
9
10
|
export declare function getPurchaseArticlesWithVat(api: ApiContext): Promise<PurchaseArticleWithVat[]>;
|
|
10
11
|
export declare function applyPurchaseVatDefaults(purchaseArticles: PurchaseArticleWithVat[], item: PurchaseInvoiceItem, isVatRegistered: boolean): PurchaseInvoiceItem;
|
|
11
12
|
export {};
|
|
@@ -16,7 +16,7 @@ function warnFallbackOnce(key, message) {
|
|
|
16
16
|
function toNumber(value) {
|
|
17
17
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
18
18
|
}
|
|
19
|
-
function normalizeVatRate(value) {
|
|
19
|
+
export function normalizeVatRate(value) {
|
|
20
20
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
21
21
|
return String(value);
|
|
22
22
|
}
|
|
@@ -3,6 +3,7 @@ import { readFile } from "fs/promises";
|
|
|
3
3
|
import { validateFilePath } from "../file-validation.js";
|
|
4
4
|
import { batch } from "../annotations.js";
|
|
5
5
|
import { reportProgress } from "../progress.js";
|
|
6
|
+
import { parseCSVLine } from "../csv.js";
|
|
6
7
|
const EXPECTED_HEADERS = [
|
|
7
8
|
"ID", "Status", "Direction", "Created on", "Finished on",
|
|
8
9
|
"Source fee amount", "Source fee currency", "Target fee amount", "Target fee currency",
|
|
@@ -10,32 +11,6 @@ const EXPECTED_HEADERS = [
|
|
|
10
11
|
"Target name", "Target amount (after fees)", "Target currency",
|
|
11
12
|
"Exchange rate", "Reference", "Batch", "Created by", "Category", "Note",
|
|
12
13
|
];
|
|
13
|
-
function parseCSVLine(line) {
|
|
14
|
-
const fields = [];
|
|
15
|
-
let current = "";
|
|
16
|
-
let inQuotes = false;
|
|
17
|
-
for (let i = 0; i < line.length; i++) {
|
|
18
|
-
const ch = line[i];
|
|
19
|
-
if (ch === '"') {
|
|
20
|
-
if (inQuotes && line[i + 1] === '"') {
|
|
21
|
-
current += '"';
|
|
22
|
-
i++;
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
inQuotes = !inQuotes;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
else if (ch === "," && !inQuotes) {
|
|
29
|
-
fields.push(current);
|
|
30
|
-
current = "";
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
current += ch;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
fields.push(current);
|
|
37
|
-
return fields;
|
|
38
|
-
}
|
|
39
14
|
function parseWiseCSV(csv) {
|
|
40
15
|
const lines = csv.trim().split("\n").filter(l => l.trim());
|
|
41
16
|
if (lines.length < 2)
|
package/dist/types/api.d.ts
CHANGED
|
@@ -346,6 +346,9 @@ export interface PurchaseInvoice {
|
|
|
346
346
|
settlements?: number[];
|
|
347
347
|
transactions?: number[];
|
|
348
348
|
}
|
|
349
|
+
export interface CreatePurchaseInvoiceData extends Pick<PurchaseInvoice, "clients_id" | "client_name" | "number" | "create_date" | "journal_date" | "term_days" | "cl_currencies_id" | "liability_accounts_id" | "bank_ref_number" | "bank_account_no" | "notes"> {
|
|
350
|
+
items: PurchaseInvoiceItem[];
|
|
351
|
+
}
|
|
349
352
|
export interface InvoiceSeries {
|
|
350
353
|
id?: number;
|
|
351
354
|
is_active: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "e-arveldaja-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MCP server for Estonian e-arveldaja (e-Financials) API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
"build": "tsc",
|
|
35
35
|
"start": "node dist/index.js",
|
|
36
36
|
"dev": "tsx src/index.ts",
|
|
37
|
-
"prepublishOnly": "npm run build"
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
43
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
"@types/node": "^22.13.14",
|
|
48
51
|
"@types/pdf-parse": "^1.1.4",
|
|
49
52
|
"tsx": "^4.19.3",
|
|
50
|
-
"typescript": "^5.8.2"
|
|
53
|
+
"typescript": "^5.8.2",
|
|
54
|
+
"vitest": "^4.1.0"
|
|
51
55
|
}
|
|
52
56
|
}
|