@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 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((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.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 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", {
@@ -36363,8 +36363,8 @@ async function invokeEdgeFunction(supabase2, name, body) {
36363
36363
  }
36364
36364
  return { data, error: null };
36365
36365
  }
36366
- function roundCents(amount) {
36367
- return Math.round(amount * 100) / 100;
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 || 0} transactions${cappedWarning}`
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) => `Useita asiakkaita osuu hakuun: ${detail}. Tarkenna nime\xE4.`,
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 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;
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
- return { code: resolved, rate: TREATMENT_RATES[resolved] ?? 0 };
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).default(14).describe("Payment term in days (default 14)"),
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).default("25.5").describe("VAT rate"),
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
- lines: params.lines
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 supabase2 = getSupabase();
37858
- const companyId2 = getCompanyId();
37973
+ const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
37859
37974
  return withIdempotency(
37860
- supabase2,
37861
- companyId2,
37975
+ ctx.supabase,
37976
+ ctx.companyId,
37862
37977
  "kirjapro_send_invoice",
37863
37978
  params.idempotencyKey,
37864
37979
  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
- }
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(text) {
38289
- if (!text) return null;
38290
- return text.trim().replace(/\s+/g, " ").slice(0, 500);
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 cents(value) {
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": cents(num(s.field_310_base) + num(s.field_313_base))
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 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.`;
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
- "kirjapro_get_ar_aging",
39314
+ AR_AGING_TOOL.name,
39171
39315
  {
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.",
39316
+ description: AR_AGING_TOOL.description,
39173
39317
  inputSchema: {},
39174
39318
  outputSchema: AR_AGING_OUTPUT_SHAPE,
39175
- annotations: READ_ONLY_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 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
- });
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((text) => Boolean(text));
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 (amount, opts) => {
40431
- const { net, vat: vat2 } = splitVat(amount, vatRate);
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(amount),
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 (amount, opts) => {
40452
- const { net, vat: vat2 } = splitVat(amount, vatRate);
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(amount),
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: (amount, opts) => {
40530
- const { net, vat: vat2 } = splitVat(amount, 25.5);
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(amount),
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: (amount, opts) => {
40552
- const { net, vat: vat2 } = splitVat(amount, 25.5);
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(amount)
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: (amount, opts) => {
40573
- const { net, vat: vat2 } = splitVat(amount, 25.5);
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(amount)
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: (amount, opts) => applyOverrides(
40714
+ expand: (amount2, opts) => applyOverrides(
40595
40715
  [
40596
40716
  {
40597
40717
  accountNumber: ACCOUNTS.TOIMITILAVUOKRAT,
40598
- debitAmount: roundCents(amount),
40718
+ debitAmount: roundCents(amount2),
40599
40719
  creditAmount: 0
40600
40720
  },
40601
40721
  {
40602
40722
  accountNumber: ACCOUNTS.PANKKITILI,
40603
40723
  debitAmount: 0,
40604
- creditAmount: roundCents(amount)
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: (amount, opts) => applyOverrides(
40736
+ expand: (amount2, opts) => applyOverrides(
40617
40737
  [
40618
40738
  {
40619
40739
  accountNumber: ACCOUNTS.PANKKITILI,
40620
- debitAmount: roundCents(amount),
40740
+ debitAmount: roundCents(amount2),
40621
40741
  creditAmount: 0
40622
40742
  },
40623
40743
  {
40624
40744
  accountNumber: ACCOUNTS.KASSA,
40625
40745
  debitAmount: 0,
40626
- creditAmount: roundCents(amount)
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, amount, opts) {
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 (amount <= 0) {
40776
+ if (amount2 <= 0) {
40657
40777
  return { error: "Amount must be positive." };
40658
40778
  }
40659
- const entries = template.expand(amount, opts);
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 amount = roundCents(params.amount);
41309
- if (amount <= 0) return toErrorContent("Oston summan on oltava v\xE4hint\xE4\xE4n yksi sentti.");
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: amount, vat: roundCents(amount * rate / 100) } : splitVat(amount, rate);
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 : amount }] : []
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 : amount },
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: amount,
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 text = [
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.13.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.13.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",
@@ -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",