@kirjapro/mcp-server 0.13.0 → 0.14.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/CHANGELOG.md +37 -0
- package/dist/index.js +249 -129
- package/package.json +1 -1
- package/server.json +1 -1
- package/tool-fingerprints.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.14.0] - 2026-10-05
|
|
6
|
+
|
|
7
|
+
Prepared, not yet published. Publish only after (1) `db:deploy` of the round-3
|
|
8
|
+
migrations `20261005170000`–`20261005200000` (AI-työtila 2.0 batches 10–13;
|
|
9
|
+
`kirjapro_send_invoice` needs `20261005180100`, the delivery intents), (2)
|
|
10
|
+
`npm run functions:deploy -- dispatch-invoice` from the same main, which resumes a
|
|
11
|
+
delivery under its intent, and (3) a commit of `src/database.types.ts` regenerated
|
|
12
|
+
from production with `--schema public`, so `check:types-drift` passes. Until then
|
|
13
|
+
0.13.0 keeps working unchanged against the current database.
|
|
14
|
+
|
|
15
|
+
- `kirjapro_get_ar_aging` shares its result shaping with Kirjapro; an unexpected
|
|
16
|
+
report payload now returns a sanitized error instead of an internal exception.
|
|
17
|
+
- `kirjapro_create_invoice`: `paymentTermDays` has no schema default any more, so
|
|
18
|
+
the customer's own payment term applies before 14 days (0.13.0 always used 14).
|
|
19
|
+
`vatRate` defaults to `25.5` only when a line has no `vatTreatmentCode`; an
|
|
20
|
+
explicit rate that contradicts the code is refused as `VAT_RATE_MISMATCH`
|
|
21
|
+
instead of the code winning silently. Lines take `discountPercent` (0–100, two
|
|
22
|
+
decimals, before VAT), and the historical `24`/`14` rates
|
|
23
|
+
(`legacy_standard_24` until 31.8.2024, `legacy_standard_14` until 31.12.2025)
|
|
24
|
+
are accepted inside their windows. Fingerprint changes.
|
|
25
|
+
- `kirjapro_send_invoice` caches only a confirmed result. A lost transport, relay
|
|
26
|
+
error, 5xx or a delivery whose provider outcome is still pending returns
|
|
27
|
+
`IDEMPOTENCY_OUTCOME_UNKNOWN` and leaves the claim pending; 0.13.0 cached it as a
|
|
28
|
+
failure, which invited a retry with a new key. A confirmed refusal returns
|
|
29
|
+
`CODE: viesti` and a send returns `delivery_id`. Needs the database migration
|
|
30
|
+
`20261005180100` and the matching `dispatch-invoice` deploy (delivery intents).
|
|
31
|
+
- The generated copies `src/compliance/purchase-vat-allocation.ts`,
|
|
32
|
+
`src/compliance/date-local.ts` and the new `src/shared/invoice-delivery.ts` are
|
|
33
|
+
byte-guarded by the test suite like `src/shared/invoice-draft.ts`.
|
|
34
|
+
- `kirjapro_search_transactions` with `minAmount` or `maxAmount` returns
|
|
35
|
+
`FILTER_TOO_BROAD: …` when more than 1 000 transactions (or an unknown number)
|
|
36
|
+
match the other filters, instead of filtering only the first 1 000 and
|
|
37
|
+
reporting that sample's size as `total_count`. Up to 1 000 matches the count
|
|
38
|
+
stays exact. Narrow `dateFrom`/`dateTo` or `query` to search further.
|
|
39
|
+
|
|
3
40
|
## [0.13.0] - 2026-10-04
|
|
4
41
|
|
|
5
42
|
No database prerequisite: every function, rule and VAT treatment it uses was
|
package/dist/index.js
CHANGED
|
@@ -4625,7 +4625,7 @@ var require_core = __commonJS({
|
|
|
4625
4625
|
errorsText(errors = this.errors, { separator = ", ", dataVar = "data" } = {}) {
|
|
4626
4626
|
if (!errors || errors.length === 0)
|
|
4627
4627
|
return "No errors";
|
|
4628
|
-
return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((
|
|
4628
|
+
return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text2, msg) => text2 + separator + msg);
|
|
4629
4629
|
}
|
|
4630
4630
|
$dataMetaSchema(metaSchema, keywordsJsonPointers) {
|
|
4631
4631
|
const rules = this.RULES.all;
|
|
@@ -19919,8 +19919,8 @@ function ko_default() {
|
|
|
19919
19919
|
}
|
|
19920
19920
|
|
|
19921
19921
|
// node_modules/zod/v4/locales/lt.js
|
|
19922
|
-
var capitalizeFirstCharacter = (
|
|
19923
|
-
return
|
|
19922
|
+
var capitalizeFirstCharacter = (text2) => {
|
|
19923
|
+
return text2.charAt(0).toUpperCase() + text2.slice(1);
|
|
19924
19924
|
};
|
|
19925
19925
|
function getUnitTypeFromNumber(number4) {
|
|
19926
19926
|
const abs = Math.abs(number4);
|
|
@@ -35679,7 +35679,7 @@ var StdioServerTransport = class {
|
|
|
35679
35679
|
import { createClient } from "@supabase/supabase-js";
|
|
35680
35680
|
|
|
35681
35681
|
// src/version.ts
|
|
35682
|
-
var VERSION = "0.
|
|
35682
|
+
var VERSION = "0.14.0";
|
|
35683
35683
|
|
|
35684
35684
|
// src/auth.ts
|
|
35685
35685
|
var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
|
|
@@ -36017,15 +36017,15 @@ function resultErrorCode(result) {
|
|
|
36017
36017
|
const match = textPart?.text.match(/^([A-Z][A-Z0-9_]{1,63}):/);
|
|
36018
36018
|
const candidate = match?.[1];
|
|
36019
36019
|
if (candidate && SAFE_TOOL_ERROR_CODES.has(candidate)) return candidate;
|
|
36020
|
-
const
|
|
36021
|
-
if (
|
|
36022
|
-
if (
|
|
36020
|
+
const text2 = (textPart?.text ?? "").replace(/^MCP error -?\d+: /, "");
|
|
36021
|
+
if (text2.startsWith("Input validation error:")) return "INVALID_ARGUMENTS";
|
|
36022
|
+
if (text2.startsWith("Output validation error:")) {
|
|
36023
36023
|
return "OUTPUT_VALIDATION_ERROR";
|
|
36024
36024
|
}
|
|
36025
|
-
if (
|
|
36025
|
+
if (text2.startsWith("Tool ") && text2.endsWith(" not found")) {
|
|
36026
36026
|
return "UNKNOWN_TOOL";
|
|
36027
36027
|
}
|
|
36028
|
-
if (
|
|
36028
|
+
if (text2.startsWith("Tool ") && text2.endsWith(" disabled")) {
|
|
36029
36029
|
return "TOOL_DISABLED";
|
|
36030
36030
|
}
|
|
36031
36031
|
return "TOOL_ERROR";
|
|
@@ -36217,11 +36217,11 @@ function instrumentToolCalls(server2, options) {
|
|
|
36217
36217
|
}
|
|
36218
36218
|
|
|
36219
36219
|
// src/utils.ts
|
|
36220
|
-
function formatCurrency(
|
|
36220
|
+
function formatCurrency(amount2) {
|
|
36221
36221
|
return new Intl.NumberFormat("fi-FI", {
|
|
36222
36222
|
style: "currency",
|
|
36223
36223
|
currency: "EUR"
|
|
36224
|
-
}).format(
|
|
36224
|
+
}).format(amount2);
|
|
36225
36225
|
}
|
|
36226
36226
|
function helsinkiDateString(now = /* @__PURE__ */ new Date()) {
|
|
36227
36227
|
const parts = new Intl.DateTimeFormat("fi-FI", {
|
|
@@ -36363,8 +36363,8 @@ async function invokeEdgeFunction(supabase2, name, body) {
|
|
|
36363
36363
|
}
|
|
36364
36364
|
return { data, error: null };
|
|
36365
36365
|
}
|
|
36366
|
-
function roundCents(
|
|
36367
|
-
return Math.round(
|
|
36366
|
+
function roundCents(amount2) {
|
|
36367
|
+
return Math.round(amount2 * 100) / 100;
|
|
36368
36368
|
}
|
|
36369
36369
|
var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
36370
36370
|
var CONTROL = /[\u0000-\u001F\u007F]/g;
|
|
@@ -36818,6 +36818,13 @@ function registerTransactionTools(server2) {
|
|
|
36818
36818
|
"Failed to search transactions. Check your filters and try again."
|
|
36819
36819
|
);
|
|
36820
36820
|
}
|
|
36821
|
+
const knownCount = Number.isSafeInteger(count) ? count : null;
|
|
36822
|
+
if (hasAmountFilter && (knownCount === null || knownCount > AMOUNT_FILTER_CAP)) {
|
|
36823
|
+
const matches = knownCount !== null ? `${knownCount} match the other filters` : "the number matching the other filters is unknown";
|
|
36824
|
+
return toErrorContent(
|
|
36825
|
+
`FILTER_TOO_BROAD: Amount filters read at most ${AMOUNT_FILTER_CAP} transactions, and ${matches}. Narrow dateFrom/dateTo or query; no partial count is returned.`
|
|
36826
|
+
);
|
|
36827
|
+
}
|
|
36821
36828
|
let results = (data || []).map(
|
|
36822
36829
|
(t) => {
|
|
36823
36830
|
const totalDebit = roundCents(
|
|
@@ -36858,10 +36865,9 @@ function registerTransactionTools(server2) {
|
|
|
36858
36865
|
}
|
|
36859
36866
|
const filteredTotal = results.length;
|
|
36860
36867
|
const paginatedResults = results.slice(offset, offset + limit);
|
|
36861
|
-
const cappedWarning = (count || 0) >= AMOUNT_FILTER_CAP ? " (results capped at 1000 rows \u2014 narrow your date range for complete results)" : "";
|
|
36862
36868
|
return toResult({
|
|
36863
36869
|
...paginate(paginatedResults, filteredTotal, offset, limit),
|
|
36864
|
-
amount_filter_note: `Filtered client-side from ${count
|
|
36870
|
+
amount_filter_note: `Filtered client-side from ${count} transactions`
|
|
36865
36871
|
});
|
|
36866
36872
|
}
|
|
36867
36873
|
return toResult(paginate(results, count || 0, offset, limit));
|
|
@@ -37312,15 +37318,17 @@ var INVOICE_VAT_TREATMENT_CODES = [
|
|
|
37312
37318
|
"exempt_education",
|
|
37313
37319
|
"exempt_financial",
|
|
37314
37320
|
"exempt_insurance",
|
|
37315
|
-
"exempt_other"
|
|
37321
|
+
"exempt_other",
|
|
37322
|
+
"legacy_standard_24",
|
|
37323
|
+
"legacy_standard_14"
|
|
37316
37324
|
];
|
|
37317
|
-
var INVOICE_VAT_RATES = ["25.5", "13.5", "10", "0"];
|
|
37325
|
+
var INVOICE_VAT_RATES = ["25.5", "13.5", "10", "0", "24", "14"];
|
|
37318
37326
|
var INVOICE_DRAFT_MAX_LINES = 100;
|
|
37319
37327
|
var INVOICE_DRAFT_CUSTOMER_COLUMNS = "id, name, payment_term_days, customer_kind, business_id, vat_number, country";
|
|
37320
37328
|
var ERROR_MESSAGES = {
|
|
37321
37329
|
INVALID_INPUT: (detail) => `Laskun tiedoissa on virhe: ${detail}.`,
|
|
37322
37330
|
CUSTOMER_NOT_FOUND: (detail) => `Asiakasta ei l\xF6ydy t\xE4st\xE4 yrityksest\xE4${detail ? ` (${detail})` : ""}.`,
|
|
37323
|
-
CUSTOMER_AMBIGUOUS: (detail) => `
|
|
37331
|
+
CUSTOMER_AMBIGUOUS: (detail) => `Asiakas pit\xE4\xE4 valita t\xE4sm\xE4llisesti: ${detail}. K\xE4yt\xE4 t\xE4sm\xE4llist\xE4 nime\xE4 tai asiakasnumeroa.`,
|
|
37324
37332
|
LOOKUP_FAILED: () => "Laskun tietojen haku ep\xE4onnistui. Mit\xE4\xE4n ei tallennettu; voit yritt\xE4\xE4 uudelleen.",
|
|
37325
37333
|
FISCAL_YEAR_REQUIRED: () => "Ei tilikautta valittuna.",
|
|
37326
37334
|
FISCAL_YEAR_NOT_FOUND: () => "Tilikautta ei l\xF6ydy. Tarkista valinta.",
|
|
@@ -37329,6 +37337,7 @@ var ERROR_MESSAGES = {
|
|
|
37329
37337
|
VAT_SELLER_NOT_REGISTERED: () => "ALV-rekisteriin kuulumaton myyj\xE4 voi k\xE4ytt\xE4\xE4 vain Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
|
|
37330
37338
|
VAT_SELLER_REGISTERED: () => "ALV-rekisteriss\xE4 oleva myyj\xE4 ei voi k\xE4ytt\xE4\xE4 Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
|
|
37331
37339
|
VAT_RATE_NOT_IN_FORCE: (detail) => `ALV-kanta ${detail}.`,
|
|
37340
|
+
VAT_RATE_MISMATCH: (detail) => `ALV-kanta ja ALV-k\xE4sittely ovat ristiriidassa (${detail}). Anna vain k\xE4sittely tai sit\xE4 vastaava kanta.`,
|
|
37332
37341
|
VAT_REQUIRES_BUSINESS_CUSTOMER: () => "Valittu ALV-k\xE4sittely vaatii yritysasiakkaan.",
|
|
37333
37342
|
VAT_REQUIRES_FINNISH_BUSINESS_ID: () => "Kotimainen k\xE4\xE4nnetty verovelvollisuus vaatii ostajan kelvollisen Y-tunnuksen.",
|
|
37334
37343
|
VAT_REQUIRES_EU_VAT_NUMBER: () => "EU-myynti vaatii muun EU-maan maakoodin ja sit\xE4 vastaavan ostajan ALV-tunnisteen.",
|
|
@@ -37353,9 +37362,9 @@ function roundNumericMoney(value) {
|
|
|
37353
37362
|
const digits = Math.abs(value).toPrecision(15);
|
|
37354
37363
|
if (digits.includes("e")) return 0;
|
|
37355
37364
|
const [whole = "0", fraction = ""] = digits.split(".");
|
|
37356
|
-
const
|
|
37357
|
-
if (
|
|
37358
|
-
return value < 0 ? -
|
|
37365
|
+
const cents3 = Number(whole) * 100 + Number(fraction.slice(0, 2).padEnd(2, "0")) + (Number(fraction.charAt(2) || "0") >= 5 ? 1 : 0);
|
|
37366
|
+
if (cents3 === 0) return 0;
|
|
37367
|
+
return value < 0 ? -cents3 / 100 : cents3 / 100;
|
|
37359
37368
|
}
|
|
37360
37369
|
function calculateLineTotals(quantity, unitPrice, vatRate, priceEntryMode = "vat_exclusive", discountPercent = 0) {
|
|
37361
37370
|
const discountFactor = 1 - discountPercent / 100;
|
|
@@ -37399,17 +37408,24 @@ function addDaysIso(date5, days) {
|
|
|
37399
37408
|
var TREATMENT_RATES = {
|
|
37400
37409
|
standard_25_5: 25.5,
|
|
37401
37410
|
standard_13_5: 13.5,
|
|
37402
|
-
standard_10: 10
|
|
37411
|
+
standard_10: 10,
|
|
37412
|
+
legacy_standard_24: 24,
|
|
37413
|
+
legacy_standard_14: 14
|
|
37403
37414
|
};
|
|
37404
37415
|
var STANDARD_TREATMENTS = {
|
|
37405
37416
|
"25.5": "standard_25_5",
|
|
37406
37417
|
"13.5": "standard_13_5",
|
|
37407
|
-
"10": "standard_10"
|
|
37418
|
+
"10": "standard_10",
|
|
37419
|
+
"24": "legacy_standard_24",
|
|
37420
|
+
"14": "legacy_standard_14"
|
|
37408
37421
|
};
|
|
37409
37422
|
function resolveInvoiceVatTreatment(vatRate, code) {
|
|
37410
37423
|
const resolved = code ?? (vatRate === void 0 ? void 0 : STANDARD_TREATMENTS[vatRate]);
|
|
37411
37424
|
if (!resolved) return invoiceDraftError("VAT_TREATMENT_REQUIRED");
|
|
37412
|
-
|
|
37425
|
+
const rate = TREATMENT_RATES[resolved] ?? 0;
|
|
37426
|
+
if (vatRate !== void 0 && Number(vatRate) !== rate)
|
|
37427
|
+
return invoiceDraftError("VAT_RATE_MISMATCH", `${vatRate} % ja ${resolved}`);
|
|
37428
|
+
return { code: resolved, rate };
|
|
37413
37429
|
}
|
|
37414
37430
|
var EU_COUNTRY_CODES = [
|
|
37415
37431
|
"AT",
|
|
@@ -37461,6 +37477,10 @@ function invoiceVatTreatmentError(code, sellerAlvRegistered, customer, documentD
|
|
|
37461
37477
|
return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "25,5 % ei ole voimassa ennen 1.9.2024");
|
|
37462
37478
|
if (code === "standard_13_5" && documentDate < "2026-01-01")
|
|
37463
37479
|
return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "13,5 % ei ole voimassa ennen 1.1.2026");
|
|
37480
|
+
if (code === "legacy_standard_24" && documentDate > "2024-08-31")
|
|
37481
|
+
return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "24 % on voimassa vain 31.8.2024 asti");
|
|
37482
|
+
if (code === "legacy_standard_14" && documentDate > "2025-12-31")
|
|
37483
|
+
return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "14 % on voimassa vain 31.12.2025 asti");
|
|
37464
37484
|
if ((code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu") && customer.customer_kind !== "business")
|
|
37465
37485
|
return invoiceDraftError("VAT_REQUIRES_BUSINESS_CUSTOMER");
|
|
37466
37486
|
if ((code === "reverse_construction" || code === "reverse_scrap") && !isFinnishBusinessId(customer.business_id ?? ""))
|
|
@@ -37496,6 +37516,9 @@ function inputError(input2) {
|
|
|
37496
37516
|
if (!Number.isFinite(line.unitPrice) || line.unitPrice < 0 || line.unitPrice > 1e9)
|
|
37497
37517
|
return `${row}: yksikk\xF6hinnan pit\xE4\xE4 olla 0\u20131 000 000 000 \u20AC`;
|
|
37498
37518
|
if (line.quantity * line.unitPrice > 1e12) return `${row}: rivin summa on liian suuri`;
|
|
37519
|
+
const discount = line.discountPercent;
|
|
37520
|
+
if (discount !== void 0 && (!Number.isFinite(discount) || discount < 0 || discount > 100 || Math.abs(discount * 100 - Math.round(discount * 100)) > 1e-9))
|
|
37521
|
+
return `${row}: alennuksen pit\xE4\xE4 olla 0\u2013100 % enint\xE4\xE4n kahdella desimaalilla`;
|
|
37499
37522
|
}
|
|
37500
37523
|
return null;
|
|
37501
37524
|
}
|
|
@@ -37514,11 +37537,13 @@ function prepareInvoiceDraft(input2, facts) {
|
|
|
37514
37537
|
input2.invoiceDate
|
|
37515
37538
|
);
|
|
37516
37539
|
if (ineligible) return { ok: false, error: ineligible };
|
|
37540
|
+
const discountPercent = line.discountPercent ?? 0;
|
|
37517
37541
|
const amounts = calculateLineTotals(
|
|
37518
37542
|
line.quantity,
|
|
37519
37543
|
line.unitPrice,
|
|
37520
37544
|
treatment.rate,
|
|
37521
|
-
input2.priceEntryMode
|
|
37545
|
+
input2.priceEntryMode,
|
|
37546
|
+
discountPercent
|
|
37522
37547
|
);
|
|
37523
37548
|
lines.push({
|
|
37524
37549
|
line_number: index + 1,
|
|
@@ -37527,6 +37552,7 @@ function prepareInvoiceDraft(input2, facts) {
|
|
|
37527
37552
|
unit: line.unit,
|
|
37528
37553
|
unit_price: line.unitPrice,
|
|
37529
37554
|
entered_unit_price: line.unitPrice,
|
|
37555
|
+
discount_percent: discountPercent,
|
|
37530
37556
|
vat_rate: treatment.rate,
|
|
37531
37557
|
vat_treatment_code: treatment.code,
|
|
37532
37558
|
vat_treatment_overridden: true,
|
|
@@ -37633,6 +37659,47 @@ function calculateInvoiceBalance(total2, payments, creditedTotal) {
|
|
|
37633
37659
|
return { paidTotal, remainingBalance: roundCents(total2 - paidTotal - creditedTotal) };
|
|
37634
37660
|
}
|
|
37635
37661
|
|
|
37662
|
+
// src/shared/invoice-delivery.ts
|
|
37663
|
+
var MESSAGES = {
|
|
37664
|
+
INVOICE_NOT_FOUND: "Laskua ei l\xF6ydy t\xE4st\xE4 yrityksest\xE4. Tarkista laskun numero.",
|
|
37665
|
+
INVOICE_NOT_DRAFT: "Vain luonnostilassa olevan laskun voi l\xE4hett\xE4\xE4. Laskua ei l\xE4hetetty.",
|
|
37666
|
+
DELIVERY_RECIPIENT_MISSING: "Asiakkaalta puuttuu valitun kanavan vastaanottaja (s\xE4hk\xF6postiosoite tai verkkolaskuosoite ja operaattori). Laskua ei l\xE4hetetty.",
|
|
37667
|
+
DELIVERY_CHANGED: "Laskun sis\xE4lt\xF6 tai vastaanottaja muuttui hyv\xE4ksynn\xE4n j\xE4lkeen. Laskua ei l\xE4hetetty; pyyd\xE4 l\xE4hetys uudelleen.",
|
|
37668
|
+
DELIVERY_IN_PROGRESS: "Laskun l\xE4hetys on jo k\xE4sittelyss\xE4 toisessa pyynn\xF6ss\xE4. Tarkista laskun tila ennen uutta yrityst\xE4.",
|
|
37669
|
+
DELIVERY_FAILED: "Laskun l\xE4hetys ep\xE4onnistui. Laskua ei l\xE4hetetty.",
|
|
37670
|
+
DELIVERY_OUTCOME_UNKNOWN: "L\xE4hetyksen tila on ep\xE4varma. \xC4l\xE4 l\xE4het\xE4 laskua uudelleen ennen kuin olet tarkistanut sen tilan.",
|
|
37671
|
+
DELIVERY_UNAVAILABLE: "Laskun l\xE4hetys ei ole k\xE4ytett\xE4viss\xE4 AI-chatissa.",
|
|
37672
|
+
DELIVERY_STOPPED: "L\xE4hetyst\xE4 ei tehty: keskustelun ty\xF6 p\xE4\xE4ttyi tai pys\xE4ytettiin ennen l\xE4hetyst\xE4."
|
|
37673
|
+
};
|
|
37674
|
+
function invoiceDeliveryMessage(code) {
|
|
37675
|
+
return MESSAGES[code];
|
|
37676
|
+
}
|
|
37677
|
+
var KNOWN_CODES = Object.keys(MESSAGES);
|
|
37678
|
+
function invoiceDeliveryErrorCode(value) {
|
|
37679
|
+
return typeof value === "string" && KNOWN_CODES.includes(value) ? value : "DELIVERY_FAILED";
|
|
37680
|
+
}
|
|
37681
|
+
var text = (value) => typeof value === "string" && value ? value : null;
|
|
37682
|
+
function classifyInvoiceDelivery(response) {
|
|
37683
|
+
const body = response.body && typeof response.body === "object" ? response.body : null;
|
|
37684
|
+
const deliveryId = text(body?.deliveryId);
|
|
37685
|
+
const unknown2 = { status: "unknown", deliveryId, errorCode: "DELIVERY_OUTCOME_UNKNOWN" };
|
|
37686
|
+
if (response.httpStatus === 200 && body) {
|
|
37687
|
+
if (body.deliveryStatus === "sent" && body.success === true)
|
|
37688
|
+
return {
|
|
37689
|
+
status: "sent",
|
|
37690
|
+
deliveryId,
|
|
37691
|
+
transmissionId: text(body.transmissionId),
|
|
37692
|
+
messageId: text(body.messageId)
|
|
37693
|
+
};
|
|
37694
|
+
if (body.deliveryStatus === "failed")
|
|
37695
|
+
return { status: "failed", deliveryId, errorCode: invoiceDeliveryErrorCode(body.errorCode) };
|
|
37696
|
+
return unknown2;
|
|
37697
|
+
}
|
|
37698
|
+
if (response.httpStatus !== null && response.httpStatus >= 400 && response.httpStatus < 500)
|
|
37699
|
+
return { status: "failed", deliveryId, errorCode: "DELIVERY_FAILED" };
|
|
37700
|
+
return unknown2;
|
|
37701
|
+
}
|
|
37702
|
+
|
|
37636
37703
|
// src/tools/invoices.ts
|
|
37637
37704
|
var LIST_INVOICES_ROW_SHAPE = {
|
|
37638
37705
|
id: external_exports.string().uuid(),
|
|
@@ -37688,7 +37755,7 @@ var GET_INVOICE_OUTPUT_SHAPE = {
|
|
|
37688
37755
|
var CREATE_INVOICE_INPUT_SHAPE = {
|
|
37689
37756
|
customerId: external_exports.string().uuid().describe("Customer UUID"),
|
|
37690
37757
|
invoiceDate: ISO_DATE_SCHEMA.describe("Invoice date (YYYY-MM-DD)"),
|
|
37691
|
-
paymentTermDays: external_exports.number().min(0).max(365).
|
|
37758
|
+
paymentTermDays: external_exports.number().min(0).max(365).optional().describe("Payment term in days (default: the customer's payment term, else 14)"),
|
|
37692
37759
|
notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
|
|
37693
37760
|
priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
|
|
37694
37761
|
reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
|
|
@@ -37702,7 +37769,8 @@ var CREATE_INVOICE_INPUT_SHAPE = {
|
|
|
37702
37769
|
quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
|
|
37703
37770
|
unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
|
|
37704
37771
|
unitPrice: external_exports.number().finite().min(0).max(1e9).describe("Entered unit price in EUR (must be >= 0)"),
|
|
37705
|
-
vatRate: external_exports.enum(INVOICE_VAT_RATES).
|
|
37772
|
+
vatRate: external_exports.enum(INVOICE_VAT_RATES).optional().describe("VAT rate (default 25.5 when no vatTreatmentCode is given). Must match the treatment's rate when both are given. 24 and 14 are historical rates (until 31.8.2024 and 31.12.2025)"),
|
|
37773
|
+
discountPercent: external_exports.number().min(0).max(100).optional().describe("Line discount in percent (0-100, at most two decimals), applied before VAT"),
|
|
37706
37774
|
vatTreatmentCode: external_exports.enum(INVOICE_VAT_TREATMENT_CODES).optional().describe("Structured VAT treatment. Required when VAT rate is 0. service_outside_eu is ONLY for AVL 65 general-rule B2B services: verify business use, evidence of buyer status and receiving establishment outside the EU; special place-of-supply services are excluded. Check foreign buyer liability and invoice notes. PDF delivery only; e-invoice delivery is blocked.")
|
|
37707
37775
|
})
|
|
37708
37776
|
).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
|
|
@@ -37727,7 +37795,10 @@ async function createInvoiceDraftCommand(ctx, params) {
|
|
|
37727
37795
|
notes: params.notes,
|
|
37728
37796
|
reverseChargeConfirmed: params.reverseChargeConfirmed,
|
|
37729
37797
|
euGoodsTransportConfirmed: params.euGoodsTransportConfirmed,
|
|
37730
|
-
|
|
37798
|
+
// A rate defaults to 25.5 only when neither a rate nor a treatment is given.
|
|
37799
|
+
lines: params.lines.map(
|
|
37800
|
+
(line) => line.vatRate === void 0 && line.vatTreatmentCode === void 0 ? { ...line, vatRate: "25.5" } : line
|
|
37801
|
+
)
|
|
37731
37802
|
},
|
|
37732
37803
|
{ sellerAlvRegistered: company.alv_registered, customer }
|
|
37733
37804
|
);
|
|
@@ -37775,6 +37846,51 @@ async function createInvoiceDraftCommand(ctx, params) {
|
|
|
37775
37846
|
}
|
|
37776
37847
|
});
|
|
37777
37848
|
}
|
|
37849
|
+
async function sendInvoiceCommand(ctx, params) {
|
|
37850
|
+
const { data: invoice, error: invoiceError } = await ctx.supabase.from("invoices").select("id, invoice_number, status").eq("id", params.invoiceId).eq("company_id", ctx.companyId).maybeSingle();
|
|
37851
|
+
if (invoiceError || !invoice) {
|
|
37852
|
+
return toErrorContent(`Invoice ${params.invoiceId} not found.`);
|
|
37853
|
+
}
|
|
37854
|
+
if (invoice.status !== "draft") {
|
|
37855
|
+
return toErrorContent(
|
|
37856
|
+
`Invoice ${invoice.invoice_number} cannot be delivered from status "${invoice.status}". Only draft invoices can be sent.`
|
|
37857
|
+
);
|
|
37858
|
+
}
|
|
37859
|
+
let httpStatus = null;
|
|
37860
|
+
let body = null;
|
|
37861
|
+
try {
|
|
37862
|
+
const { data, error: error61, response } = await ctx.supabase.functions.invoke("dispatch-invoice", {
|
|
37863
|
+
body: { invoiceId: invoice.id, channel: params.channel }
|
|
37864
|
+
});
|
|
37865
|
+
const failed = error61?.context;
|
|
37866
|
+
if (!error61) {
|
|
37867
|
+
httpStatus = response?.status ?? 200;
|
|
37868
|
+
body = data;
|
|
37869
|
+
} else if (failed instanceof Response) {
|
|
37870
|
+
httpStatus = failed.status;
|
|
37871
|
+
body = await failed.json().catch(() => null);
|
|
37872
|
+
}
|
|
37873
|
+
} catch {
|
|
37874
|
+
}
|
|
37875
|
+
const outcome = classifyInvoiceDelivery({ httpStatus, body });
|
|
37876
|
+
if (outcome.status === "unknown") {
|
|
37877
|
+
return toWriteOutcomeUnknown("kirjapro_send_invoice", "kirjapro_get_invoice");
|
|
37878
|
+
}
|
|
37879
|
+
if (outcome.status === "failed") {
|
|
37880
|
+
const detail = body && typeof body === "object" && typeof body.error === "string" ? body.error : invoiceDeliveryMessage(outcome.errorCode);
|
|
37881
|
+
return toErrorContent(`${outcome.errorCode}: ${detail}`);
|
|
37882
|
+
}
|
|
37883
|
+
return toResult({
|
|
37884
|
+
message: `Invoice ${invoice.invoice_number} delivered via ${params.channel}`,
|
|
37885
|
+
invoice_id: invoice.id,
|
|
37886
|
+
invoice_number: invoice.invoice_number,
|
|
37887
|
+
channel: params.channel,
|
|
37888
|
+
delivery_id: outcome.deliveryId,
|
|
37889
|
+
transmission_id: outcome.transmissionId,
|
|
37890
|
+
message_id: outcome.messageId,
|
|
37891
|
+
status: "sent"
|
|
37892
|
+
});
|
|
37893
|
+
}
|
|
37778
37894
|
function registerInvoiceTools(server2) {
|
|
37779
37895
|
server2.registerTool(
|
|
37780
37896
|
"kirjapro_list_invoices",
|
|
@@ -37854,42 +37970,14 @@ function registerInvoiceTools(server2) {
|
|
|
37854
37970
|
annotations: WRITE_EXTERNAL_ANNOTATIONS
|
|
37855
37971
|
},
|
|
37856
37972
|
async (params) => {
|
|
37857
|
-
const
|
|
37858
|
-
const companyId2 = getCompanyId();
|
|
37973
|
+
const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
|
|
37859
37974
|
return withIdempotency(
|
|
37860
|
-
|
|
37861
|
-
|
|
37975
|
+
ctx.supabase,
|
|
37976
|
+
ctx.companyId,
|
|
37862
37977
|
"kirjapro_send_invoice",
|
|
37863
37978
|
params.idempotencyKey,
|
|
37864
37979
|
params,
|
|
37865
|
-
|
|
37866
|
-
const { data: invoice, error: invoiceError } = await supabase2.from("invoices").select("id, invoice_number, status").eq("id", params.invoiceId).eq("company_id", companyId2).maybeSingle();
|
|
37867
|
-
if (invoiceError || !invoice) {
|
|
37868
|
-
return toErrorContent(`Invoice ${params.invoiceId} not found.`);
|
|
37869
|
-
}
|
|
37870
|
-
if (invoice.status !== "draft") {
|
|
37871
|
-
return toErrorContent(
|
|
37872
|
-
`Invoice ${invoice.invoice_number} cannot be delivered from status "${invoice.status}". Only draft invoices can be sent.`
|
|
37873
|
-
);
|
|
37874
|
-
}
|
|
37875
|
-
const { data, error: error61 } = await supabase2.functions.invoke("dispatch-invoice", {
|
|
37876
|
-
body: { invoiceId: invoice.id, channel: params.channel }
|
|
37877
|
-
});
|
|
37878
|
-
if (error61 || !data?.success) {
|
|
37879
|
-
return toErrorContent(
|
|
37880
|
-
data?.error || error61?.message || `Failed to deliver invoice ${invoice.invoice_number}.`
|
|
37881
|
-
);
|
|
37882
|
-
}
|
|
37883
|
-
return toResult({
|
|
37884
|
-
message: `Invoice ${invoice.invoice_number} delivered via ${params.channel}`,
|
|
37885
|
-
invoice_id: invoice.id,
|
|
37886
|
-
invoice_number: invoice.invoice_number,
|
|
37887
|
-
channel: data.channel,
|
|
37888
|
-
transmission_id: data.transmissionId ?? null,
|
|
37889
|
-
message_id: data.messageId ?? null,
|
|
37890
|
-
status: "sent"
|
|
37891
|
-
});
|
|
37892
|
-
}
|
|
37980
|
+
() => sendInvoiceCommand(ctx, params)
|
|
37893
37981
|
);
|
|
37894
37982
|
}
|
|
37895
37983
|
);
|
|
@@ -38285,9 +38373,9 @@ var PRH_LOOKUP_OUTPUT_SHAPE = {
|
|
|
38285
38373
|
website: external_exports.string().nullable(),
|
|
38286
38374
|
phone: external_exports.string().nullable()
|
|
38287
38375
|
};
|
|
38288
|
-
function sanitizeText(
|
|
38289
|
-
if (!
|
|
38290
|
-
return
|
|
38376
|
+
function sanitizeText(text2) {
|
|
38377
|
+
if (!text2) return null;
|
|
38378
|
+
return text2.trim().replace(/\s+/g, " ").slice(0, 500);
|
|
38291
38379
|
}
|
|
38292
38380
|
function registerCustomerTools(server2) {
|
|
38293
38381
|
server2.registerTool(
|
|
@@ -38557,6 +38645,62 @@ function registerCustomerTools(server2) {
|
|
|
38557
38645
|
);
|
|
38558
38646
|
}
|
|
38559
38647
|
|
|
38648
|
+
// src/shared/ar-aging-report.ts
|
|
38649
|
+
var AR_AGING_TOOL = {
|
|
38650
|
+
name: "kirjapro_get_ar_aging",
|
|
38651
|
+
description: "Get an exact, bounded accounts-receivable aging summary as of today in Europe/Helsinki. Outstanding amounts account for partial payments and sent credit notes. Returns aggregate 0-30, 31-60, 61-90, 90+ and missing-due-date buckets; it intentionally does not return an unbounded invoice list.",
|
|
38652
|
+
// Equal to the MCP package's READ_ONLY_ANNOTATIONS (tool fingerprint).
|
|
38653
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
38654
|
+
};
|
|
38655
|
+
var NUMERIC_FIELDS = [
|
|
38656
|
+
"bucket_0_30_count",
|
|
38657
|
+
"bucket_0_30_amount",
|
|
38658
|
+
"bucket_31_60_count",
|
|
38659
|
+
"bucket_31_60_amount",
|
|
38660
|
+
"bucket_61_90_count",
|
|
38661
|
+
"bucket_61_90_amount",
|
|
38662
|
+
"bucket_90_plus_count",
|
|
38663
|
+
"bucket_90_plus_amount",
|
|
38664
|
+
"no_due_date_count",
|
|
38665
|
+
"no_due_date_amount",
|
|
38666
|
+
"total_count",
|
|
38667
|
+
"total_receivables",
|
|
38668
|
+
"weighted_average_days_overdue"
|
|
38669
|
+
];
|
|
38670
|
+
var cents = (amount2) => Math.round(amount2 * 100) / 100;
|
|
38671
|
+
var eur = (amount2) => new Intl.NumberFormat("fi-FI", { style: "currency", currency: "EUR" }).format(amount2);
|
|
38672
|
+
var amount = (count, total2) => ({
|
|
38673
|
+
count,
|
|
38674
|
+
total: cents(total2),
|
|
38675
|
+
total_formatted: eur(total2)
|
|
38676
|
+
});
|
|
38677
|
+
function isRawArAging(data) {
|
|
38678
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
|
|
38679
|
+
const row = data;
|
|
38680
|
+
return typeof row.as_of_date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(row.as_of_date) && NUMERIC_FIELDS.every((field) => {
|
|
38681
|
+
const value = row[field];
|
|
38682
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
38683
|
+
});
|
|
38684
|
+
}
|
|
38685
|
+
function toArAgingReport(data) {
|
|
38686
|
+
if (!isRawArAging(data)) return null;
|
|
38687
|
+
return {
|
|
38688
|
+
as_of_date: data.as_of_date,
|
|
38689
|
+
buckets: [
|
|
38690
|
+
{ label: "0-30 days", ...amount(data.bucket_0_30_count, data.bucket_0_30_amount) },
|
|
38691
|
+
{ label: "31-60 days", ...amount(data.bucket_31_60_count, data.bucket_31_60_amount) },
|
|
38692
|
+
{ label: "61-90 days", ...amount(data.bucket_61_90_count, data.bucket_61_90_amount) },
|
|
38693
|
+
{ label: "90+ days", ...amount(data.bucket_90_plus_count, data.bucket_90_plus_amount) }
|
|
38694
|
+
],
|
|
38695
|
+
no_due_date: amount(data.no_due_date_count, data.no_due_date_amount),
|
|
38696
|
+
total_count: data.total_count,
|
|
38697
|
+
total_receivables: cents(data.total_receivables),
|
|
38698
|
+
total_receivables_formatted: eur(data.total_receivables),
|
|
38699
|
+
weighted_average_days_overdue: data.weighted_average_days_overdue,
|
|
38700
|
+
metric_note: "weighted_average_days_overdue is the amount-weighted age of currently outstanding receivables with a due date; it is not revenue-based DSO."
|
|
38701
|
+
};
|
|
38702
|
+
}
|
|
38703
|
+
|
|
38560
38704
|
// src/compliance/omavero-fields.ts
|
|
38561
38705
|
var OMAVERO_FIELD_NUMBERS = [
|
|
38562
38706
|
"301",
|
|
@@ -38597,7 +38741,7 @@ var LEGACY_ALV_SUMMARY_KEYS = [
|
|
|
38597
38741
|
"reverse_charge_base",
|
|
38598
38742
|
"omavero"
|
|
38599
38743
|
];
|
|
38600
|
-
function
|
|
38744
|
+
function cents2(value) {
|
|
38601
38745
|
return Math.round(value * 100) / 100;
|
|
38602
38746
|
}
|
|
38603
38747
|
function num(value) {
|
|
@@ -38622,7 +38766,7 @@ function omaveroFieldsFromSummary(s) {
|
|
|
38622
38766
|
"314": num(s.field_309_base),
|
|
38623
38767
|
"318": num(s.output_vat_construction_scrap),
|
|
38624
38768
|
"319": num(s.reverse_charge_sales_base),
|
|
38625
|
-
"320":
|
|
38769
|
+
"320": cents2(num(s.field_310_base) + num(s.field_313_base))
|
|
38626
38770
|
};
|
|
38627
38771
|
const map2 = s.omavero;
|
|
38628
38772
|
if (map2 && typeof map2 === "object") {
|
|
@@ -38814,8 +38958,8 @@ function alvReconciliationWarning(reconciliation) {
|
|
|
38814
38958
|
if (!reconciliation || typeof reconciliation !== "object") return void 0;
|
|
38815
38959
|
const r = reconciliation;
|
|
38816
38960
|
if (r.status !== "mismatch" && r.status !== "explained") return void 0;
|
|
38817
|
-
const
|
|
38818
|
-
return `VAT amounts or the audit trail need review (output VAT difference ${
|
|
38961
|
+
const amount2 = (value) => (Number(value) || 0).toFixed(2);
|
|
38962
|
+
return `VAT amounts or the audit trail need review (output VAT difference ${amount2(r.output_difference)} \u20AC, input VAT difference ${amount2(r.input_difference)} \u20AC). Equal amounts do not link a manual VAT row to a voucher. Review reconciliation before filing; do not duplicate the purchase expense.`;
|
|
38819
38963
|
}
|
|
38820
38964
|
var BALANCE_SHEET_ROW_SHAPE = external_exports.object({
|
|
38821
38965
|
account_number: external_exports.string(),
|
|
@@ -39167,12 +39311,12 @@ function registerReportTools(server2) {
|
|
|
39167
39311
|
}
|
|
39168
39312
|
);
|
|
39169
39313
|
server2.registerTool(
|
|
39170
|
-
|
|
39314
|
+
AR_AGING_TOOL.name,
|
|
39171
39315
|
{
|
|
39172
|
-
description:
|
|
39316
|
+
description: AR_AGING_TOOL.description,
|
|
39173
39317
|
inputSchema: {},
|
|
39174
39318
|
outputSchema: AR_AGING_OUTPUT_SHAPE,
|
|
39175
|
-
annotations:
|
|
39319
|
+
annotations: AR_AGING_TOOL.annotations
|
|
39176
39320
|
},
|
|
39177
39321
|
async () => {
|
|
39178
39322
|
const supabase2 = getSupabase();
|
|
@@ -39183,32 +39327,8 @@ function registerReportTools(server2) {
|
|
|
39183
39327
|
if (error61) {
|
|
39184
39328
|
return toErrorContent(sanitizeDbError("Failed to get AR aging", error61));
|
|
39185
39329
|
}
|
|
39186
|
-
const
|
|
39187
|
-
|
|
39188
|
-
{ label: "0-30 days", count: aging.bucket_0_30_count, total: aging.bucket_0_30_amount },
|
|
39189
|
-
{ label: "31-60 days", count: aging.bucket_31_60_count, total: aging.bucket_31_60_amount },
|
|
39190
|
-
{ label: "61-90 days", count: aging.bucket_61_90_count, total: aging.bucket_61_90_amount },
|
|
39191
|
-
{ label: "90+ days", count: aging.bucket_90_plus_count, total: aging.bucket_90_plus_amount }
|
|
39192
|
-
];
|
|
39193
|
-
return toResult({
|
|
39194
|
-
as_of_date: aging.as_of_date,
|
|
39195
|
-
buckets: buckets.map((b) => ({
|
|
39196
|
-
label: b.label,
|
|
39197
|
-
count: b.count,
|
|
39198
|
-
total: roundCents(b.total),
|
|
39199
|
-
total_formatted: formatCurrency(b.total)
|
|
39200
|
-
})),
|
|
39201
|
-
no_due_date: {
|
|
39202
|
-
count: aging.no_due_date_count,
|
|
39203
|
-
total: roundCents(aging.no_due_date_amount),
|
|
39204
|
-
total_formatted: formatCurrency(aging.no_due_date_amount)
|
|
39205
|
-
},
|
|
39206
|
-
total_count: aging.total_count,
|
|
39207
|
-
total_receivables: roundCents(aging.total_receivables),
|
|
39208
|
-
total_receivables_formatted: formatCurrency(aging.total_receivables),
|
|
39209
|
-
weighted_average_days_overdue: aging.weighted_average_days_overdue,
|
|
39210
|
-
metric_note: "weighted_average_days_overdue is the amount-weighted age of currently outstanding receivables with a due date; it is not revenue-based DSO."
|
|
39211
|
-
});
|
|
39330
|
+
const report = toArAgingReport(data);
|
|
39331
|
+
return report ? toResult(report) : toErrorContent("Failed to get AR aging. Please try again.");
|
|
39212
39332
|
}
|
|
39213
39333
|
);
|
|
39214
39334
|
server2.registerTool(
|
|
@@ -39340,7 +39460,7 @@ function registerReportTools(server2) {
|
|
|
39340
39460
|
const warnings = [
|
|
39341
39461
|
alvReconciliationWarning(raw.reconciliation),
|
|
39342
39462
|
Number(raw.unclassified_legacy_reverse_charge_base) ? LEGACY_REVERSE_CHARGE_WARNING : void 0
|
|
39343
|
-
].filter((
|
|
39463
|
+
].filter((text2) => Boolean(text2));
|
|
39344
39464
|
return toResult({
|
|
39345
39465
|
...safeResult,
|
|
39346
39466
|
...warnings.length > 0 && { warning: warnings.join(" ") },
|
|
@@ -40427,12 +40547,12 @@ async function validateAccountOverrides(overrides) {
|
|
|
40427
40547
|
return { ok: true };
|
|
40428
40548
|
}
|
|
40429
40549
|
function saleCashTemplate(revenueAccount, vatRate) {
|
|
40430
|
-
return (
|
|
40431
|
-
const { net, vat: vat2 } = splitVat(
|
|
40550
|
+
return (amount2, opts) => {
|
|
40551
|
+
const { net, vat: vat2 } = splitVat(amount2, vatRate);
|
|
40432
40552
|
const entries = [
|
|
40433
40553
|
{
|
|
40434
40554
|
accountNumber: ACCOUNTS.KASSA,
|
|
40435
|
-
debitAmount: roundCents(
|
|
40555
|
+
debitAmount: roundCents(amount2),
|
|
40436
40556
|
creditAmount: 0
|
|
40437
40557
|
},
|
|
40438
40558
|
{ accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
|
|
@@ -40448,12 +40568,12 @@ function saleCashTemplate(revenueAccount, vatRate) {
|
|
|
40448
40568
|
};
|
|
40449
40569
|
}
|
|
40450
40570
|
function saleBankTemplate(revenueAccount, vatRate) {
|
|
40451
|
-
return (
|
|
40452
|
-
const { net, vat: vat2 } = splitVat(
|
|
40571
|
+
return (amount2, opts) => {
|
|
40572
|
+
const { net, vat: vat2 } = splitVat(amount2, vatRate);
|
|
40453
40573
|
const entries = [
|
|
40454
40574
|
{
|
|
40455
40575
|
accountNumber: ACCOUNTS.PANKKITILI,
|
|
40456
|
-
debitAmount: roundCents(
|
|
40576
|
+
debitAmount: roundCents(amount2),
|
|
40457
40577
|
creditAmount: 0
|
|
40458
40578
|
},
|
|
40459
40579
|
{ accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
|
|
@@ -40526,13 +40646,13 @@ var TEMPLATES = [
|
|
|
40526
40646
|
kind: "sale",
|
|
40527
40647
|
description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
|
|
40528
40648
|
vatRate: 25.5,
|
|
40529
|
-
expand: (
|
|
40530
|
-
const { net, vat: vat2 } = splitVat(
|
|
40649
|
+
expand: (amount2, opts) => {
|
|
40650
|
+
const { net, vat: vat2 } = splitVat(amount2, 25.5);
|
|
40531
40651
|
return applyOverrides(
|
|
40532
40652
|
[
|
|
40533
40653
|
{
|
|
40534
40654
|
accountNumber: ACCOUNTS.MYYNTISAAMISET,
|
|
40535
|
-
debitAmount: roundCents(
|
|
40655
|
+
debitAmount: roundCents(amount2),
|
|
40536
40656
|
creditAmount: 0
|
|
40537
40657
|
},
|
|
40538
40658
|
{ accountNumber: ACCOUNTS.MYYNTI_25_5, debitAmount: 0, creditAmount: net },
|
|
@@ -40548,8 +40668,8 @@ var TEMPLATES = [
|
|
|
40548
40668
|
kind: "purchase",
|
|
40549
40669
|
description: "Osto ALV 25,5% (pankkitililt\xE4)",
|
|
40550
40670
|
vatRate: 25.5,
|
|
40551
|
-
expand: (
|
|
40552
|
-
const { net, vat: vat2 } = splitVat(
|
|
40671
|
+
expand: (amount2, opts) => {
|
|
40672
|
+
const { net, vat: vat2 } = splitVat(amount2, 25.5);
|
|
40553
40673
|
return applyOverrides(
|
|
40554
40674
|
[
|
|
40555
40675
|
{ accountNumber: ACCOUNTS.OSTOT, debitAmount: net, creditAmount: 0 },
|
|
@@ -40557,7 +40677,7 @@ var TEMPLATES = [
|
|
|
40557
40677
|
{
|
|
40558
40678
|
accountNumber: ACCOUNTS.PANKKITILI,
|
|
40559
40679
|
debitAmount: 0,
|
|
40560
|
-
creditAmount: roundCents(
|
|
40680
|
+
creditAmount: roundCents(amount2)
|
|
40561
40681
|
}
|
|
40562
40682
|
],
|
|
40563
40683
|
opts?.accountOverrides
|
|
@@ -40569,8 +40689,8 @@ var TEMPLATES = [
|
|
|
40569
40689
|
kind: "purchase",
|
|
40570
40690
|
description: "Ulkopuolinen palvelu ALV 25,5%",
|
|
40571
40691
|
vatRate: 25.5,
|
|
40572
|
-
expand: (
|
|
40573
|
-
const { net, vat: vat2 } = splitVat(
|
|
40692
|
+
expand: (amount2, opts) => {
|
|
40693
|
+
const { net, vat: vat2 } = splitVat(amount2, 25.5);
|
|
40574
40694
|
return applyOverrides(
|
|
40575
40695
|
[
|
|
40576
40696
|
{ accountNumber: ACCOUNTS.OSTOT_PALVELUT, debitAmount: net, creditAmount: 0 },
|
|
@@ -40578,7 +40698,7 @@ var TEMPLATES = [
|
|
|
40578
40698
|
{
|
|
40579
40699
|
accountNumber: ACCOUNTS.PANKKITILI,
|
|
40580
40700
|
debitAmount: 0,
|
|
40581
|
-
creditAmount: roundCents(
|
|
40701
|
+
creditAmount: roundCents(amount2)
|
|
40582
40702
|
}
|
|
40583
40703
|
],
|
|
40584
40704
|
opts?.accountOverrides
|
|
@@ -40591,17 +40711,17 @@ var TEMPLATES = [
|
|
|
40591
40711
|
kind: "other",
|
|
40592
40712
|
description: "Vuokranmaksu (pankkitililt\xE4)",
|
|
40593
40713
|
vatRate: null,
|
|
40594
|
-
expand: (
|
|
40714
|
+
expand: (amount2, opts) => applyOverrides(
|
|
40595
40715
|
[
|
|
40596
40716
|
{
|
|
40597
40717
|
accountNumber: ACCOUNTS.TOIMITILAVUOKRAT,
|
|
40598
|
-
debitAmount: roundCents(
|
|
40718
|
+
debitAmount: roundCents(amount2),
|
|
40599
40719
|
creditAmount: 0
|
|
40600
40720
|
},
|
|
40601
40721
|
{
|
|
40602
40722
|
accountNumber: ACCOUNTS.PANKKITILI,
|
|
40603
40723
|
debitAmount: 0,
|
|
40604
|
-
creditAmount: roundCents(
|
|
40724
|
+
creditAmount: roundCents(amount2)
|
|
40605
40725
|
}
|
|
40606
40726
|
],
|
|
40607
40727
|
opts?.accountOverrides
|
|
@@ -40613,17 +40733,17 @@ var TEMPLATES = [
|
|
|
40613
40733
|
kind: "other",
|
|
40614
40734
|
description: "Tilisiirto kassasta pankkitilille",
|
|
40615
40735
|
vatRate: null,
|
|
40616
|
-
expand: (
|
|
40736
|
+
expand: (amount2, opts) => applyOverrides(
|
|
40617
40737
|
[
|
|
40618
40738
|
{
|
|
40619
40739
|
accountNumber: ACCOUNTS.PANKKITILI,
|
|
40620
|
-
debitAmount: roundCents(
|
|
40740
|
+
debitAmount: roundCents(amount2),
|
|
40621
40741
|
creditAmount: 0
|
|
40622
40742
|
},
|
|
40623
40743
|
{
|
|
40624
40744
|
accountNumber: ACCOUNTS.KASSA,
|
|
40625
40745
|
debitAmount: 0,
|
|
40626
|
-
creditAmount: roundCents(
|
|
40746
|
+
creditAmount: roundCents(amount2)
|
|
40627
40747
|
}
|
|
40628
40748
|
],
|
|
40629
40749
|
opts?.accountOverrides
|
|
@@ -40646,17 +40766,17 @@ function isVatPurchaseTemplate(templateId) {
|
|
|
40646
40766
|
function availableTemplates(alvRegistered) {
|
|
40647
40767
|
return alvRegistered ? TEMPLATES.filter((t) => t.kind !== "sale") : TEMPLATES.filter((t) => !isVatPurchaseTemplate(t.id));
|
|
40648
40768
|
}
|
|
40649
|
-
function expandTemplate(templateId,
|
|
40769
|
+
function expandTemplate(templateId, amount2, opts) {
|
|
40650
40770
|
const template = TEMPLATE_MAP.get(templateId);
|
|
40651
40771
|
if (!template) {
|
|
40652
40772
|
return {
|
|
40653
40773
|
error: `Unknown template "${templateId}". Use kirjapro_list_templates to see available templates.`
|
|
40654
40774
|
};
|
|
40655
40775
|
}
|
|
40656
|
-
if (
|
|
40776
|
+
if (amount2 <= 0) {
|
|
40657
40777
|
return { error: "Amount must be positive." };
|
|
40658
40778
|
}
|
|
40659
|
-
const entries = template.expand(
|
|
40779
|
+
const entries = template.expand(amount2, opts);
|
|
40660
40780
|
return { entries };
|
|
40661
40781
|
}
|
|
40662
40782
|
async function validateFiscalYearForDate(fiscalYearId, transactionDate) {
|
|
@@ -41305,8 +41425,8 @@ function registerBookingTools(server2) {
|
|
|
41305
41425
|
async (params) => {
|
|
41306
41426
|
const execute = async () => {
|
|
41307
41427
|
const rate = Number(params.vatRate);
|
|
41308
|
-
const
|
|
41309
|
-
if (
|
|
41428
|
+
const amount2 = roundCents(params.amount);
|
|
41429
|
+
if (amount2 <= 0) return toErrorContent("Oston summan on oltava v\xE4hint\xE4\xE4n yksi sentti.");
|
|
41310
41430
|
const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
|
|
41311
41431
|
if ("isError" in companyVat) return companyVat;
|
|
41312
41432
|
if (params.reverseCharge && !params.reverseChargeCategory) return toErrorContent(REVERSE_CHARGE_CATEGORY_REQUIRED);
|
|
@@ -41326,7 +41446,7 @@ function registerBookingTools(server2) {
|
|
|
41326
41446
|
if (basisError) return toErrorContent(basisError);
|
|
41327
41447
|
}
|
|
41328
41448
|
const basis = params.vatRateBasis;
|
|
41329
|
-
const { net, vat: vat2 } = params.reverseCharge ? { net:
|
|
41449
|
+
const { net, vat: vat2 } = params.reverseCharge ? { net: amount2, vat: roundCents(amount2 * rate / 100) } : splitVat(amount2, rate);
|
|
41330
41450
|
const allocation = allocatePurchaseVat(net, vat2, percent);
|
|
41331
41451
|
const keepFacts = rate > 0 && (params.reverseCharge || companyVat.alvRegistered && !params.edustus);
|
|
41332
41452
|
const vatAllocation = percent > 0 && percent < 100 ? {
|
|
@@ -41352,13 +41472,13 @@ function registerBookingTools(server2) {
|
|
|
41352
41472
|
{ accountNumber: params.expenseAccount, debitAmount: expense, creditAmount: 0 },
|
|
41353
41473
|
{ accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: allocation.deductibleVat, creditAmount: 0 },
|
|
41354
41474
|
...params.reverseCharge ? [{ accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }] : [],
|
|
41355
|
-
...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net :
|
|
41475
|
+
...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net : amount2 }] : []
|
|
41356
41476
|
].filter((entry) => entry.debitAmount !== 0 || entry.creditAmount !== 0);
|
|
41357
41477
|
if (entries.length < 2) return toErrorContent("Tositteen veron on oltava v\xE4hint\xE4\xE4n yksi sentti.");
|
|
41358
41478
|
const purchaseAccountWarning = expenseAccountWarning(params.expenseAccount);
|
|
41359
41479
|
const calculation = {
|
|
41360
41480
|
amount_basis: assessmentOnly ? "customs_tax_base" : params.reverseCharge ? "supplier_net" : "supplier_gross",
|
|
41361
|
-
...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net :
|
|
41481
|
+
...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net : amount2 },
|
|
41362
41482
|
tax_base: net,
|
|
41363
41483
|
output_vat: params.reverseCharge ? vat2 : 0,
|
|
41364
41484
|
source_vat: vat2,
|
|
@@ -41369,7 +41489,7 @@ function registerBookingTools(server2) {
|
|
|
41369
41489
|
};
|
|
41370
41490
|
if (params.dryRun) return toDryRunResponse("kirjapro_book_purchase", {
|
|
41371
41491
|
action: "book_purchase",
|
|
41372
|
-
gross:
|
|
41492
|
+
gross: amount2,
|
|
41373
41493
|
vat_rate: params.vatRate,
|
|
41374
41494
|
description: params.description,
|
|
41375
41495
|
date: params.date,
|
|
@@ -43806,7 +43926,7 @@ function registerPrompts(mcpServer) {
|
|
|
43806
43926
|
} else {
|
|
43807
43927
|
fyBlock = "Ei avointa tilikautta \u2014 luo tilikausi ennen kirjausten tekemist\xE4.";
|
|
43808
43928
|
}
|
|
43809
|
-
const
|
|
43929
|
+
const text2 = [
|
|
43810
43930
|
`T\xE4m\xE4 on Kirjapro MCP -palvelin (v${VERSION}). Yhdistetty yritys:`,
|
|
43811
43931
|
"",
|
|
43812
43932
|
companyBlock,
|
|
@@ -43824,7 +43944,7 @@ function registerPrompts(mcpServer) {
|
|
|
43824
43944
|
messages: [
|
|
43825
43945
|
{
|
|
43826
43946
|
role: "user",
|
|
43827
|
-
content: { type: "text", text }
|
|
43947
|
+
content: { type: "text", text: text2 }
|
|
43828
43948
|
}
|
|
43829
43949
|
]
|
|
43830
43950
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirjapro/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "MCP server for Finnish accounting — exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude, ChatGPT, Mistral, and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@kirjapro/mcp-server",
|
|
3
3
|
"displayName": "Kirjapro - Finnish Accounting",
|
|
4
4
|
"description": "Finnish double-entry bookkeeping, invoicing, and financial reporting for growing businesses. Create transactions, generate invoices with VAT calculation, produce income statements and balance sheets, and validate Finnish business identifiers.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.14.0",
|
|
6
6
|
"homepage": "https://kirjapro.fi",
|
|
7
7
|
"repository": "https://github.com/Jaollo/kirja-pro",
|
|
8
8
|
"license": "MIT",
|