@kirjapro/mcp-server 0.12.0 → 0.13.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,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0] - 2026-10-04
4
+
5
+ No database prerequisite: every function, rule and VAT treatment it uses was
6
+ already live with 0.12.0, and 0.12.0 remains compatible with the same database.
7
+
8
+ - `kirjapro_create_invoice` runs on the invoice-draft core that Kirjapro's AI
9
+ Chat also uses (`src/shared/invoice-draft.ts`, generated from the repository's
10
+ `src/lib/invoice-draft.ts`). Line amounts round like the database (numeric
11
+ `ROUND`, half away from zero), so 3 × 1.255 € returns 3.77 €, as stored;
12
+ 0.12.0 returned 3.76 €.
13
+ - The database allocates the invoice and reference number in the creation
14
+ transaction. The server no longer reads `get_next_invoice_number` first, and
15
+ the success message names the stored number.
16
+ - Errors are a stable code with a Finnish explanation, `CODE: viesti` (for
17
+ example `VAT_REQUIRES_EU_VAT_NUMBER: …` or `FISCAL_YEAR_CLOSED: …`), like the
18
+ existing `IDEMPOTENCY_*:` errors. Other tools keep their English messages.
19
+ - A lost database response returns `IDEMPOTENCY_OUTCOME_UNKNOWN` and leaves the
20
+ idempotency claim pending, so a retry with the same key never creates a
21
+ second invoice. 0.12.0 cached it as an ordinary failure.
22
+ - When the invoice is created but reading it back fails, the result is a
23
+ success with a `warning` and the invoice id, cached like any success.
24
+ - `paymentTermDays` must be a whole number of days (0–365). It still defaults
25
+ to 14, so the customer's own payment term applies only when it is passed.
26
+ - Returned `lines` no longer echo `reverse_charge_confirmation_text`; the
27
+ database sets the invoice's confirmation text.
28
+ - `kirjapro_send_invoice` declares `openWorldHint: true`: email and e-invoice
29
+ delivery are external effects. Only its fingerprint changes; the
30
+ `kirjapro_create_invoice` input schema and fingerprint are unchanged.
31
+ - `kirjapro://compliance/vat`: the reverse-charge `implementation_limits` now
32
+ says that `kirjapro_book_purchase` takes `deductionPercent` 0–100 with a
33
+ documented reason, also for reverse charge. 0.12.0 still said that the
34
+ parameter did not exist.
35
+
3
36
  ## [0.12.0] - 2026-10-04
4
37
 
5
38
  Requires deployment of all seven migrations in the compliance batch before release:
package/dist/index.js CHANGED
@@ -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.12.0";
35682
+ var VERSION = "0.13.0";
35683
35683
 
35684
35684
  // src/auth.ts
35685
35685
  var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
@@ -36392,18 +36392,6 @@ function tagCustomer(row) {
36392
36392
  if ("notes" in row) wrapped.notes = tagUntrusted(row.notes);
36393
36393
  return wrapped;
36394
36394
  }
36395
- function addDaysToIsoDate(date5, days) {
36396
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date5);
36397
- if (!match) {
36398
- throw new Error(`Invalid ISO date: ${date5}`);
36399
- }
36400
- const year = Number.parseInt(match[1], 10);
36401
- const month = Number.parseInt(match[2], 10);
36402
- const day = Number.parseInt(match[3], 10);
36403
- const utcDate = new Date(Date.UTC(year, month - 1, day));
36404
- utcDate.setUTCDate(utcDate.getUTCDate() + days);
36405
- return utcDate.toISOString().slice(0, 10);
36406
- }
36407
36395
  function formatFinnishDate(iso) {
36408
36396
  const [y, m, d] = iso.split("-");
36409
36397
  return `${Number.parseInt(d, 10)}.${Number.parseInt(m, 10)}.${y}`;
@@ -37307,45 +37295,123 @@ var VALID_TRANSITIONS = {
37307
37295
  cancelled: []
37308
37296
  };
37309
37297
 
37310
- // src/finnish-business-id.ts
37311
- function validateFinnishBusinessIdCore(input2) {
37312
- const cleaned = input2.replace(/\s/g, "").toUpperCase();
37313
- const match = cleaned.match(/^(\d{7})-(\d)$/);
37314
- if (!match) return { valid: false, reason: "format" };
37315
- const digits = match[1];
37316
- const actualCheckDigit = Number(match[2]);
37317
- if (!digits || !Number.isInteger(actualCheckDigit)) {
37318
- return { valid: false, reason: "format" };
37319
- }
37320
- const weights = [7, 9, 10, 5, 8, 4, 2];
37321
- let sum = 0;
37322
- for (let index = 0; index < weights.length; index += 1) {
37323
- const digit = digits[index];
37324
- const weight = weights[index];
37325
- if (digit === void 0 || weight === void 0) {
37326
- return { valid: false, reason: "format" };
37327
- }
37328
- sum += Number(digit) * weight;
37329
- }
37330
- const remainder = sum % 11;
37331
- if (remainder === 1) return { valid: false, reason: "remainder_one" };
37332
- const expectedCheckDigit = remainder === 0 ? 0 : 11 - remainder;
37333
- if (actualCheckDigit !== expectedCheckDigit) {
37334
- return {
37335
- valid: false,
37336
- reason: "checksum",
37337
- expectedCheckDigit,
37338
- actualCheckDigit
37339
- };
37340
- }
37341
- return { valid: true };
37342
- }
37343
- function isValidFinnishBusinessId(input2) {
37344
- return validateFinnishBusinessIdCore(input2).valid;
37298
+ // src/shared/invoice-draft.ts
37299
+ var INVOICE_VAT_TREATMENT_CODES = [
37300
+ "standard_25_5",
37301
+ "standard_13_5",
37302
+ "standard_10",
37303
+ "not_vat_registered",
37304
+ "reverse_construction",
37305
+ "reverse_scrap",
37306
+ "reverse_eu_service",
37307
+ "export_outside_eu",
37308
+ "service_outside_eu",
37309
+ "intra_eu_goods",
37310
+ "exempt_healthcare",
37311
+ "exempt_social_welfare",
37312
+ "exempt_education",
37313
+ "exempt_financial",
37314
+ "exempt_insurance",
37315
+ "exempt_other"
37316
+ ];
37317
+ var INVOICE_VAT_RATES = ["25.5", "13.5", "10", "0"];
37318
+ var INVOICE_DRAFT_MAX_LINES = 100;
37319
+ var INVOICE_DRAFT_CUSTOMER_COLUMNS = "id, name, payment_term_days, customer_kind, business_id, vat_number, country";
37320
+ var ERROR_MESSAGES = {
37321
+ INVALID_INPUT: (detail) => `Laskun tiedoissa on virhe: ${detail}.`,
37322
+ 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.`,
37324
+ LOOKUP_FAILED: () => "Laskun tietojen haku ep\xE4onnistui. Mit\xE4\xE4n ei tallennettu; voit yritt\xE4\xE4 uudelleen.",
37325
+ FISCAL_YEAR_REQUIRED: () => "Ei tilikautta valittuna.",
37326
+ FISCAL_YEAR_NOT_FOUND: () => "Tilikautta ei l\xF6ydy. Tarkista valinta.",
37327
+ FISCAL_YEAR_CLOSED: () => "Tilikausi on suljettu. Laskua ei voi luoda.",
37328
+ VAT_TREATMENT_REQUIRED: () => "0 % ALV vaatii t\xE4sm\xE4llisen ALV-k\xE4sittelyn syyn.",
37329
+ VAT_SELLER_NOT_REGISTERED: () => "ALV-rekisteriin kuulumaton myyj\xE4 voi k\xE4ytt\xE4\xE4 vain Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
37330
+ VAT_SELLER_REGISTERED: () => "ALV-rekisteriss\xE4 oleva myyj\xE4 ei voi k\xE4ytt\xE4\xE4 Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
37331
+ VAT_RATE_NOT_IN_FORCE: (detail) => `ALV-kanta ${detail}.`,
37332
+ VAT_REQUIRES_BUSINESS_CUSTOMER: () => "Valittu ALV-k\xE4sittely vaatii yritysasiakkaan.",
37333
+ VAT_REQUIRES_FINNISH_BUSINESS_ID: () => "Kotimainen k\xE4\xE4nnetty verovelvollisuus vaatii ostajan kelvollisen Y-tunnuksen.",
37334
+ VAT_REQUIRES_EU_VAT_NUMBER: () => "EU-myynti vaatii muun EU-maan maakoodin ja sit\xE4 vastaavan ostajan ALV-tunnisteen.",
37335
+ VAT_REQUIRES_NON_EU_COUNTRY: () => "EU:n ulkopuolinen myynti vaatii ostajalle EU:n ulkopuolisen maan.",
37336
+ INVOICE_NUMBER_CONFLICT: () => "Laskunumero on jo k\xE4yt\xF6ss\xE4 (samanaikainen luonti). Yrit\xE4 uudelleen.",
37337
+ INVALID_LINE: () => "Laskurivi ei kelpaa. Tarkista m\xE4\xE4r\xE4, hinta ja ALV-k\xE4sittely.",
37338
+ PERMISSION_DENIED: () => "Ei oikeutta luoda laskua t\xE4lle yritykselle.",
37339
+ MFA_REQUIRED: () => "Vahvista kaksivaiheinen tunnistautuminen Kirjaprossa ja yrit\xE4 sitten uudelleen.",
37340
+ WRITE_REJECTED: () => "Laskun luonti hyl\xE4ttiin. Mit\xE4\xE4n ei tallennettu.",
37341
+ OUTCOME_UNKNOWN: () => "Laskun luonnin lopputulosta ei voitu varmistaa. Tarkista laskulista ennen uutta yrityst\xE4."
37342
+ };
37343
+ function retryFor(code) {
37344
+ if (code === "OUTCOME_UNKNOWN") return "reconcile";
37345
+ return code === "LOOKUP_FAILED" || code === "INVOICE_NUMBER_CONFLICT" ? "safe" : "never";
37346
+ }
37347
+ function invoiceDraftError(code, detail = "") {
37348
+ return { code, message: ERROR_MESSAGES[code](detail), retry: retryFor(code) };
37349
+ }
37350
+ function roundNumericMoney(value) {
37351
+ if (!Number.isFinite(value)) return value;
37352
+ if (Math.abs(value) >= 9e13) return Math.round(value * 100) / 100;
37353
+ const digits = Math.abs(value).toPrecision(15);
37354
+ if (digits.includes("e")) return 0;
37355
+ 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;
37359
+ }
37360
+ function calculateLineTotals(quantity, unitPrice, vatRate, priceEntryMode = "vat_exclusive", discountPercent = 0) {
37361
+ const discountFactor = 1 - discountPercent / 100;
37362
+ if (priceEntryMode === "vat_inclusive" && vatRate > 0) {
37363
+ const total3 = roundNumericMoney(quantity * unitPrice * discountFactor);
37364
+ const vatAmount2 = roundNumericMoney(total3 * vatRate / (100 + vatRate));
37365
+ const subtotal2 = roundNumericMoney(total3 - vatAmount2);
37366
+ return { subtotal: subtotal2, vatAmount: vatAmount2, total: total3 };
37367
+ }
37368
+ const subtotal = roundNumericMoney(quantity * unitPrice * discountFactor);
37369
+ const vatAmount = roundNumericMoney(subtotal * (vatRate / 100));
37370
+ const total2 = roundNumericMoney(subtotal + vatAmount);
37371
+ return { subtotal, vatAmount, total: total2 };
37372
+ }
37373
+ function calculateInvoiceTotals(lines) {
37374
+ const subtotalCents = lines.reduce((sum, line) => sum + Math.round(line.subtotal * 100), 0);
37375
+ const vatTotalCents = lines.reduce((sum, line) => sum + Math.round(line.vat_amount * 100), 0);
37376
+ const totalCents = lines.reduce((sum, line) => sum + Math.round(line.total * 100), 0);
37377
+ return {
37378
+ subtotal: subtotalCents / 100,
37379
+ vatTotal: vatTotalCents / 100,
37380
+ total: totalCents / 100
37381
+ };
37345
37382
  }
37346
-
37347
- // src/vat.ts
37348
- var EU_COUNTRY_CODES = /* @__PURE__ */ new Set([
37383
+ var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
37384
+ function dateParts(date5) {
37385
+ const match = ISO_DATE.exec(date5);
37386
+ if (!match) return null;
37387
+ const year = Number(match[1]);
37388
+ const month = Number(match[2]);
37389
+ const day = Number(match[3]);
37390
+ const utc = new Date(Date.UTC(year, month - 1, day));
37391
+ return utc.getUTCFullYear() === year && utc.getUTCMonth() === month - 1 && utc.getUTCDate() === day ? [year, month, day] : null;
37392
+ }
37393
+ function addDaysIso(date5, days) {
37394
+ const parts = dateParts(date5);
37395
+ if (!parts) throw new RangeError(`Invalid date: ${date5}`);
37396
+ const [year, month, day] = parts;
37397
+ return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
37398
+ }
37399
+ var TREATMENT_RATES = {
37400
+ standard_25_5: 25.5,
37401
+ standard_13_5: 13.5,
37402
+ standard_10: 10
37403
+ };
37404
+ var STANDARD_TREATMENTS = {
37405
+ "25.5": "standard_25_5",
37406
+ "13.5": "standard_13_5",
37407
+ "10": "standard_10"
37408
+ };
37409
+ function resolveInvoiceVatTreatment(vatRate, code) {
37410
+ const resolved = code ?? (vatRate === void 0 ? void 0 : STANDARD_TREATMENTS[vatRate]);
37411
+ if (!resolved) return invoiceDraftError("VAT_TREATMENT_REQUIRED");
37412
+ return { code: resolved, rate: TREATMENT_RATES[resolved] ?? 0 };
37413
+ }
37414
+ var EU_COUNTRY_CODES = [
37349
37415
  "AT",
37350
37416
  "BE",
37351
37417
  "BG",
@@ -37373,51 +37439,164 @@ var EU_COUNTRY_CODES = /* @__PURE__ */ new Set([
37373
37439
  "SE",
37374
37440
  "SI",
37375
37441
  "SK"
37376
- ]);
37377
- function normalizedVatNumber(value) {
37378
- return (value ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
37379
- }
37380
- function validateVatTreatmentEligibility({
37381
- code,
37382
- sellerAlvRegistered,
37383
- customer,
37384
- documentDate
37385
- }) {
37386
- if (!sellerAlvRegistered && code !== "not_vat_registered") {
37387
- return "A seller outside the VAT register can only use not_vat_registered.";
37388
- }
37389
- if (sellerAlvRegistered && code === "not_vat_registered") {
37390
- return "A VAT-registered seller cannot use not_vat_registered.";
37391
- }
37392
- if (code === "standard_25_5" && documentDate < "2024-09-01") {
37393
- return "standard_25_5 is not valid before 2024-09-01.";
37394
- }
37395
- if (code === "standard_13_5" && documentDate < "2026-01-01") {
37396
- return "standard_13_5 is not valid before 2026-01-01.";
37397
- }
37398
- const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu";
37399
- if (requiresBusiness && customer?.customer_kind !== "business") {
37400
- return "The selected VAT treatment requires a business customer.";
37401
- }
37402
- if ((code === "reverse_construction" || code === "reverse_scrap") && !isValidFinnishBusinessId(customer?.business_id ?? "")) {
37403
- return "Domestic reverse charge requires a Finnish buyer business ID in the form 1234567-8.";
37404
- }
37442
+ ];
37443
+ function isFinnishBusinessId(value) {
37444
+ const match = /^(\d{7})-(\d)$/.exec(value.replace(/\s/g, ""));
37445
+ if (!match) return false;
37446
+ const digits = match[1] ?? "";
37447
+ const weights = [7, 9, 10, 5, 8, 4, 2];
37448
+ let sum = 0;
37449
+ for (let index = 0; index < weights.length; index += 1)
37450
+ sum += Number(digits.charAt(index)) * (weights[index] ?? 0);
37451
+ const remainder = sum % 11;
37452
+ if (remainder === 1) return false;
37453
+ return Number(match[2]) === (remainder === 0 ? 0 : 11 - remainder);
37454
+ }
37455
+ function invoiceVatTreatmentError(code, sellerAlvRegistered, customer, documentDate) {
37456
+ if (!sellerAlvRegistered && code !== "not_vat_registered")
37457
+ return invoiceDraftError("VAT_SELLER_NOT_REGISTERED");
37458
+ if (sellerAlvRegistered && code === "not_vat_registered")
37459
+ return invoiceDraftError("VAT_SELLER_REGISTERED");
37460
+ if (code === "standard_25_5" && documentDate < "2024-09-01")
37461
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "25,5 % ei ole voimassa ennen 1.9.2024");
37462
+ if (code === "standard_13_5" && documentDate < "2026-01-01")
37463
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "13,5 % ei ole voimassa ennen 1.1.2026");
37464
+ if ((code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu") && customer.customer_kind !== "business")
37465
+ return invoiceDraftError("VAT_REQUIRES_BUSINESS_CUSTOMER");
37466
+ if ((code === "reverse_construction" || code === "reverse_scrap") && !isFinnishBusinessId(customer.business_id ?? ""))
37467
+ return invoiceDraftError("VAT_REQUIRES_FINNISH_BUSINESS_ID");
37468
+ const country = (customer.country ?? "").trim().toUpperCase();
37405
37469
  if (code === "reverse_eu_service" || code === "intra_eu_goods") {
37406
- const country = (customer?.country ?? "").trim().toUpperCase();
37407
- const vatNumber = normalizedVatNumber(customer?.vat_number);
37470
+ const vatNumber = (customer.vat_number ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
37408
37471
  const expectedPrefix = country === "GR" ? "EL" : country;
37409
- if (!EU_COUNTRY_CODES.has(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix)) {
37410
- return "EU sale requires a non-Finnish EU country and a matching buyer VAT number.";
37411
- }
37472
+ if (!EU_COUNTRY_CODES.includes(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix))
37473
+ return invoiceDraftError("VAT_REQUIRES_EU_VAT_NUMBER");
37412
37474
  }
37413
- if (code === "export_outside_eu" || code === "service_outside_eu") {
37414
- const country = (customer?.country ?? "").trim().toUpperCase();
37415
- if (!country || EU_COUNTRY_CODES.has(country)) {
37416
- return "Export outside the EU requires a buyer country outside the EU.";
37417
- }
37475
+ if ((code === "export_outside_eu" || code === "service_outside_eu") && (!country || EU_COUNTRY_CODES.includes(country)))
37476
+ return invoiceDraftError("VAT_REQUIRES_NON_EU_COUNTRY");
37477
+ return null;
37478
+ }
37479
+ function inputError(input2) {
37480
+ if (!dateParts(input2.invoiceDate)) return "laskun p\xE4iv\xE4m\xE4\xE4r\xE4 ei ole kelvollinen";
37481
+ if (input2.lines.length < 1 || input2.lines.length > INVOICE_DRAFT_MAX_LINES)
37482
+ return `laskulla pit\xE4\xE4 olla 1\u2013${INVOICE_DRAFT_MAX_LINES} rivi\xE4`;
37483
+ const term = input2.paymentTermDays;
37484
+ if (term !== void 0 && (!Number.isInteger(term) || term < 0 || term > 365))
37485
+ return "maksuehdon pit\xE4\xE4 olla 0\u2013365 kokonaista p\xE4iv\xE4\xE4";
37486
+ if ((input2.notes ?? "").length > 2e3) return "lis\xE4tiedot ovat yli 2000 merkki\xE4";
37487
+ for (let index = 0; index < input2.lines.length; index += 1) {
37488
+ const line = input2.lines[index];
37489
+ const row = `rivi ${index + 1}`;
37490
+ if (!line) return `${row} puuttuu`;
37491
+ if (!line.description.trim() || line.description.length > 500)
37492
+ return `${row}: kuvauksen pit\xE4\xE4 olla 1\u2013500 merkki\xE4`;
37493
+ if (line.unit.length > 20) return `${row}: yksikk\xF6 on yli 20 merkki\xE4`;
37494
+ if (!Number.isFinite(line.quantity) || line.quantity <= 0 || line.quantity > 1e6)
37495
+ return `${row}: m\xE4\xE4r\xE4n pit\xE4\xE4 olla yli 0 ja enint\xE4\xE4n 1 000 000`;
37496
+ if (!Number.isFinite(line.unitPrice) || line.unitPrice < 0 || line.unitPrice > 1e9)
37497
+ return `${row}: yksikk\xF6hinnan pit\xE4\xE4 olla 0\u20131 000 000 000 \u20AC`;
37498
+ if (line.quantity * line.unitPrice > 1e12) return `${row}: rivin summa on liian suuri`;
37418
37499
  }
37419
37500
  return null;
37420
37501
  }
37502
+ function prepareInvoiceDraft(input2, facts) {
37503
+ const invalid = inputError(input2);
37504
+ if (invalid) return { ok: false, error: invoiceDraftError("INVALID_INPUT", invalid) };
37505
+ const { customer } = facts;
37506
+ const lines = [];
37507
+ for (const [index, line] of input2.lines.entries()) {
37508
+ const treatment = resolveInvoiceVatTreatment(line.vatRate, line.vatTreatmentCode);
37509
+ if ("message" in treatment) return { ok: false, error: treatment };
37510
+ const ineligible = invoiceVatTreatmentError(
37511
+ treatment.code,
37512
+ facts.sellerAlvRegistered,
37513
+ customer,
37514
+ input2.invoiceDate
37515
+ );
37516
+ if (ineligible) return { ok: false, error: ineligible };
37517
+ const amounts = calculateLineTotals(
37518
+ line.quantity,
37519
+ line.unitPrice,
37520
+ treatment.rate,
37521
+ input2.priceEntryMode
37522
+ );
37523
+ lines.push({
37524
+ line_number: index + 1,
37525
+ description: line.description,
37526
+ quantity: line.quantity,
37527
+ unit: line.unit,
37528
+ unit_price: line.unitPrice,
37529
+ entered_unit_price: line.unitPrice,
37530
+ vat_rate: treatment.rate,
37531
+ vat_treatment_code: treatment.code,
37532
+ vat_treatment_overridden: true,
37533
+ price_entry_mode: input2.priceEntryMode,
37534
+ default_vat_treatment_code: treatment.code,
37535
+ reverse_charge_confirmed: input2.reverseChargeConfirmed ?? false,
37536
+ eu_goods_transport_confirmed: input2.euGoodsTransportConfirmed ?? false,
37537
+ subtotal: amounts.subtotal,
37538
+ vat_amount: amounts.vatAmount,
37539
+ total: amounts.total
37540
+ });
37541
+ }
37542
+ const totals = calculateInvoiceTotals(lines);
37543
+ return {
37544
+ ok: true,
37545
+ draft: {
37546
+ customerId: customer.id,
37547
+ customerName: customer.name,
37548
+ invoiceDate: input2.invoiceDate,
37549
+ dueDate: addDaysIso(
37550
+ input2.invoiceDate,
37551
+ input2.paymentTermDays ?? customer.payment_term_days ?? 14
37552
+ ),
37553
+ notes: input2.notes ?? "",
37554
+ lines,
37555
+ subtotal: totals.subtotal,
37556
+ vatTotal: totals.vatTotal,
37557
+ total: totals.total
37558
+ }
37559
+ };
37560
+ }
37561
+ function invoiceDraftRpcArgs(companyId2, draft, audit) {
37562
+ return {
37563
+ p_company_id: companyId2,
37564
+ p_customer_id: draft.customerId,
37565
+ p_invoice_number: null,
37566
+ p_reference_number: null,
37567
+ p_invoice_date: draft.invoiceDate,
37568
+ p_due_date: draft.dueDate,
37569
+ p_status: "draft",
37570
+ p_subtotal: draft.subtotal,
37571
+ p_vat_total: draft.vatTotal,
37572
+ p_total: draft.total,
37573
+ p_notes: draft.notes,
37574
+ p_lines: draft.lines,
37575
+ ...audit ? { p_audit_action: audit.action, p_audit_details: audit.details } : {}
37576
+ };
37577
+ }
37578
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
37579
+ var CONFIRMED_ERROR_CODE = /^(?:[0-9A-Z]{5}|PGRST\d{3})$/;
37580
+ function invoiceDraftWriteOutcome(response) {
37581
+ const { data, error: error61 } = response;
37582
+ if (error61) {
37583
+ const code = error61.code ?? "";
37584
+ if (!CONFIRMED_ERROR_CODE.test(code))
37585
+ return { ok: false, error: invoiceDraftError("OUTCOME_UNKNOWN") };
37586
+ if ((error61.message ?? "").startsWith("AAL2_REQUIRED"))
37587
+ return { ok: false, error: invoiceDraftError("MFA_REQUIRED") };
37588
+ const mapped = {
37589
+ "23505": "INVOICE_NUMBER_CONFLICT",
37590
+ "55000": "FISCAL_YEAR_CLOSED",
37591
+ "42501": "PERMISSION_DENIED",
37592
+ "23514": "INVALID_LINE"
37593
+ };
37594
+ return { ok: false, error: invoiceDraftError(mapped[code] ?? "WRITE_REJECTED") };
37595
+ }
37596
+ if (typeof data !== "string" || !UUID.test(data))
37597
+ return { ok: false, error: invoiceDraftError("OUTCOME_UNKNOWN") };
37598
+ return { ok: true, invoiceId: data };
37599
+ }
37421
37600
 
37422
37601
  // src/lib/invoice-balances.ts
37423
37602
  var CREDIT_PAGE_SIZE = 500;
@@ -37455,19 +37634,6 @@ function calculateInvoiceBalance(total2, payments, creditedTotal) {
37455
37634
  }
37456
37635
 
37457
37636
  // src/tools/invoices.ts
37458
- function generateReferenceNumber(baseNumber) {
37459
- const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
37460
- if (digits.length < 3 || digits.length > 19) {
37461
- throw new Error("Base number must be 3-19 digits without leading zeros");
37462
- }
37463
- const weights = [7, 3, 1];
37464
- let sum = 0;
37465
- for (let i = digits.length - 1, w = 0; i >= 0; i--, w++) {
37466
- sum += parseInt(digits[i]) * weights[w % 3];
37467
- }
37468
- const checkDigit = (10 - sum % 10) % 10;
37469
- return digits + checkDigit;
37470
- }
37471
37637
  var LIST_INVOICES_ROW_SHAPE = {
37472
37638
  id: external_exports.string().uuid(),
37473
37639
  document_type: external_exports.enum(["invoice", "credit_note"]),
@@ -37519,6 +37685,96 @@ var GET_INVOICE_OUTPUT_SHAPE = {
37519
37685
  credited_total: external_exports.number(),
37520
37686
  remaining_balance: external_exports.number()
37521
37687
  };
37688
+ var CREATE_INVOICE_INPUT_SHAPE = {
37689
+ customerId: external_exports.string().uuid().describe("Customer UUID"),
37690
+ 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)"),
37692
+ notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
37693
+ priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
37694
+ reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
37695
+ euGoodsTransportConfirmed: external_exports.boolean().default(false).describe("Explicit confirmation that intra-EU goods are transported from Finland to another EU country"),
37696
+ lines: external_exports.array(
37697
+ external_exports.object({
37698
+ description: external_exports.string().min(1).max(500).describe("Line item description"),
37699
+ // DB enforces CHECK (quantity > 0); reject 0/negative/Infinity at
37700
+ // the boundary so the agent gets a clear error instead of an
37701
+ // opaque 23514 deep in create_invoice_with_lines.
37702
+ quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
37703
+ unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
37704
+ 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"),
37706
+ 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
+ })
37708
+ ).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
37709
+ idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37710
+ };
37711
+ async function createInvoiceDraftCommand(ctx, params) {
37712
+ const fail = (error62) => toErrorContent(`${error62.code}: ${error62.message}`);
37713
+ const { data: customer, error: customerError } = await ctx.supabase.from("customers").select(INVOICE_DRAFT_CUSTOMER_COLUMNS).eq("id", params.customerId).eq("company_id", ctx.companyId).maybeSingle();
37714
+ if (customerError) return fail(invoiceDraftError("LOOKUP_FAILED"));
37715
+ if (!customer) {
37716
+ return fail(
37717
+ invoiceDraftError("CUSTOMER_NOT_FOUND", "hae tunniste kirjapro_list_customers-ty\xF6kalulla")
37718
+ );
37719
+ }
37720
+ const { data: company, error: companyError } = await ctx.supabase.from("companies").select("alv_registered").eq("id", ctx.companyId).single();
37721
+ if (companyError || !company) return fail(invoiceDraftError("LOOKUP_FAILED"));
37722
+ const prepared = prepareInvoiceDraft(
37723
+ {
37724
+ invoiceDate: params.invoiceDate,
37725
+ paymentTermDays: params.paymentTermDays,
37726
+ priceEntryMode: params.priceEntryMode,
37727
+ notes: params.notes,
37728
+ reverseChargeConfirmed: params.reverseChargeConfirmed,
37729
+ euGoodsTransportConfirmed: params.euGoodsTransportConfirmed,
37730
+ lines: params.lines
37731
+ },
37732
+ { sellerAlvRegistered: company.alv_registered, customer }
37733
+ );
37734
+ if (!prepared.ok) return fail(prepared.error);
37735
+ const { draft } = prepared;
37736
+ const { data, error: error61, status } = await ctx.supabase.rpc(
37737
+ "create_invoice_with_lines",
37738
+ // NULL number/reference = DB allocation; generated types do not mark them nullable.
37739
+ invoiceDraftRpcArgs(ctx.companyId, draft)
37740
+ );
37741
+ const outcome = invoiceDraftWriteOutcome({ data, error: error61 });
37742
+ if (isWriteOutcomeUnknown(error61, status) || !outcome.ok && outcome.error.code === "OUTCOME_UNKNOWN") {
37743
+ return toWriteOutcomeUnknown("kirjapro_create_invoice", "kirjapro_list_invoices");
37744
+ }
37745
+ if (!outcome.ok) {
37746
+ console.error("[kirjapro-mcp] create_invoice rejected:", { code: error61?.code });
37747
+ return fail(outcome.error);
37748
+ }
37749
+ const { data: invoice, error: fetchError } = await ctx.supabase.from("invoices").select(
37750
+ "id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at"
37751
+ ).eq("id", outcome.invoiceId).eq("company_id", ctx.companyId).single();
37752
+ if (fetchError || !invoice) {
37753
+ return toResult({
37754
+ message: "Lasku luotiin, mutta sen tietojen haku ep\xE4onnistui.",
37755
+ invoice: {
37756
+ id: outcome.invoiceId,
37757
+ status: "draft",
37758
+ invoice_date: draft.invoiceDate,
37759
+ due_date: draft.dueDate,
37760
+ subtotal: draft.subtotal,
37761
+ vat_total: draft.vatTotal,
37762
+ total: draft.total,
37763
+ customer_name: customer.name,
37764
+ lines: draft.lines
37765
+ },
37766
+ warning: "Tarkista lasku kirjapro_get_invoice-ty\xF6kalulla."
37767
+ });
37768
+ }
37769
+ return toResult({
37770
+ message: `Invoice ${invoice.invoice_number} created for ${customer.name}`,
37771
+ invoice: {
37772
+ ...invoice,
37773
+ customer_name: customer.name,
37774
+ lines: draft.lines
37775
+ }
37776
+ });
37777
+ }
37522
37778
  function registerInvoiceTools(server2) {
37523
37779
  server2.registerTool(
37524
37780
  "kirjapro_list_invoices",
@@ -37570,196 +37826,18 @@ function registerInvoiceTools(server2) {
37570
37826
  "kirjapro_create_invoice",
37571
37827
  {
37572
37828
  description: "Create a new sales invoice with line items. Automatically generates invoice number, reference number (viitenumero), and calculates VAT. Returns the complete invoice.",
37573
- inputSchema: {
37574
- customerId: external_exports.string().uuid().describe("Customer UUID"),
37575
- invoiceDate: ISO_DATE_SCHEMA.describe("Invoice date (YYYY-MM-DD)"),
37576
- paymentTermDays: external_exports.number().min(0).max(365).default(14).describe("Payment term in days (default 14)"),
37577
- notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
37578
- priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
37579
- reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
37580
- euGoodsTransportConfirmed: external_exports.boolean().default(false).describe("Explicit confirmation that intra-EU goods are transported from Finland to another EU country"),
37581
- lines: external_exports.array(
37582
- external_exports.object({
37583
- description: external_exports.string().min(1).max(500).describe("Line item description"),
37584
- // DB enforces CHECK (quantity > 0); reject 0/negative/Infinity at
37585
- // the boundary so the agent gets a clear error instead of an
37586
- // opaque 23514 deep in create_invoice_with_lines.
37587
- quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
37588
- unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
37589
- unitPrice: external_exports.number().finite().min(0).max(1e9).describe("Entered unit price in EUR (must be >= 0)"),
37590
- vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).default("25.5").describe("VAT rate"),
37591
- vatTreatmentCode: external_exports.enum([
37592
- "standard_25_5",
37593
- "standard_13_5",
37594
- "standard_10",
37595
- "not_vat_registered",
37596
- "reverse_construction",
37597
- "reverse_scrap",
37598
- "reverse_eu_service",
37599
- "export_outside_eu",
37600
- "service_outside_eu",
37601
- "intra_eu_goods",
37602
- "exempt_healthcare",
37603
- "exempt_social_welfare",
37604
- "exempt_education",
37605
- "exempt_financial",
37606
- "exempt_insurance",
37607
- "exempt_other"
37608
- ]).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.")
37609
- })
37610
- ).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
37611
- idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37612
- },
37829
+ inputSchema: CREATE_INVOICE_INPUT_SHAPE,
37613
37830
  annotations: WRITE_ANNOTATIONS
37614
37831
  },
37615
37832
  async (params) => {
37616
- const supabase2 = getSupabase();
37617
- const companyId2 = getCompanyId();
37833
+ const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
37618
37834
  return withIdempotency(
37619
- supabase2,
37620
- companyId2,
37835
+ ctx.supabase,
37836
+ ctx.companyId,
37621
37837
  "kirjapro_create_invoice",
37622
37838
  params.idempotencyKey,
37623
37839
  params,
37624
- async () => {
37625
- const { data: customer, error: custError } = await supabase2.from("customers").select("id, name, customer_kind, business_id, vat_number, country").eq("id", params.customerId).eq("company_id", companyId2).single();
37626
- if (custError || !customer) {
37627
- return toErrorContent(
37628
- `Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`
37629
- );
37630
- }
37631
- const { data: company, error: companyError } = await supabase2.from("companies").select("alv_registered").eq("id", companyId2).single();
37632
- if (companyError || !company) {
37633
- return toErrorContent("Failed to verify the seller VAT registration status.");
37634
- }
37635
- const { data: invoiceNumber, error: numError } = await supabase2.rpc(
37636
- "get_next_invoice_number",
37637
- {
37638
- p_company_id: companyId2
37639
- }
37640
- );
37641
- if (numError || !invoiceNumber) {
37642
- return toErrorContent(
37643
- numError ? sanitizeDbError("Failed to generate invoice number", numError) : "Failed to generate invoice number."
37644
- );
37645
- }
37646
- const referenceNumber = generateReferenceNumber(
37647
- invoiceNumber.replace(/\D/g, "")
37648
- );
37649
- const dueDateStr = addDaysToIsoDate(
37650
- params.invoiceDate,
37651
- params.paymentTermDays
37652
- );
37653
- const treatmentRates = {
37654
- standard_25_5: 25.5,
37655
- standard_13_5: 13.5,
37656
- standard_10: 10,
37657
- not_vat_registered: 0,
37658
- reverse_construction: 0,
37659
- reverse_scrap: 0,
37660
- reverse_eu_service: 0,
37661
- export_outside_eu: 0,
37662
- service_outside_eu: 0,
37663
- intra_eu_goods: 0,
37664
- exempt_healthcare: 0,
37665
- exempt_social_welfare: 0,
37666
- exempt_education: 0,
37667
- exempt_financial: 0,
37668
- exempt_insurance: 0,
37669
- exempt_other: 0
37670
- };
37671
- const resolvedTreatments = params.lines.map((line) => {
37672
- const inputRate = parseFloat(line.vatRate);
37673
- const code = line.vatTreatmentCode ?? (inputRate === 25.5 ? "standard_25_5" : inputRate === 13.5 ? "standard_13_5" : inputRate === 10 ? "standard_10" : null);
37674
- if (!code) return { error: "0 % VAT requires a structured vatTreatmentCode." };
37675
- const eligibilityError = validateVatTreatmentEligibility({
37676
- code,
37677
- sellerAlvRegistered: company.alv_registered,
37678
- customer,
37679
- documentDate: params.invoiceDate
37680
- });
37681
- if (eligibilityError) return { error: eligibilityError };
37682
- return { code, rate: treatmentRates[code] ?? inputRate };
37683
- });
37684
- const treatmentError = resolvedTreatments.find((item) => "error" in item);
37685
- if (treatmentError && "error" in treatmentError) {
37686
- return toErrorContent(treatmentError.error);
37687
- }
37688
- const calculatedLines = params.lines.map((line, i) => {
37689
- const resolved = resolvedTreatments[i];
37690
- if (!resolved || !("code" in resolved)) throw new Error("VAT treatment validation failed");
37691
- const vatRate = resolved.rate;
37692
- const vatTreatmentCode = resolved.code;
37693
- const gross = roundCents(line.quantity * line.unitPrice);
37694
- const vatAmount = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross * vatRate / (100 + vatRate)) : roundCents(gross * (vatRate / 100));
37695
- const subtotal2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross - vatAmount) : gross;
37696
- const total3 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? gross : roundCents(subtotal2 + vatAmount);
37697
- return {
37698
- line_number: i + 1,
37699
- description: line.description,
37700
- quantity: line.quantity,
37701
- unit: line.unit,
37702
- unit_price: line.unitPrice,
37703
- entered_unit_price: line.unitPrice,
37704
- vat_rate: vatRate,
37705
- vat_treatment_code: vatTreatmentCode,
37706
- vat_treatment_overridden: true,
37707
- price_entry_mode: params.priceEntryMode,
37708
- default_vat_treatment_code: vatTreatmentCode,
37709
- reverse_charge_confirmed: params.reverseChargeConfirmed,
37710
- reverse_charge_confirmation_text: params.reverseChargeConfirmed ? "Buyer qualification explicitly confirmed through MCP invoice creation." : null,
37711
- eu_goods_transport_confirmed: params.euGoodsTransportConfirmed,
37712
- subtotal: subtotal2,
37713
- vat_amount: vatAmount,
37714
- total: total3
37715
- };
37716
- });
37717
- const subtotal = roundCents(
37718
- calculatedLines.reduce((s, l) => s + l.subtotal, 0)
37719
- );
37720
- const vatTotal = roundCents(
37721
- calculatedLines.reduce((s, l) => s + l.vat_amount, 0)
37722
- );
37723
- const total2 = roundCents(subtotal + vatTotal);
37724
- const { data: invoiceId, error: invError } = await supabase2.rpc(
37725
- "create_invoice_with_lines",
37726
- {
37727
- p_company_id: companyId2,
37728
- p_customer_id: params.customerId,
37729
- p_invoice_number: invoiceNumber,
37730
- p_reference_number: referenceNumber,
37731
- p_invoice_date: params.invoiceDate,
37732
- p_due_date: dueDateStr,
37733
- p_status: "draft",
37734
- p_subtotal: subtotal,
37735
- p_vat_total: vatTotal,
37736
- p_total: total2,
37737
- p_notes: params.notes || "",
37738
- p_lines: calculatedLines
37739
- }
37740
- );
37741
- if (invError || !invoiceId) {
37742
- return toErrorContent(
37743
- invError ? sanitizeDbError("Failed to create invoice", invError) : "Failed to create invoice."
37744
- );
37745
- }
37746
- const { data: invoice, error: fetchError } = await supabase2.from("invoices").select(
37747
- "id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at"
37748
- ).eq("id", invoiceId).eq("company_id", companyId2).single();
37749
- if (fetchError || !invoice) {
37750
- return toErrorContent(
37751
- fetchError ? sanitizeDbError("Invoice created but fetch failed", fetchError) : "Invoice created but could not be retrieved."
37752
- );
37753
- }
37754
- return toResult({
37755
- message: `Invoice ${invoiceNumber} created for ${customer.name}`,
37756
- invoice: {
37757
- ...invoice,
37758
- customer_name: customer.name,
37759
- lines: calculatedLines
37760
- }
37761
- });
37762
- }
37840
+ () => createInvoiceDraftCommand(ctx, params)
37763
37841
  );
37764
37842
  }
37765
37843
  );
@@ -37772,7 +37850,8 @@ function registerInvoiceTools(server2) {
37772
37850
  channel: external_exports.enum(["email", "einvoice"]).describe("Delivery channel: email or einvoice"),
37773
37851
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37774
37852
  },
37775
- annotations: WRITE_ANNOTATIONS
37853
+ // Delivers through email or Maventa: an external effect (openWorldHint).
37854
+ annotations: WRITE_EXTERNAL_ANNOTATIONS
37776
37855
  },
37777
37856
  async (params) => {
37778
37857
  const supabase2 = getSupabase();
@@ -38132,6 +38211,43 @@ function registerInvoiceTools(server2) {
38132
38211
  );
38133
38212
  }
38134
38213
 
38214
+ // src/finnish-business-id.ts
38215
+ function validateFinnishBusinessIdCore(input2) {
38216
+ const cleaned = input2.replace(/\s/g, "").toUpperCase();
38217
+ const match = cleaned.match(/^(\d{7})-(\d)$/);
38218
+ if (!match) return { valid: false, reason: "format" };
38219
+ const digits = match[1];
38220
+ const actualCheckDigit = Number(match[2]);
38221
+ if (!digits || !Number.isInteger(actualCheckDigit)) {
38222
+ return { valid: false, reason: "format" };
38223
+ }
38224
+ const weights = [7, 9, 10, 5, 8, 4, 2];
38225
+ let sum = 0;
38226
+ for (let index = 0; index < weights.length; index += 1) {
38227
+ const digit = digits[index];
38228
+ const weight = weights[index];
38229
+ if (digit === void 0 || weight === void 0) {
38230
+ return { valid: false, reason: "format" };
38231
+ }
38232
+ sum += Number(digit) * weight;
38233
+ }
38234
+ const remainder = sum % 11;
38235
+ if (remainder === 1) return { valid: false, reason: "remainder_one" };
38236
+ const expectedCheckDigit = remainder === 0 ? 0 : 11 - remainder;
38237
+ if (actualCheckDigit !== expectedCheckDigit) {
38238
+ return {
38239
+ valid: false,
38240
+ reason: "checksum",
38241
+ expectedCheckDigit,
38242
+ actualCheckDigit
38243
+ };
38244
+ }
38245
+ return { valid: true };
38246
+ }
38247
+ function isValidFinnishBusinessId(input2) {
38248
+ return validateFinnishBusinessIdCore(input2).valid;
38249
+ }
38250
+
38135
38251
  // src/tools/customers.ts
38136
38252
  var CUSTOMER_STATS_RPC_SCHEMA = external_exports.object({
38137
38253
  invoice_count: external_exports.number().int().nonnegative(),
@@ -39695,7 +39811,7 @@ function validateFinnishIBAN(input2) {
39695
39811
  }
39696
39812
  return { valid: true };
39697
39813
  }
39698
- function generateReferenceNumber2(baseNumber) {
39814
+ function generateReferenceNumber(baseNumber) {
39699
39815
  const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
39700
39816
  if (digits.length < 3 || digits.length > 19) {
39701
39817
  return { reference: "", error: "Base number must be 3-19 digits without leading zeros." };
@@ -39756,7 +39872,7 @@ function registerValidationTools(server2) {
39756
39872
  annotations: READ_ONLY_ANNOTATIONS
39757
39873
  },
39758
39874
  async (params) => {
39759
- const result = generateReferenceNumber2(params.baseNumber);
39875
+ const result = generateReferenceNumber(params.baseNumber);
39760
39876
  return toResult({
39761
39877
  input: params.baseNumber,
39762
39878
  ...result
@@ -41501,6 +41617,81 @@ function registerBookingTools(server2) {
41501
41617
  );
41502
41618
  }
41503
41619
 
41620
+ // src/vat.ts
41621
+ var EU_COUNTRY_CODES2 = /* @__PURE__ */ new Set([
41622
+ "AT",
41623
+ "BE",
41624
+ "BG",
41625
+ "HR",
41626
+ "CY",
41627
+ "CZ",
41628
+ "DE",
41629
+ "DK",
41630
+ "EE",
41631
+ "ES",
41632
+ "FI",
41633
+ "FR",
41634
+ "GR",
41635
+ "HU",
41636
+ "IE",
41637
+ "IT",
41638
+ "LT",
41639
+ "LU",
41640
+ "LV",
41641
+ "MT",
41642
+ "NL",
41643
+ "PL",
41644
+ "PT",
41645
+ "RO",
41646
+ "SE",
41647
+ "SI",
41648
+ "SK"
41649
+ ]);
41650
+ function normalizedVatNumber(value) {
41651
+ return (value ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
41652
+ }
41653
+ function validateVatTreatmentEligibility({
41654
+ code,
41655
+ sellerAlvRegistered,
41656
+ customer,
41657
+ documentDate
41658
+ }) {
41659
+ if (!sellerAlvRegistered && code !== "not_vat_registered") {
41660
+ return "A seller outside the VAT register can only use not_vat_registered.";
41661
+ }
41662
+ if (sellerAlvRegistered && code === "not_vat_registered") {
41663
+ return "A VAT-registered seller cannot use not_vat_registered.";
41664
+ }
41665
+ if (code === "standard_25_5" && documentDate < "2024-09-01") {
41666
+ return "standard_25_5 is not valid before 2024-09-01.";
41667
+ }
41668
+ if (code === "standard_13_5" && documentDate < "2026-01-01") {
41669
+ return "standard_13_5 is not valid before 2026-01-01.";
41670
+ }
41671
+ const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu";
41672
+ if (requiresBusiness && customer?.customer_kind !== "business") {
41673
+ return "The selected VAT treatment requires a business customer.";
41674
+ }
41675
+ if ((code === "reverse_construction" || code === "reverse_scrap") && !isValidFinnishBusinessId(customer?.business_id ?? "")) {
41676
+ return "Domestic reverse charge requires a Finnish buyer business ID in the form 1234567-8.";
41677
+ }
41678
+ if (code === "reverse_eu_service" || code === "intra_eu_goods") {
41679
+ const country = (customer?.country ?? "").trim().toUpperCase();
41680
+ const vatNumber = normalizedVatNumber(customer?.vat_number);
41681
+ const expectedPrefix = country === "GR" ? "EL" : country;
41682
+ if (!EU_COUNTRY_CODES2.has(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix)) {
41683
+ return "EU sale requires a non-Finnish EU country and a matching buyer VAT number.";
41684
+ }
41685
+ }
41686
+ if (code === "export_outside_eu" || code === "service_outside_eu") {
41687
+ const country = (customer?.country ?? "").trim().toUpperCase();
41688
+ if (!country || EU_COUNTRY_CODES2.has(country)) {
41689
+ return "Export outside the EU requires a buyer country outside the EU.";
41690
+ }
41691
+ }
41692
+ return null;
41693
+ }
41694
+
41504
41695
  // src/tools/offers.ts
41505
41696
  var OFFER_DETAIL_OUTPUT_SHAPE = {
41506
41697
  id: external_exports.string(),
@@ -42962,7 +43153,7 @@ var VAT_COMPLIANCE = {
42962
43153
  ],
42963
43154
  booking: "D expense/asset for net amount plus non-deductible self-assessed VAT; D ALV-saamiset only for deductible VAT; C bank/payable for supplier amount; C ALV-velka for full self-assessed VAT. Use the canonical VAT purchase command, not unlinked VAT account rows.",
42964
43155
  note: "Net VAT effect is zero only with a full deduction. Report the full assessed VAT and only the deductible share. The seller does not charge VAT on a reverse-charge invoice; this does not remove the buyer\u2019s VAT liability.",
42965
- implementation_limits: "The MCP purchase shortcut currently has no deduction-share parameter and does not support partial-deduction reverse charge. Do not use its default full deduction for that case: this example describes the accounting rule, not an added tool capability.",
43156
+ implementation_limits: "kirjapro_book_purchase (full surface) takes deductionPercent 0\u2013100 with a documented deductionReason, including for reverse charge: the full assessed VAT is payable (ALV-velka) and only the deductible share reaches ALV-saamiset and field 307. The rest is expense. Entertainment and non-registered buyers use 0.",
42966
43157
  example: { net_purchase: 100, rate: FINNISH_RATES_2026.vat.standard, deductible_share: "60%", output_vat: exampleOutputVat, deductible_vat: exampleDeductibleVat, non_deductible_vat: exampleNonDeductibleVat, expense: roundCents(100 + exampleNonDeductibleVat), payable_vat: exampleNonDeductibleVat },
42967
43158
  omavero_fields: "EU goods: 305 (VAT) / 313 (base); EU services: 306 / 314; services from outside the EU: VAT in 301-303 by rate (no base field); imports: 304 / 310; domestic construction/scrap purchases: 318 / 320, sales 319. The deductible part of each is in 307."
42968
43159
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirjapro/mcp-server",
3
- "version": "0.12.0",
3
+ "version": "0.13.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.12.0",
5
+ "version": "0.13.0",
6
6
  "homepage": "https://kirjapro.fi",
7
7
  "repository": "https://github.com/Jaollo/kirja-pro",
8
8
  "license": "MIT",
@@ -186,7 +186,7 @@
186
186
  },
187
187
  {
188
188
  "name": "kirjapro_send_invoice",
189
- "hash": "ec584d23c747d458"
189
+ "hash": "9f714a1e932b6090"
190
190
  },
191
191
  {
192
192
  "name": "kirjapro_send_offer",