@kirjapro/mcp-server 0.13.0 → 0.14.1

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 CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.14.1] - 2026-10-05
6
+
7
+ Uses the database migration `20261005210000` (AI-työtila 2.0 round 4, batch 7).
8
+ Against a database without it the refusal keeps the 0.14.0 text. 0.14.0 keeps
9
+ working against the new database and reports such a key as "Permission denied."
10
+ Tool definitions are unchanged.
11
+
12
+ - A revoked or expired API key, or one whose creator has since left (or been removed
13
+ from and re-invited to) the company, now fails with `CREDENTIAL_INVALID: the API
14
+ key is no longer valid ...` instead of "MFA step-up required"; `kirjapro_create_invoice`
15
+ returns the code `CREDENTIAL_INVALID` instead of `MFA_REQUIRED`. Needs the
16
+ `20261005210000` migration; against an older database the old text stays.
17
+
18
+ ## [0.14.0] - 2026-10-05
19
+
20
+ Published after (1) `db:deploy` of the round-3 migrations
21
+ `20261005170000`–`20261005200000` (AI-työtila 2.0 batches 10–13;
22
+ `kirjapro_send_invoice` needs `20261005180100`, the delivery intents), (2) the
23
+ `dispatch-invoice` deploy from the same main, which resumes a delivery under its
24
+ intent, and (3) the production-generated `src/database.types.ts`. 0.13.0 keeps
25
+ working unchanged against the same database.
26
+
27
+ - `kirjapro_get_ar_aging` shares its result shaping with Kirjapro; an unexpected
28
+ report payload now returns a sanitized error instead of an internal exception.
29
+ - `kirjapro_create_invoice`: `paymentTermDays` has no schema default any more, so
30
+ the customer's own payment term applies before 14 days (0.13.0 always used 14).
31
+ `vatRate` defaults to `25.5` only when a line has no `vatTreatmentCode`; an
32
+ explicit rate that contradicts the code is refused as `VAT_RATE_MISMATCH`
33
+ instead of the code winning silently. Lines take `discountPercent` (0–100, two
34
+ decimals, before VAT), and the historical `24`/`14` rates
35
+ (`legacy_standard_24` until 31.8.2024, `legacy_standard_14` until 31.12.2025)
36
+ are accepted inside their windows. Fingerprint changes.
37
+ - `kirjapro_send_invoice` caches only a confirmed result. A lost transport, relay
38
+ error, 5xx or a delivery whose provider outcome is still pending returns
39
+ `IDEMPOTENCY_OUTCOME_UNKNOWN` and leaves the claim pending; 0.13.0 cached it as a
40
+ failure, which invited a retry with a new key. A confirmed refusal returns
41
+ `CODE: viesti` and a send returns `delivery_id`. Needs the database migration
42
+ `20261005180100` and the matching `dispatch-invoice` deploy (delivery intents).
43
+ - The generated copies `src/compliance/purchase-vat-allocation.ts`,
44
+ `src/compliance/date-local.ts` and the new `src/shared/invoice-delivery.ts` are
45
+ byte-guarded by the test suite like `src/shared/invoice-draft.ts`.
46
+ - `kirjapro_search_transactions` with `minAmount` or `maxAmount` returns
47
+ `FILTER_TOO_BROAD: …` when more than 1 000 transactions (or an unknown number)
48
+ match the other filters, instead of filtering only the first 1 000 and
49
+ reporting that sample's size as `total_count`. Up to 1 000 matches the count
50
+ stays exact. Narrow `dateFrom`/`dateTo` or `query` to search further.
51
+
3
52
  ## [0.13.0] - 2026-10-04
4
53
 
5
54
  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((text, msg) => text + separator + msg);
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 = (text) => {
19923
- return text.charAt(0).toUpperCase() + text.slice(1);
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.13.0";
35682
+ var VERSION = "0.14.1";
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 text = (textPart?.text ?? "").replace(/^MCP error -?\d+: /, "");
36021
- if (text.startsWith("Input validation error:")) return "INVALID_ARGUMENTS";
36022
- if (text.startsWith("Output validation error:")) {
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 (text.startsWith("Tool ") && text.endsWith(" not found")) {
36025
+ if (text2.startsWith("Tool ") && text2.endsWith(" not found")) {
36026
36026
  return "UNKNOWN_TOOL";
36027
36027
  }
36028
- if (text.startsWith("Tool ") && text.endsWith(" disabled")) {
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(amount) {
36220
+ function formatCurrency(amount2) {
36221
36221
  return new Intl.NumberFormat("fi-FI", {
36222
36222
  style: "currency",
36223
36223
  currency: "EUR"
36224
- }).format(amount);
36224
+ }).format(amount2);
36225
36225
  }
36226
36226
  function helsinkiDateString(now = /* @__PURE__ */ new Date()) {
36227
36227
  const parts = new Intl.DateTimeFormat("fi-FI", {
@@ -36325,6 +36325,9 @@ function sanitizeDbError(context, error61) {
36325
36325
  details: error61.details,
36326
36326
  hint: error61.hint
36327
36327
  });
36328
+ if (error61.message?.startsWith("CREDENTIAL_INVALID")) {
36329
+ return `${context}: CREDENTIAL_INVALID: the API key is no longer valid (revoked, expired or its creator left the company). Create a new key in Kirjapro.`;
36330
+ }
36328
36331
  if (error61.message?.startsWith("AAL2_REQUIRED")) {
36329
36332
  return `${context}: MFA step-up required. Verify your 2FA code in Kirjapro and retry.`;
36330
36333
  }
@@ -36363,8 +36366,8 @@ async function invokeEdgeFunction(supabase2, name, body) {
36363
36366
  }
36364
36367
  return { data, error: null };
36365
36368
  }
36366
- function roundCents(amount) {
36367
- return Math.round(amount * 100) / 100;
36369
+ function roundCents(amount2) {
36370
+ return Math.round(amount2 * 100) / 100;
36368
36371
  }
36369
36372
  var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
36370
36373
  var CONTROL = /[\u0000-\u001F\u007F]/g;
@@ -36818,6 +36821,13 @@ function registerTransactionTools(server2) {
36818
36821
  "Failed to search transactions. Check your filters and try again."
36819
36822
  );
36820
36823
  }
36824
+ const knownCount = Number.isSafeInteger(count) ? count : null;
36825
+ if (hasAmountFilter && (knownCount === null || knownCount > AMOUNT_FILTER_CAP)) {
36826
+ const matches = knownCount !== null ? `${knownCount} match the other filters` : "the number matching the other filters is unknown";
36827
+ return toErrorContent(
36828
+ `FILTER_TOO_BROAD: Amount filters read at most ${AMOUNT_FILTER_CAP} transactions, and ${matches}. Narrow dateFrom/dateTo or query; no partial count is returned.`
36829
+ );
36830
+ }
36821
36831
  let results = (data || []).map(
36822
36832
  (t) => {
36823
36833
  const totalDebit = roundCents(
@@ -36858,10 +36868,9 @@ function registerTransactionTools(server2) {
36858
36868
  }
36859
36869
  const filteredTotal = results.length;
36860
36870
  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
36871
  return toResult({
36863
36872
  ...paginate(paginatedResults, filteredTotal, offset, limit),
36864
- amount_filter_note: `Filtered client-side from ${count || 0} transactions${cappedWarning}`
36873
+ amount_filter_note: `Filtered client-side from ${count} transactions`
36865
36874
  });
36866
36875
  }
36867
36876
  return toResult(paginate(results, count || 0, offset, limit));
@@ -37312,15 +37321,17 @@ var INVOICE_VAT_TREATMENT_CODES = [
37312
37321
  "exempt_education",
37313
37322
  "exempt_financial",
37314
37323
  "exempt_insurance",
37315
- "exempt_other"
37324
+ "exempt_other",
37325
+ "legacy_standard_24",
37326
+ "legacy_standard_14"
37316
37327
  ];
37317
- var INVOICE_VAT_RATES = ["25.5", "13.5", "10", "0"];
37328
+ var INVOICE_VAT_RATES = ["25.5", "13.5", "10", "0", "24", "14"];
37318
37329
  var INVOICE_DRAFT_MAX_LINES = 100;
37319
37330
  var INVOICE_DRAFT_CUSTOMER_COLUMNS = "id, name, payment_term_days, customer_kind, business_id, vat_number, country";
37320
37331
  var ERROR_MESSAGES = {
37321
37332
  INVALID_INPUT: (detail) => `Laskun tiedoissa on virhe: ${detail}.`,
37322
37333
  CUSTOMER_NOT_FOUND: (detail) => `Asiakasta ei l\xF6ydy t\xE4st\xE4 yrityksest\xE4${detail ? ` (${detail})` : ""}.`,
37323
- CUSTOMER_AMBIGUOUS: (detail) => `Useita asiakkaita osuu hakuun: ${detail}. Tarkenna nime\xE4.`,
37334
+ CUSTOMER_AMBIGUOUS: (detail) => `Asiakas pit\xE4\xE4 valita t\xE4sm\xE4llisesti: ${detail}. K\xE4yt\xE4 t\xE4sm\xE4llist\xE4 nime\xE4 tai asiakasnumeroa.`,
37324
37335
  LOOKUP_FAILED: () => "Laskun tietojen haku ep\xE4onnistui. Mit\xE4\xE4n ei tallennettu; voit yritt\xE4\xE4 uudelleen.",
37325
37336
  FISCAL_YEAR_REQUIRED: () => "Ei tilikautta valittuna.",
37326
37337
  FISCAL_YEAR_NOT_FOUND: () => "Tilikautta ei l\xF6ydy. Tarkista valinta.",
@@ -37329,6 +37340,7 @@ var ERROR_MESSAGES = {
37329
37340
  VAT_SELLER_NOT_REGISTERED: () => "ALV-rekisteriin kuulumaton myyj\xE4 voi k\xE4ytt\xE4\xE4 vain Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
37330
37341
  VAT_SELLER_REGISTERED: () => "ALV-rekisteriss\xE4 oleva myyj\xE4 ei voi k\xE4ytt\xE4\xE4 Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
37331
37342
  VAT_RATE_NOT_IN_FORCE: (detail) => `ALV-kanta ${detail}.`,
37343
+ VAT_RATE_MISMATCH: (detail) => `ALV-kanta ja ALV-k\xE4sittely ovat ristiriidassa (${detail}). Anna vain k\xE4sittely tai sit\xE4 vastaava kanta.`,
37332
37344
  VAT_REQUIRES_BUSINESS_CUSTOMER: () => "Valittu ALV-k\xE4sittely vaatii yritysasiakkaan.",
37333
37345
  VAT_REQUIRES_FINNISH_BUSINESS_ID: () => "Kotimainen k\xE4\xE4nnetty verovelvollisuus vaatii ostajan kelvollisen Y-tunnuksen.",
37334
37346
  VAT_REQUIRES_EU_VAT_NUMBER: () => "EU-myynti vaatii muun EU-maan maakoodin ja sit\xE4 vastaavan ostajan ALV-tunnisteen.",
@@ -37336,6 +37348,7 @@ var ERROR_MESSAGES = {
37336
37348
  INVOICE_NUMBER_CONFLICT: () => "Laskunumero on jo k\xE4yt\xF6ss\xE4 (samanaikainen luonti). Yrit\xE4 uudelleen.",
37337
37349
  INVALID_LINE: () => "Laskurivi ei kelpaa. Tarkista m\xE4\xE4r\xE4, hinta ja ALV-k\xE4sittely.",
37338
37350
  PERMISSION_DENIED: () => "Ei oikeutta luoda laskua t\xE4lle yritykselle.",
37351
+ CREDENTIAL_INVALID: () => "Tunniste ei en\xE4\xE4 kelpaa: API-avain on peruttu tai vanhentunut. Luo uusi avain Kirjaprossa.",
37339
37352
  MFA_REQUIRED: () => "Vahvista kaksivaiheinen tunnistautuminen Kirjaprossa ja yrit\xE4 sitten uudelleen.",
37340
37353
  WRITE_REJECTED: () => "Laskun luonti hyl\xE4ttiin. Mit\xE4\xE4n ei tallennettu.",
37341
37354
  OUTCOME_UNKNOWN: () => "Laskun luonnin lopputulosta ei voitu varmistaa. Tarkista laskulista ennen uutta yrityst\xE4."
@@ -37353,9 +37366,9 @@ function roundNumericMoney(value) {
37353
37366
  const digits = Math.abs(value).toPrecision(15);
37354
37367
  if (digits.includes("e")) return 0;
37355
37368
  const [whole = "0", fraction = ""] = digits.split(".");
37356
- const cents2 = Number(whole) * 100 + Number(fraction.slice(0, 2).padEnd(2, "0")) + (Number(fraction.charAt(2) || "0") >= 5 ? 1 : 0);
37357
- if (cents2 === 0) return 0;
37358
- return value < 0 ? -cents2 / 100 : cents2 / 100;
37369
+ const cents3 = Number(whole) * 100 + Number(fraction.slice(0, 2).padEnd(2, "0")) + (Number(fraction.charAt(2) || "0") >= 5 ? 1 : 0);
37370
+ if (cents3 === 0) return 0;
37371
+ return value < 0 ? -cents3 / 100 : cents3 / 100;
37359
37372
  }
37360
37373
  function calculateLineTotals(quantity, unitPrice, vatRate, priceEntryMode = "vat_exclusive", discountPercent = 0) {
37361
37374
  const discountFactor = 1 - discountPercent / 100;
@@ -37399,17 +37412,24 @@ function addDaysIso(date5, days) {
37399
37412
  var TREATMENT_RATES = {
37400
37413
  standard_25_5: 25.5,
37401
37414
  standard_13_5: 13.5,
37402
- standard_10: 10
37415
+ standard_10: 10,
37416
+ legacy_standard_24: 24,
37417
+ legacy_standard_14: 14
37403
37418
  };
37404
37419
  var STANDARD_TREATMENTS = {
37405
37420
  "25.5": "standard_25_5",
37406
37421
  "13.5": "standard_13_5",
37407
- "10": "standard_10"
37422
+ "10": "standard_10",
37423
+ "24": "legacy_standard_24",
37424
+ "14": "legacy_standard_14"
37408
37425
  };
37409
37426
  function resolveInvoiceVatTreatment(vatRate, code) {
37410
37427
  const resolved = code ?? (vatRate === void 0 ? void 0 : STANDARD_TREATMENTS[vatRate]);
37411
37428
  if (!resolved) return invoiceDraftError("VAT_TREATMENT_REQUIRED");
37412
- return { code: resolved, rate: TREATMENT_RATES[resolved] ?? 0 };
37429
+ const rate = TREATMENT_RATES[resolved] ?? 0;
37430
+ if (vatRate !== void 0 && Number(vatRate) !== rate)
37431
+ return invoiceDraftError("VAT_RATE_MISMATCH", `${vatRate} % ja ${resolved}`);
37432
+ return { code: resolved, rate };
37413
37433
  }
37414
37434
  var EU_COUNTRY_CODES = [
37415
37435
  "AT",
@@ -37461,6 +37481,10 @@ function invoiceVatTreatmentError(code, sellerAlvRegistered, customer, documentD
37461
37481
  return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "25,5 % ei ole voimassa ennen 1.9.2024");
37462
37482
  if (code === "standard_13_5" && documentDate < "2026-01-01")
37463
37483
  return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "13,5 % ei ole voimassa ennen 1.1.2026");
37484
+ if (code === "legacy_standard_24" && documentDate > "2024-08-31")
37485
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "24 % on voimassa vain 31.8.2024 asti");
37486
+ if (code === "legacy_standard_14" && documentDate > "2025-12-31")
37487
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "14 % on voimassa vain 31.12.2025 asti");
37464
37488
  if ((code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu") && customer.customer_kind !== "business")
37465
37489
  return invoiceDraftError("VAT_REQUIRES_BUSINESS_CUSTOMER");
37466
37490
  if ((code === "reverse_construction" || code === "reverse_scrap") && !isFinnishBusinessId(customer.business_id ?? ""))
@@ -37496,6 +37520,9 @@ function inputError(input2) {
37496
37520
  if (!Number.isFinite(line.unitPrice) || line.unitPrice < 0 || line.unitPrice > 1e9)
37497
37521
  return `${row}: yksikk\xF6hinnan pit\xE4\xE4 olla 0\u20131 000 000 000 \u20AC`;
37498
37522
  if (line.quantity * line.unitPrice > 1e12) return `${row}: rivin summa on liian suuri`;
37523
+ const discount = line.discountPercent;
37524
+ if (discount !== void 0 && (!Number.isFinite(discount) || discount < 0 || discount > 100 || Math.abs(discount * 100 - Math.round(discount * 100)) > 1e-9))
37525
+ return `${row}: alennuksen pit\xE4\xE4 olla 0\u2013100 % enint\xE4\xE4n kahdella desimaalilla`;
37499
37526
  }
37500
37527
  return null;
37501
37528
  }
@@ -37514,11 +37541,13 @@ function prepareInvoiceDraft(input2, facts) {
37514
37541
  input2.invoiceDate
37515
37542
  );
37516
37543
  if (ineligible) return { ok: false, error: ineligible };
37544
+ const discountPercent = line.discountPercent ?? 0;
37517
37545
  const amounts = calculateLineTotals(
37518
37546
  line.quantity,
37519
37547
  line.unitPrice,
37520
37548
  treatment.rate,
37521
- input2.priceEntryMode
37549
+ input2.priceEntryMode,
37550
+ discountPercent
37522
37551
  );
37523
37552
  lines.push({
37524
37553
  line_number: index + 1,
@@ -37527,6 +37556,7 @@ function prepareInvoiceDraft(input2, facts) {
37527
37556
  unit: line.unit,
37528
37557
  unit_price: line.unitPrice,
37529
37558
  entered_unit_price: line.unitPrice,
37559
+ discount_percent: discountPercent,
37530
37560
  vat_rate: treatment.rate,
37531
37561
  vat_treatment_code: treatment.code,
37532
37562
  vat_treatment_overridden: true,
@@ -37583,6 +37613,8 @@ function invoiceDraftWriteOutcome(response) {
37583
37613
  const code = error61.code ?? "";
37584
37614
  if (!CONFIRMED_ERROR_CODE.test(code))
37585
37615
  return { ok: false, error: invoiceDraftError("OUTCOME_UNKNOWN") };
37616
+ if ((error61.message ?? "").startsWith("CREDENTIAL_INVALID"))
37617
+ return { ok: false, error: invoiceDraftError("CREDENTIAL_INVALID") };
37586
37618
  if ((error61.message ?? "").startsWith("AAL2_REQUIRED"))
37587
37619
  return { ok: false, error: invoiceDraftError("MFA_REQUIRED") };
37588
37620
  const mapped = {
@@ -37633,6 +37665,47 @@ function calculateInvoiceBalance(total2, payments, creditedTotal) {
37633
37665
  return { paidTotal, remainingBalance: roundCents(total2 - paidTotal - creditedTotal) };
37634
37666
  }
37635
37667
 
37668
+ // src/shared/invoice-delivery.ts
37669
+ var MESSAGES = {
37670
+ INVOICE_NOT_FOUND: "Laskua ei l\xF6ydy t\xE4st\xE4 yrityksest\xE4. Tarkista laskun numero.",
37671
+ INVOICE_NOT_DRAFT: "Vain luonnostilassa olevan laskun voi l\xE4hett\xE4\xE4. Laskua ei l\xE4hetetty.",
37672
+ DELIVERY_RECIPIENT_MISSING: "Asiakkaalta puuttuu valitun kanavan vastaanottaja (s\xE4hk\xF6postiosoite tai verkkolaskuosoite ja operaattori). Laskua ei l\xE4hetetty.",
37673
+ DELIVERY_CHANGED: "Laskun sis\xE4lt\xF6 tai vastaanottaja muuttui hyv\xE4ksynn\xE4n j\xE4lkeen. Laskua ei l\xE4hetetty; pyyd\xE4 l\xE4hetys uudelleen.",
37674
+ DELIVERY_IN_PROGRESS: "Laskun l\xE4hetys on jo k\xE4sittelyss\xE4 toisessa pyynn\xF6ss\xE4. Tarkista laskun tila ennen uutta yrityst\xE4.",
37675
+ DELIVERY_FAILED: "Laskun l\xE4hetys ep\xE4onnistui. Laskua ei l\xE4hetetty.",
37676
+ DELIVERY_OUTCOME_UNKNOWN: "L\xE4hetyksen tila on ep\xE4varma. \xC4l\xE4 l\xE4het\xE4 laskua uudelleen ennen kuin olet tarkistanut sen tilan.",
37677
+ DELIVERY_UNAVAILABLE: "Laskun l\xE4hetys ei ole k\xE4ytett\xE4viss\xE4 AI-chatissa.",
37678
+ DELIVERY_STOPPED: "L\xE4hetyst\xE4 ei tehty: keskustelun ty\xF6 p\xE4\xE4ttyi tai pys\xE4ytettiin ennen l\xE4hetyst\xE4."
37679
+ };
37680
+ function invoiceDeliveryMessage(code) {
37681
+ return MESSAGES[code];
37682
+ }
37683
+ var KNOWN_CODES = Object.keys(MESSAGES);
37684
+ function invoiceDeliveryErrorCode(value) {
37685
+ return typeof value === "string" && KNOWN_CODES.includes(value) ? value : "DELIVERY_FAILED";
37686
+ }
37687
+ var text = (value) => typeof value === "string" && value ? value : null;
37688
+ function classifyInvoiceDelivery(response) {
37689
+ const body = response.body && typeof response.body === "object" ? response.body : null;
37690
+ const deliveryId = text(body?.deliveryId);
37691
+ const unknown2 = { status: "unknown", deliveryId, errorCode: "DELIVERY_OUTCOME_UNKNOWN" };
37692
+ if (response.httpStatus === 200 && body) {
37693
+ if (body.deliveryStatus === "sent" && body.success === true)
37694
+ return {
37695
+ status: "sent",
37696
+ deliveryId,
37697
+ transmissionId: text(body.transmissionId),
37698
+ messageId: text(body.messageId)
37699
+ };
37700
+ if (body.deliveryStatus === "failed")
37701
+ return { status: "failed", deliveryId, errorCode: invoiceDeliveryErrorCode(body.errorCode) };
37702
+ return unknown2;
37703
+ }
37704
+ if (response.httpStatus !== null && response.httpStatus >= 400 && response.httpStatus < 500)
37705
+ return { status: "failed", deliveryId, errorCode: "DELIVERY_FAILED" };
37706
+ return unknown2;
37707
+ }
37708
+
37636
37709
  // src/tools/invoices.ts
37637
37710
  var LIST_INVOICES_ROW_SHAPE = {
37638
37711
  id: external_exports.string().uuid(),
@@ -37688,7 +37761,7 @@ var GET_INVOICE_OUTPUT_SHAPE = {
37688
37761
  var CREATE_INVOICE_INPUT_SHAPE = {
37689
37762
  customerId: external_exports.string().uuid().describe("Customer UUID"),
37690
37763
  invoiceDate: ISO_DATE_SCHEMA.describe("Invoice date (YYYY-MM-DD)"),
37691
- paymentTermDays: external_exports.number().min(0).max(365).default(14).describe("Payment term in days (default 14)"),
37764
+ paymentTermDays: external_exports.number().min(0).max(365).optional().describe("Payment term in days (default: the customer's payment term, else 14)"),
37692
37765
  notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
37693
37766
  priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
37694
37767
  reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
@@ -37702,7 +37775,8 @@ var CREATE_INVOICE_INPUT_SHAPE = {
37702
37775
  quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
37703
37776
  unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
37704
37777
  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).default("25.5").describe("VAT rate"),
37778
+ 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)"),
37779
+ discountPercent: external_exports.number().min(0).max(100).optional().describe("Line discount in percent (0-100, at most two decimals), applied before VAT"),
37706
37780
  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
37781
  })
37708
37782
  ).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
@@ -37727,7 +37801,10 @@ async function createInvoiceDraftCommand(ctx, params) {
37727
37801
  notes: params.notes,
37728
37802
  reverseChargeConfirmed: params.reverseChargeConfirmed,
37729
37803
  euGoodsTransportConfirmed: params.euGoodsTransportConfirmed,
37730
- lines: params.lines
37804
+ // A rate defaults to 25.5 only when neither a rate nor a treatment is given.
37805
+ lines: params.lines.map(
37806
+ (line) => line.vatRate === void 0 && line.vatTreatmentCode === void 0 ? { ...line, vatRate: "25.5" } : line
37807
+ )
37731
37808
  },
37732
37809
  { sellerAlvRegistered: company.alv_registered, customer }
37733
37810
  );
@@ -37775,6 +37852,51 @@ async function createInvoiceDraftCommand(ctx, params) {
37775
37852
  }
37776
37853
  });
37777
37854
  }
37855
+ async function sendInvoiceCommand(ctx, params) {
37856
+ 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();
37857
+ if (invoiceError || !invoice) {
37858
+ return toErrorContent(`Invoice ${params.invoiceId} not found.`);
37859
+ }
37860
+ if (invoice.status !== "draft") {
37861
+ return toErrorContent(
37862
+ `Invoice ${invoice.invoice_number} cannot be delivered from status "${invoice.status}". Only draft invoices can be sent.`
37863
+ );
37864
+ }
37865
+ let httpStatus = null;
37866
+ let body = null;
37867
+ try {
37868
+ const { data, error: error61, response } = await ctx.supabase.functions.invoke("dispatch-invoice", {
37869
+ body: { invoiceId: invoice.id, channel: params.channel }
37870
+ });
37871
+ const failed = error61?.context;
37872
+ if (!error61) {
37873
+ httpStatus = response?.status ?? 200;
37874
+ body = data;
37875
+ } else if (failed instanceof Response) {
37876
+ httpStatus = failed.status;
37877
+ body = await failed.json().catch(() => null);
37878
+ }
37879
+ } catch {
37880
+ }
37881
+ const outcome = classifyInvoiceDelivery({ httpStatus, body });
37882
+ if (outcome.status === "unknown") {
37883
+ return toWriteOutcomeUnknown("kirjapro_send_invoice", "kirjapro_get_invoice");
37884
+ }
37885
+ if (outcome.status === "failed") {
37886
+ const detail = body && typeof body === "object" && typeof body.error === "string" ? body.error : invoiceDeliveryMessage(outcome.errorCode);
37887
+ return toErrorContent(`${outcome.errorCode}: ${detail}`);
37888
+ }
37889
+ return toResult({
37890
+ message: `Invoice ${invoice.invoice_number} delivered via ${params.channel}`,
37891
+ invoice_id: invoice.id,
37892
+ invoice_number: invoice.invoice_number,
37893
+ channel: params.channel,
37894
+ delivery_id: outcome.deliveryId,
37895
+ transmission_id: outcome.transmissionId,
37896
+ message_id: outcome.messageId,
37897
+ status: "sent"
37898
+ });
37899
+ }
37778
37900
  function registerInvoiceTools(server2) {
37779
37901
  server2.registerTool(
37780
37902
  "kirjapro_list_invoices",
@@ -37854,42 +37976,14 @@ function registerInvoiceTools(server2) {
37854
37976
  annotations: WRITE_EXTERNAL_ANNOTATIONS
37855
37977
  },
37856
37978
  async (params) => {
37857
- const supabase2 = getSupabase();
37858
- const companyId2 = getCompanyId();
37979
+ const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
37859
37980
  return withIdempotency(
37860
- supabase2,
37861
- companyId2,
37981
+ ctx.supabase,
37982
+ ctx.companyId,
37862
37983
  "kirjapro_send_invoice",
37863
37984
  params.idempotencyKey,
37864
37985
  params,
37865
- async () => {
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
- }
37986
+ () => sendInvoiceCommand(ctx, params)
37893
37987
  );
37894
37988
  }
37895
37989
  );
@@ -38285,9 +38379,9 @@ var PRH_LOOKUP_OUTPUT_SHAPE = {
38285
38379
  website: external_exports.string().nullable(),
38286
38380
  phone: external_exports.string().nullable()
38287
38381
  };
38288
- function sanitizeText(text) {
38289
- if (!text) return null;
38290
- return text.trim().replace(/\s+/g, " ").slice(0, 500);
38382
+ function sanitizeText(text2) {
38383
+ if (!text2) return null;
38384
+ return text2.trim().replace(/\s+/g, " ").slice(0, 500);
38291
38385
  }
38292
38386
  function registerCustomerTools(server2) {
38293
38387
  server2.registerTool(
@@ -38557,6 +38651,62 @@ function registerCustomerTools(server2) {
38557
38651
  );
38558
38652
  }
38559
38653
 
38654
+ // src/shared/ar-aging-report.ts
38655
+ var AR_AGING_TOOL = {
38656
+ name: "kirjapro_get_ar_aging",
38657
+ 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.",
38658
+ // Equal to the MCP package's READ_ONLY_ANNOTATIONS (tool fingerprint).
38659
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
38660
+ };
38661
+ var NUMERIC_FIELDS = [
38662
+ "bucket_0_30_count",
38663
+ "bucket_0_30_amount",
38664
+ "bucket_31_60_count",
38665
+ "bucket_31_60_amount",
38666
+ "bucket_61_90_count",
38667
+ "bucket_61_90_amount",
38668
+ "bucket_90_plus_count",
38669
+ "bucket_90_plus_amount",
38670
+ "no_due_date_count",
38671
+ "no_due_date_amount",
38672
+ "total_count",
38673
+ "total_receivables",
38674
+ "weighted_average_days_overdue"
38675
+ ];
38676
+ var cents = (amount2) => Math.round(amount2 * 100) / 100;
38677
+ var eur = (amount2) => new Intl.NumberFormat("fi-FI", { style: "currency", currency: "EUR" }).format(amount2);
38678
+ var amount = (count, total2) => ({
38679
+ count,
38680
+ total: cents(total2),
38681
+ total_formatted: eur(total2)
38682
+ });
38683
+ function isRawArAging(data) {
38684
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
38685
+ const row = data;
38686
+ return typeof row.as_of_date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(row.as_of_date) && NUMERIC_FIELDS.every((field) => {
38687
+ const value = row[field];
38688
+ return typeof value === "number" && Number.isFinite(value);
38689
+ });
38690
+ }
38691
+ function toArAgingReport(data) {
38692
+ if (!isRawArAging(data)) return null;
38693
+ return {
38694
+ as_of_date: data.as_of_date,
38695
+ buckets: [
38696
+ { label: "0-30 days", ...amount(data.bucket_0_30_count, data.bucket_0_30_amount) },
38697
+ { label: "31-60 days", ...amount(data.bucket_31_60_count, data.bucket_31_60_amount) },
38698
+ { label: "61-90 days", ...amount(data.bucket_61_90_count, data.bucket_61_90_amount) },
38699
+ { label: "90+ days", ...amount(data.bucket_90_plus_count, data.bucket_90_plus_amount) }
38700
+ ],
38701
+ no_due_date: amount(data.no_due_date_count, data.no_due_date_amount),
38702
+ total_count: data.total_count,
38703
+ total_receivables: cents(data.total_receivables),
38704
+ total_receivables_formatted: eur(data.total_receivables),
38705
+ weighted_average_days_overdue: data.weighted_average_days_overdue,
38706
+ 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."
38707
+ };
38708
+ }
38709
+
38560
38710
  // src/compliance/omavero-fields.ts
38561
38711
  var OMAVERO_FIELD_NUMBERS = [
38562
38712
  "301",
@@ -38597,7 +38747,7 @@ var LEGACY_ALV_SUMMARY_KEYS = [
38597
38747
  "reverse_charge_base",
38598
38748
  "omavero"
38599
38749
  ];
38600
- function cents(value) {
38750
+ function cents2(value) {
38601
38751
  return Math.round(value * 100) / 100;
38602
38752
  }
38603
38753
  function num(value) {
@@ -38622,7 +38772,7 @@ function omaveroFieldsFromSummary(s) {
38622
38772
  "314": num(s.field_309_base),
38623
38773
  "318": num(s.output_vat_construction_scrap),
38624
38774
  "319": num(s.reverse_charge_sales_base),
38625
- "320": cents(num(s.field_310_base) + num(s.field_313_base))
38775
+ "320": cents2(num(s.field_310_base) + num(s.field_313_base))
38626
38776
  };
38627
38777
  const map2 = s.omavero;
38628
38778
  if (map2 && typeof map2 === "object") {
@@ -38814,8 +38964,8 @@ function alvReconciliationWarning(reconciliation) {
38814
38964
  if (!reconciliation || typeof reconciliation !== "object") return void 0;
38815
38965
  const r = reconciliation;
38816
38966
  if (r.status !== "mismatch" && r.status !== "explained") return void 0;
38817
- const amount = (value) => (Number(value) || 0).toFixed(2);
38818
- return `VAT amounts or the audit trail need review (output VAT difference ${amount(r.output_difference)} \u20AC, input VAT difference ${amount(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.`;
38967
+ const amount2 = (value) => (Number(value) || 0).toFixed(2);
38968
+ 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
38969
  }
38820
38970
  var BALANCE_SHEET_ROW_SHAPE = external_exports.object({
38821
38971
  account_number: external_exports.string(),
@@ -39167,12 +39317,12 @@ function registerReportTools(server2) {
39167
39317
  }
39168
39318
  );
39169
39319
  server2.registerTool(
39170
- "kirjapro_get_ar_aging",
39320
+ AR_AGING_TOOL.name,
39171
39321
  {
39172
- 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.",
39322
+ description: AR_AGING_TOOL.description,
39173
39323
  inputSchema: {},
39174
39324
  outputSchema: AR_AGING_OUTPUT_SHAPE,
39175
- annotations: READ_ONLY_ANNOTATIONS
39325
+ annotations: AR_AGING_TOOL.annotations
39176
39326
  },
39177
39327
  async () => {
39178
39328
  const supabase2 = getSupabase();
@@ -39183,32 +39333,8 @@ function registerReportTools(server2) {
39183
39333
  if (error61) {
39184
39334
  return toErrorContent(sanitizeDbError("Failed to get AR aging", error61));
39185
39335
  }
39186
- const aging = data;
39187
- const buckets = [
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
- });
39336
+ const report = toArAgingReport(data);
39337
+ return report ? toResult(report) : toErrorContent("Failed to get AR aging. Please try again.");
39212
39338
  }
39213
39339
  );
39214
39340
  server2.registerTool(
@@ -39340,7 +39466,7 @@ function registerReportTools(server2) {
39340
39466
  const warnings = [
39341
39467
  alvReconciliationWarning(raw.reconciliation),
39342
39468
  Number(raw.unclassified_legacy_reverse_charge_base) ? LEGACY_REVERSE_CHARGE_WARNING : void 0
39343
- ].filter((text) => Boolean(text));
39469
+ ].filter((text2) => Boolean(text2));
39344
39470
  return toResult({
39345
39471
  ...safeResult,
39346
39472
  ...warnings.length > 0 && { warning: warnings.join(" ") },
@@ -40427,12 +40553,12 @@ async function validateAccountOverrides(overrides) {
40427
40553
  return { ok: true };
40428
40554
  }
40429
40555
  function saleCashTemplate(revenueAccount, vatRate) {
40430
- return (amount, opts) => {
40431
- const { net, vat: vat2 } = splitVat(amount, vatRate);
40556
+ return (amount2, opts) => {
40557
+ const { net, vat: vat2 } = splitVat(amount2, vatRate);
40432
40558
  const entries = [
40433
40559
  {
40434
40560
  accountNumber: ACCOUNTS.KASSA,
40435
- debitAmount: roundCents(amount),
40561
+ debitAmount: roundCents(amount2),
40436
40562
  creditAmount: 0
40437
40563
  },
40438
40564
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
@@ -40448,12 +40574,12 @@ function saleCashTemplate(revenueAccount, vatRate) {
40448
40574
  };
40449
40575
  }
40450
40576
  function saleBankTemplate(revenueAccount, vatRate) {
40451
- return (amount, opts) => {
40452
- const { net, vat: vat2 } = splitVat(amount, vatRate);
40577
+ return (amount2, opts) => {
40578
+ const { net, vat: vat2 } = splitVat(amount2, vatRate);
40453
40579
  const entries = [
40454
40580
  {
40455
40581
  accountNumber: ACCOUNTS.PANKKITILI,
40456
- debitAmount: roundCents(amount),
40582
+ debitAmount: roundCents(amount2),
40457
40583
  creditAmount: 0
40458
40584
  },
40459
40585
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
@@ -40526,13 +40652,13 @@ var TEMPLATES = [
40526
40652
  kind: "sale",
40527
40653
  description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
40528
40654
  vatRate: 25.5,
40529
- expand: (amount, opts) => {
40530
- const { net, vat: vat2 } = splitVat(amount, 25.5);
40655
+ expand: (amount2, opts) => {
40656
+ const { net, vat: vat2 } = splitVat(amount2, 25.5);
40531
40657
  return applyOverrides(
40532
40658
  [
40533
40659
  {
40534
40660
  accountNumber: ACCOUNTS.MYYNTISAAMISET,
40535
- debitAmount: roundCents(amount),
40661
+ debitAmount: roundCents(amount2),
40536
40662
  creditAmount: 0
40537
40663
  },
40538
40664
  { accountNumber: ACCOUNTS.MYYNTI_25_5, debitAmount: 0, creditAmount: net },
@@ -40548,8 +40674,8 @@ var TEMPLATES = [
40548
40674
  kind: "purchase",
40549
40675
  description: "Osto ALV 25,5% (pankkitililt\xE4)",
40550
40676
  vatRate: 25.5,
40551
- expand: (amount, opts) => {
40552
- const { net, vat: vat2 } = splitVat(amount, 25.5);
40677
+ expand: (amount2, opts) => {
40678
+ const { net, vat: vat2 } = splitVat(amount2, 25.5);
40553
40679
  return applyOverrides(
40554
40680
  [
40555
40681
  { accountNumber: ACCOUNTS.OSTOT, debitAmount: net, creditAmount: 0 },
@@ -40557,7 +40683,7 @@ var TEMPLATES = [
40557
40683
  {
40558
40684
  accountNumber: ACCOUNTS.PANKKITILI,
40559
40685
  debitAmount: 0,
40560
- creditAmount: roundCents(amount)
40686
+ creditAmount: roundCents(amount2)
40561
40687
  }
40562
40688
  ],
40563
40689
  opts?.accountOverrides
@@ -40569,8 +40695,8 @@ var TEMPLATES = [
40569
40695
  kind: "purchase",
40570
40696
  description: "Ulkopuolinen palvelu ALV 25,5%",
40571
40697
  vatRate: 25.5,
40572
- expand: (amount, opts) => {
40573
- const { net, vat: vat2 } = splitVat(amount, 25.5);
40698
+ expand: (amount2, opts) => {
40699
+ const { net, vat: vat2 } = splitVat(amount2, 25.5);
40574
40700
  return applyOverrides(
40575
40701
  [
40576
40702
  { accountNumber: ACCOUNTS.OSTOT_PALVELUT, debitAmount: net, creditAmount: 0 },
@@ -40578,7 +40704,7 @@ var TEMPLATES = [
40578
40704
  {
40579
40705
  accountNumber: ACCOUNTS.PANKKITILI,
40580
40706
  debitAmount: 0,
40581
- creditAmount: roundCents(amount)
40707
+ creditAmount: roundCents(amount2)
40582
40708
  }
40583
40709
  ],
40584
40710
  opts?.accountOverrides
@@ -40591,17 +40717,17 @@ var TEMPLATES = [
40591
40717
  kind: "other",
40592
40718
  description: "Vuokranmaksu (pankkitililt\xE4)",
40593
40719
  vatRate: null,
40594
- expand: (amount, opts) => applyOverrides(
40720
+ expand: (amount2, opts) => applyOverrides(
40595
40721
  [
40596
40722
  {
40597
40723
  accountNumber: ACCOUNTS.TOIMITILAVUOKRAT,
40598
- debitAmount: roundCents(amount),
40724
+ debitAmount: roundCents(amount2),
40599
40725
  creditAmount: 0
40600
40726
  },
40601
40727
  {
40602
40728
  accountNumber: ACCOUNTS.PANKKITILI,
40603
40729
  debitAmount: 0,
40604
- creditAmount: roundCents(amount)
40730
+ creditAmount: roundCents(amount2)
40605
40731
  }
40606
40732
  ],
40607
40733
  opts?.accountOverrides
@@ -40613,17 +40739,17 @@ var TEMPLATES = [
40613
40739
  kind: "other",
40614
40740
  description: "Tilisiirto kassasta pankkitilille",
40615
40741
  vatRate: null,
40616
- expand: (amount, opts) => applyOverrides(
40742
+ expand: (amount2, opts) => applyOverrides(
40617
40743
  [
40618
40744
  {
40619
40745
  accountNumber: ACCOUNTS.PANKKITILI,
40620
- debitAmount: roundCents(amount),
40746
+ debitAmount: roundCents(amount2),
40621
40747
  creditAmount: 0
40622
40748
  },
40623
40749
  {
40624
40750
  accountNumber: ACCOUNTS.KASSA,
40625
40751
  debitAmount: 0,
40626
- creditAmount: roundCents(amount)
40752
+ creditAmount: roundCents(amount2)
40627
40753
  }
40628
40754
  ],
40629
40755
  opts?.accountOverrides
@@ -40646,17 +40772,17 @@ function isVatPurchaseTemplate(templateId) {
40646
40772
  function availableTemplates(alvRegistered) {
40647
40773
  return alvRegistered ? TEMPLATES.filter((t) => t.kind !== "sale") : TEMPLATES.filter((t) => !isVatPurchaseTemplate(t.id));
40648
40774
  }
40649
- function expandTemplate(templateId, amount, opts) {
40775
+ function expandTemplate(templateId, amount2, opts) {
40650
40776
  const template = TEMPLATE_MAP.get(templateId);
40651
40777
  if (!template) {
40652
40778
  return {
40653
40779
  error: `Unknown template "${templateId}". Use kirjapro_list_templates to see available templates.`
40654
40780
  };
40655
40781
  }
40656
- if (amount <= 0) {
40782
+ if (amount2 <= 0) {
40657
40783
  return { error: "Amount must be positive." };
40658
40784
  }
40659
- const entries = template.expand(amount, opts);
40785
+ const entries = template.expand(amount2, opts);
40660
40786
  return { entries };
40661
40787
  }
40662
40788
  async function validateFiscalYearForDate(fiscalYearId, transactionDate) {
@@ -41305,8 +41431,8 @@ function registerBookingTools(server2) {
41305
41431
  async (params) => {
41306
41432
  const execute = async () => {
41307
41433
  const rate = Number(params.vatRate);
41308
- const amount = roundCents(params.amount);
41309
- if (amount <= 0) return toErrorContent("Oston summan on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41434
+ const amount2 = roundCents(params.amount);
41435
+ if (amount2 <= 0) return toErrorContent("Oston summan on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41310
41436
  const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
41311
41437
  if ("isError" in companyVat) return companyVat;
41312
41438
  if (params.reverseCharge && !params.reverseChargeCategory) return toErrorContent(REVERSE_CHARGE_CATEGORY_REQUIRED);
@@ -41326,7 +41452,7 @@ function registerBookingTools(server2) {
41326
41452
  if (basisError) return toErrorContent(basisError);
41327
41453
  }
41328
41454
  const basis = params.vatRateBasis;
41329
- const { net, vat: vat2 } = params.reverseCharge ? { net: amount, vat: roundCents(amount * rate / 100) } : splitVat(amount, rate);
41455
+ const { net, vat: vat2 } = params.reverseCharge ? { net: amount2, vat: roundCents(amount2 * rate / 100) } : splitVat(amount2, rate);
41330
41456
  const allocation = allocatePurchaseVat(net, vat2, percent);
41331
41457
  const keepFacts = rate > 0 && (params.reverseCharge || companyVat.alvRegistered && !params.edustus);
41332
41458
  const vatAllocation = percent > 0 && percent < 100 ? {
@@ -41352,13 +41478,13 @@ function registerBookingTools(server2) {
41352
41478
  { accountNumber: params.expenseAccount, debitAmount: expense, creditAmount: 0 },
41353
41479
  { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: allocation.deductibleVat, creditAmount: 0 },
41354
41480
  ...params.reverseCharge ? [{ accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }] : [],
41355
- ...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net : amount }] : []
41481
+ ...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net : amount2 }] : []
41356
41482
  ].filter((entry) => entry.debitAmount !== 0 || entry.creditAmount !== 0);
41357
41483
  if (entries.length < 2) return toErrorContent("Tositteen veron on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41358
41484
  const purchaseAccountWarning = expenseAccountWarning(params.expenseAccount);
41359
41485
  const calculation = {
41360
41486
  amount_basis: assessmentOnly ? "customs_tax_base" : params.reverseCharge ? "supplier_net" : "supplier_gross",
41361
- ...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net : amount },
41487
+ ...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net : amount2 },
41362
41488
  tax_base: net,
41363
41489
  output_vat: params.reverseCharge ? vat2 : 0,
41364
41490
  source_vat: vat2,
@@ -41369,7 +41495,7 @@ function registerBookingTools(server2) {
41369
41495
  };
41370
41496
  if (params.dryRun) return toDryRunResponse("kirjapro_book_purchase", {
41371
41497
  action: "book_purchase",
41372
- gross: amount,
41498
+ gross: amount2,
41373
41499
  vat_rate: params.vatRate,
41374
41500
  description: params.description,
41375
41501
  date: params.date,
@@ -43806,7 +43932,7 @@ function registerPrompts(mcpServer) {
43806
43932
  } else {
43807
43933
  fyBlock = "Ei avointa tilikautta \u2014 luo tilikausi ennen kirjausten tekemist\xE4.";
43808
43934
  }
43809
- const text = [
43935
+ const text2 = [
43810
43936
  `T\xE4m\xE4 on Kirjapro MCP -palvelin (v${VERSION}). Yhdistetty yritys:`,
43811
43937
  "",
43812
43938
  companyBlock,
@@ -43824,7 +43950,7 @@ function registerPrompts(mcpServer) {
43824
43950
  messages: [
43825
43951
  {
43826
43952
  role: "user",
43827
- content: { type: "text", text }
43953
+ content: { type: "text", text: text2 }
43828
43954
  }
43829
43955
  ]
43830
43956
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirjapro/mcp-server",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
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.13.0",
5
+ "version": "0.14.1",
6
6
  "homepage": "https://kirjapro.fi",
7
7
  "repository": "https://github.com/Jaollo/kirja-pro",
8
8
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  {
32
32
  "name": "kirjapro_create_invoice",
33
- "hash": "f5b9d183f3649ebc"
33
+ "hash": "67c934bc676e9937"
34
34
  },
35
35
  {
36
36
  "name": "kirjapro_create_offer",