@kirjapro/mcp-server 0.11.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/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.11.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,57 +37439,170 @@ 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";
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") {
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;
37424
37603
  async function fetchFinalizedCreditTotal(supabase2, companyId2, invoiceId) {
37425
37604
  let offset = 0;
37426
- let total = 0;
37605
+ let total2 = 0;
37427
37606
  let expectedCount;
37428
37607
  for (; ; ) {
37429
37608
  const { data, count, error: error61 } = await supabase2.from("invoices").select("id, total", { count: "exact" }).eq("company_id", companyId2).eq("document_type", "credit_note").eq("status", "sent").eq("original_invoice_id", invoiceId).order("id", { ascending: true }).range(offset, offset + CREDIT_PAGE_SIZE - 1);
@@ -37441,33 +37620,20 @@ async function fetchFinalizedCreditTotal(supabase2, companyId2, invoiceId) {
37441
37620
  if (!Number.isFinite(credit.total)) {
37442
37621
  return { total: null, error: { message: "A finalized credit has an invalid amount." } };
37443
37622
  }
37444
- total += credit.total;
37623
+ total2 += credit.total;
37445
37624
  }
37446
37625
  offset += data.length;
37447
- if (offset === count) return { total: roundCents(total), error: null };
37626
+ if (offset === count) return { total: roundCents(total2), error: null };
37448
37627
  }
37449
37628
  }
37450
- function calculateInvoiceBalance(total, payments, creditedTotal) {
37629
+ function calculateInvoiceBalance(total2, payments, creditedTotal) {
37451
37630
  const paidTotal = roundCents(
37452
37631
  (payments ?? []).reduce((sum, payment) => sum + (payment.amount || 0), 0)
37453
37632
  );
37454
- return { paidTotal, remainingBalance: roundCents(total - paidTotal - creditedTotal) };
37633
+ return { paidTotal, remainingBalance: roundCents(total2 - paidTotal - creditedTotal) };
37455
37634
  }
37456
37635
 
37457
37636
  // src/tools/invoices.ts
37458
- function generateReferenceNumber(baseNumber) {
37459
- const digits = baseNumber.replace(/\D/g, "");
37460
- if (digits.length < 1 || digits.length > 19) {
37461
- throw new Error("Base number must be 1-19 digits");
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,194 +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
- "intra_eu_goods",
37601
- "exempt_healthcare",
37602
- "exempt_social_welfare",
37603
- "exempt_education",
37604
- "exempt_financial",
37605
- "exempt_insurance",
37606
- "exempt_other"
37607
- ]).optional().describe("Structured VAT treatment. Required when VAT rate is 0.")
37608
- })
37609
- ).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
37610
- idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37611
- },
37829
+ inputSchema: CREATE_INVOICE_INPUT_SHAPE,
37612
37830
  annotations: WRITE_ANNOTATIONS
37613
37831
  },
37614
37832
  async (params) => {
37615
- const supabase2 = getSupabase();
37616
- const companyId2 = getCompanyId();
37833
+ const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
37617
37834
  return withIdempotency(
37618
- supabase2,
37619
- companyId2,
37835
+ ctx.supabase,
37836
+ ctx.companyId,
37620
37837
  "kirjapro_create_invoice",
37621
37838
  params.idempotencyKey,
37622
37839
  params,
37623
- async () => {
37624
- 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();
37625
- if (custError || !customer) {
37626
- return toErrorContent(
37627
- `Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`
37628
- );
37629
- }
37630
- const { data: company, error: companyError } = await supabase2.from("companies").select("alv_registered").eq("id", companyId2).single();
37631
- if (companyError || !company) {
37632
- return toErrorContent("Failed to verify the seller VAT registration status.");
37633
- }
37634
- const { data: invoiceNumber, error: numError } = await supabase2.rpc(
37635
- "get_next_invoice_number",
37636
- {
37637
- p_company_id: companyId2
37638
- }
37639
- );
37640
- if (numError || !invoiceNumber) {
37641
- return toErrorContent(
37642
- numError ? sanitizeDbError("Failed to generate invoice number", numError) : "Failed to generate invoice number."
37643
- );
37644
- }
37645
- const referenceNumber = generateReferenceNumber(
37646
- invoiceNumber.replace(/\D/g, "")
37647
- );
37648
- const dueDateStr = addDaysToIsoDate(
37649
- params.invoiceDate,
37650
- params.paymentTermDays
37651
- );
37652
- const treatmentRates = {
37653
- standard_25_5: 25.5,
37654
- standard_13_5: 13.5,
37655
- standard_10: 10,
37656
- not_vat_registered: 0,
37657
- reverse_construction: 0,
37658
- reverse_scrap: 0,
37659
- reverse_eu_service: 0,
37660
- export_outside_eu: 0,
37661
- intra_eu_goods: 0,
37662
- exempt_healthcare: 0,
37663
- exempt_social_welfare: 0,
37664
- exempt_education: 0,
37665
- exempt_financial: 0,
37666
- exempt_insurance: 0,
37667
- exempt_other: 0
37668
- };
37669
- const resolvedTreatments = params.lines.map((line) => {
37670
- const inputRate = parseFloat(line.vatRate);
37671
- const code = line.vatTreatmentCode ?? (inputRate === 25.5 ? "standard_25_5" : inputRate === 13.5 ? "standard_13_5" : inputRate === 10 ? "standard_10" : null);
37672
- if (!code) return { error: "0 % VAT requires a structured vatTreatmentCode." };
37673
- const eligibilityError = validateVatTreatmentEligibility({
37674
- code,
37675
- sellerAlvRegistered: company.alv_registered,
37676
- customer,
37677
- documentDate: params.invoiceDate
37678
- });
37679
- if (eligibilityError) return { error: eligibilityError };
37680
- return { code, rate: treatmentRates[code] ?? inputRate };
37681
- });
37682
- const treatmentError = resolvedTreatments.find((item) => "error" in item);
37683
- if (treatmentError && "error" in treatmentError) {
37684
- return toErrorContent(treatmentError.error);
37685
- }
37686
- const calculatedLines = params.lines.map((line, i) => {
37687
- const resolved = resolvedTreatments[i];
37688
- if (!resolved || !("code" in resolved)) throw new Error("VAT treatment validation failed");
37689
- const vatRate = resolved.rate;
37690
- const vatTreatmentCode = resolved.code;
37691
- const gross = roundCents(line.quantity * line.unitPrice);
37692
- const vatAmount = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross * vatRate / (100 + vatRate)) : roundCents(gross * (vatRate / 100));
37693
- const subtotal2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross - vatAmount) : gross;
37694
- const total2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? gross : roundCents(subtotal2 + vatAmount);
37695
- return {
37696
- line_number: i + 1,
37697
- description: line.description,
37698
- quantity: line.quantity,
37699
- unit: line.unit,
37700
- unit_price: line.unitPrice,
37701
- entered_unit_price: line.unitPrice,
37702
- vat_rate: vatRate,
37703
- vat_treatment_code: vatTreatmentCode,
37704
- vat_treatment_overridden: true,
37705
- price_entry_mode: params.priceEntryMode,
37706
- default_vat_treatment_code: vatTreatmentCode,
37707
- reverse_charge_confirmed: params.reverseChargeConfirmed,
37708
- reverse_charge_confirmation_text: params.reverseChargeConfirmed ? "Buyer qualification explicitly confirmed through MCP invoice creation." : null,
37709
- eu_goods_transport_confirmed: params.euGoodsTransportConfirmed,
37710
- subtotal: subtotal2,
37711
- vat_amount: vatAmount,
37712
- total: total2
37713
- };
37714
- });
37715
- const subtotal = roundCents(
37716
- calculatedLines.reduce((s, l) => s + l.subtotal, 0)
37717
- );
37718
- const vatTotal = roundCents(
37719
- calculatedLines.reduce((s, l) => s + l.vat_amount, 0)
37720
- );
37721
- const total = roundCents(subtotal + vatTotal);
37722
- const { data: invoiceId, error: invError } = await supabase2.rpc(
37723
- "create_invoice_with_lines",
37724
- {
37725
- p_company_id: companyId2,
37726
- p_customer_id: params.customerId,
37727
- p_invoice_number: invoiceNumber,
37728
- p_reference_number: referenceNumber,
37729
- p_invoice_date: params.invoiceDate,
37730
- p_due_date: dueDateStr,
37731
- p_status: "draft",
37732
- p_subtotal: subtotal,
37733
- p_vat_total: vatTotal,
37734
- p_total: total,
37735
- p_notes: params.notes || "",
37736
- p_lines: calculatedLines
37737
- }
37738
- );
37739
- if (invError || !invoiceId) {
37740
- return toErrorContent(
37741
- invError ? sanitizeDbError("Failed to create invoice", invError) : "Failed to create invoice."
37742
- );
37743
- }
37744
- const { data: invoice, error: fetchError } = await supabase2.from("invoices").select(
37745
- "id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at"
37746
- ).eq("id", invoiceId).eq("company_id", companyId2).single();
37747
- if (fetchError || !invoice) {
37748
- return toErrorContent(
37749
- fetchError ? sanitizeDbError("Invoice created but fetch failed", fetchError) : "Invoice created but could not be retrieved."
37750
- );
37751
- }
37752
- return toResult({
37753
- message: `Invoice ${invoiceNumber} created for ${customer.name}`,
37754
- invoice: {
37755
- ...invoice,
37756
- customer_name: customer.name,
37757
- lines: calculatedLines
37758
- }
37759
- });
37760
- }
37840
+ () => createInvoiceDraftCommand(ctx, params)
37761
37841
  );
37762
37842
  }
37763
37843
  );
@@ -37770,7 +37850,8 @@ function registerInvoiceTools(server2) {
37770
37850
  channel: external_exports.enum(["email", "einvoice"]).describe("Delivery channel: email or einvoice"),
37771
37851
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37772
37852
  },
37773
- annotations: WRITE_ANNOTATIONS
37853
+ // Delivers through email or Maventa: an external effect (openWorldHint).
37854
+ annotations: WRITE_EXTERNAL_ANNOTATIONS
37774
37855
  },
37775
37856
  async (params) => {
37776
37857
  const supabase2 = getSupabase();
@@ -38130,6 +38211,43 @@ function registerInvoiceTools(server2) {
38130
38211
  );
38131
38212
  }
38132
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
+
38133
38251
  // src/tools/customers.ts
38134
38252
  var CUSTOMER_STATS_RPC_SCHEMA = external_exports.object({
38135
38253
  invoice_count: external_exports.number().int().nonnegative(),
@@ -38695,9 +38813,9 @@ var LEGACY_REVERSE_CHARGE_WARNING = "Legacy reverse-charge invoice lines must be
38695
38813
  function alvReconciliationWarning(reconciliation) {
38696
38814
  if (!reconciliation || typeof reconciliation !== "object") return void 0;
38697
38815
  const r = reconciliation;
38698
- if (r.status !== "mismatch") return void 0;
38816
+ if (r.status !== "mismatch" && r.status !== "explained") return void 0;
38699
38817
  const amount = (value) => (Number(value) || 0).toFixed(2);
38700
- return `Ledger accounts 4210/2320 differ from the ALV report (output VAT difference ${amount(r.output_difference)} \u20AC, input VAT difference ${amount(r.input_difference)} \u20AC). Review reconciliation before filing.`;
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.`;
38701
38819
  }
38702
38820
  var BALANCE_SHEET_ROW_SHAPE = external_exports.object({
38703
38821
  account_number: external_exports.string(),
@@ -39693,10 +39811,10 @@ function validateFinnishIBAN(input2) {
39693
39811
  }
39694
39812
  return { valid: true };
39695
39813
  }
39696
- function generateReferenceNumber2(baseNumber) {
39697
- const digits = baseNumber.replace(/\D/g, "");
39698
- if (digits.length < 1 || digits.length > 19) {
39699
- return { reference: "", error: "Base number must be 1-19 digits." };
39814
+ function generateReferenceNumber(baseNumber) {
39815
+ const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
39816
+ if (digits.length < 3 || digits.length > 19) {
39817
+ return { reference: "", error: "Base number must be 3-19 digits without leading zeros." };
39700
39818
  }
39701
39819
  const weights = [7, 3, 1];
39702
39820
  let sum = 0;
@@ -39748,13 +39866,13 @@ function registerValidationTools(server2) {
39748
39866
  {
39749
39867
  description: "Generate a Finnish bank reference number (viitenumero) from a base number. Appends the 7-3-1 weighted check digit. No database access \u2014 pure computation.",
39750
39868
  inputSchema: {
39751
- baseNumber: external_exports.string().describe('Base number (1-19 digits, e.g. invoice number "00042")')
39869
+ baseNumber: external_exports.string().describe('Base number (3-19 digits excluding leading zeros, e.g. "20260042")')
39752
39870
  },
39753
39871
  outputSchema: REFERENCE_OUTPUT_SHAPE,
39754
39872
  annotations: READ_ONLY_ANNOTATIONS
39755
39873
  },
39756
39874
  async (params) => {
39757
- const result = generateReferenceNumber2(params.baseNumber);
39875
+ const result = generateReferenceNumber(params.baseNumber);
39758
39876
  return toResult({
39759
39877
  input: params.baseNumber,
39760
39878
  ...result
@@ -39797,6 +39915,392 @@ function registerFiscalYearTools(server2) {
39797
39915
  );
39798
39916
  }
39799
39917
 
39918
+ // src/compliance/purchase-vat-input.ts
39919
+ var PURCHASE_VAT_BASIS_SCHEMA = external_exports.object({
39920
+ version: external_exports.literal(1),
39921
+ kind: external_exports.enum([
39922
+ "supply",
39923
+ "supply_month",
39924
+ "advance_payment",
39925
+ "continuous_period_end",
39926
+ "continuous_year_end",
39927
+ "eu_acquisition_month",
39928
+ "customs_acceptance",
39929
+ "original_document_rate"
39930
+ ]),
39931
+ date: ISO_DATE_SCHEMA.optional(),
39932
+ month: external_exports.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).optional(),
39933
+ evidence_reference: external_exports.string().trim().min(1).max(2e3),
39934
+ correction_reason: external_exports.string().trim().min(1).max(500).optional()
39935
+ }).strict().describe("Confirmed rate-selection evidence, not the posting date. Supply/advance/period-end/customs use date; supply_month and EU acquisition use month. Advance date is receipt of funds by seller. original_document_rate requires correction_reason and the original document/period reference, no invented date.");
39936
+
39937
+ // src/compliance/regulatory-data.ts
39938
+ var MCP_REGISTRY = {
39939
+ schemaVersion: 1,
39940
+ jurisdiction: "FI",
39941
+ sources: {
39942
+ vat: {
39943
+ title: "Verohallinto: arvonlis\xE4veroprosentit",
39944
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/arvonlisaveroprosentit/"
39945
+ },
39946
+ "vat-2013-transition": {
39947
+ title: "Verohallinto A119/200/2012: arvonlis\xE4verokantojen muutos 1.1.2013 (virallisohjeen Edilex-kopio)",
39948
+ url: "https://www.edilex.fi/verohallinnon_ohjeet/2012_1220.html",
39949
+ watch: false
39950
+ },
39951
+ "vat-vehicles": {
39952
+ title: "Vero: ajoneuvojen ja k\xE4ytt\xF6kulujen ALV",
39953
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ajoneuvon-ja-sen-kayttokulujen-arvonlisavero/"
39954
+ },
39955
+ "vat-own-use": {
39956
+ title: "Vero: oman k\xE4yt\xF6n ja alivastikkeellisten luovutusten ALV",
39957
+ url: "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/47899/oman-k%C3%A4yt%C3%B6n-ja-alivastikkeellisten-luovutusten-arvonlis%C3%A4verotus/"
39958
+ },
39959
+ "vat-return": {
39960
+ title: "Vero: ALV-ilmoituksen t\xE4ytt\xF6ohje",
39961
+ url: "https://www.vero.fi/alv/tayttoohje"
39962
+ },
39963
+ "vat-small-business": {
39964
+ title: "Vero: arvonlis\xE4veroton v\xE4h\xE4inen toiminta",
39965
+ url: "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48658/arvonlisaveroton-vahainen-toiminta/"
39966
+ },
39967
+ "vat-recap": {
39968
+ title: "Vero: yhteenvetoilmoitus",
39969
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ulkomaankauppa/yhteisokauppa/yhteenvetoilmoitus-ilmoitusohjeet/"
39970
+ },
39971
+ "vat-invoices": {
39972
+ title: "Vero: laskutusvaatimukset",
39973
+ url: "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48090/laskutusvaatimukset-arvonlisaverotuksessa3/"
39974
+ },
39975
+ "einvoice-law": {
39976
+ title: "Valtiokonttori: verkkolaskulaki ja valtion vastaanotto",
39977
+ url: "https://www.valtiokonttori.fi/verkkolasku/"
39978
+ },
39979
+ payroll: {
39980
+ title: "Verohallinto: sosiaalivakuutusmaksut",
39981
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/yritys_tyonantajana/sosiaalivakuutusmaksut/"
39982
+ },
39983
+ unemployment: {
39984
+ title: "Ty\xF6llisyysrahasto: vahvistetut maksut 2026",
39985
+ url: "https://www.tyollisyysrahasto.fi/uutiset/vuoden-2026-tyottomyysvakuutusmaksut-on-vahvistettu/"
39986
+ },
39987
+ unemploymentScope: {
39988
+ title: "Ty\xF6llisyysrahasto: maksuvelvollisuus",
39989
+ url: "https://www.tyollisyysrahasto.fi/tyottomyysvakuusmaksut/tyottomyysvakuutusmaksuvelvollisuus/"
39990
+ },
39991
+ tyel: {
39992
+ title: "Ilmarinen: TyEL-maksut",
39993
+ url: "https://www.ilmarinen.fi/tyonantaja/tyel-maksut/"
39994
+ },
39995
+ "incomes-register": {
39996
+ title: "Tulorekisteri: palkkojen ilmoittaminen",
39997
+ url: "https://www.vero.fi/tulorekisteri/yritykset-ja-organisaatiot/yksityiskohtaiset-tulorekisterin-ohjeet/90124/palkat-tietojen-ilmoittaminen-tulorekisteriin6/"
39998
+ },
39999
+ "company-size": {
40000
+ title: "Suomi.fi: tilinp\xE4\xE4t\xF6s",
40001
+ url: "https://www.suomi.fi/yritykselle/talouden-hallinta-ja-verotus/yrityksen-taloushallinto/opas/tilinpaatos-ja-tilintarkastus/tilinpaatos"
40002
+ },
40003
+ "bookkeeping-act": {
40004
+ title: "Finlex: ajantasainen kirjanpitolaki",
40005
+ url: "https://www.finlex.fi/fi/laki/ajantasa/1997/19971336",
40006
+ format: "finlex-akn",
40007
+ workUri: "/akn/fi/act/statute-consolidated/1997/1336",
40008
+ requiredText: "Luettelo kirjanpidoista ja aineistoista"
40009
+ },
40010
+ "bookkeeping-snapshot": {
40011
+ title: "Kirjanpitolaki, ajantasaisen lain virallinen tallenne",
40012
+ url: "https://eur-lex.europa.eu/legal-content/FI/TXT/PDF/?uri=NIM%3A202604826",
40013
+ watch: false
40014
+ },
40015
+ "small-company-transition": {
40016
+ title: "KILA 1982/2018: yksitt\xE4isen pienyrityksen kokorajasiirtym\xE4, kohta 2.2",
40017
+ url: "https://kirjanpitolautakunta.fi/-/pienyrityksen-ja-pienkonsernin-raja-arvojen-arvioimises-1"
40018
+ }
40019
+ },
40020
+ rules: {
40021
+ vat: {
40022
+ kind: "statutory",
40023
+ units: {
40024
+ general: "percent",
40025
+ reduced: "percent",
40026
+ newspapers: "percent",
40027
+ zero: "percent"
40028
+ },
40029
+ conditions: "Kanta valitaan tapahtuman ja hy\xF6dykkeen verokohtelun perusteella. 0 % ei ole sama kuin verosta vapautettu myynti. Ei muuta tallennettujen tapahtumien historiallisia kantoja.",
40030
+ periods: [
40031
+ {
40032
+ validFrom: "2013-01-01",
40033
+ validUntil: "2024-08-31",
40034
+ reviewedOn: "2026-10-03",
40035
+ reviewDue: "2026-11-20",
40036
+ sourceIds: ["vat", "vat-2013-transition"],
40037
+ values: {
40038
+ general: 24,
40039
+ reduced: 14,
40040
+ newspapers: 10,
40041
+ zero: 0
40042
+ }
40043
+ },
40044
+ {
40045
+ validFrom: "2024-09-01",
40046
+ validUntil: "2025-12-31",
40047
+ reviewedOn: "2026-09-05",
40048
+ reviewDue: "2026-11-20",
40049
+ sourceIds: ["vat"],
40050
+ values: {
40051
+ general: 25.5,
40052
+ reduced: 14,
40053
+ newspapers: 10,
40054
+ zero: 0
40055
+ }
40056
+ },
40057
+ {
40058
+ validFrom: "2026-01-01",
40059
+ validUntil: null,
40060
+ reviewedOn: "2026-09-06",
40061
+ reviewDue: "2026-11-20",
40062
+ sourceIds: [
40063
+ "vat",
40064
+ "vat-vehicles",
40065
+ "vat-own-use",
40066
+ "vat-return",
40067
+ "vat-small-business",
40068
+ "vat-recap",
40069
+ "vat-invoices",
40070
+ "einvoice-law"
40071
+ ],
40072
+ values: {
40073
+ general: 25.5,
40074
+ reduced: 13.5,
40075
+ newspapers: 10,
40076
+ zero: 0
40077
+ }
40078
+ }
40079
+ ]
40080
+ },
40081
+ payroll: {
40082
+ kind: "statutory",
40083
+ units: {
40084
+ tyelEmployee: "percent",
40085
+ health: "percent",
40086
+ unemploymentEmployee: "percent",
40087
+ unemploymentLower: "percent",
40088
+ unemploymentUpper: "percent",
40089
+ unemploymentThreshold: "EUR",
40090
+ unemploymentExemption: "EUR",
40091
+ tyelMonthlyMinimum: "EUR"
40092
+ },
40093
+ conditions: "Tavallinen Suomessa vakuutettu 18\u201364-vuotias ty\xF6ntekij\xE4, ei osaomistaja, yritt\xE4j\xE4 tai ulkomaanty\xF6. TyEL ja ty\xF6nantajan vakuutusten sopimushinnat eritell\xE4\xE4n arvioista.",
40094
+ periods: [
40095
+ {
40096
+ validFrom: "2026-01-01",
40097
+ validUntil: "2026-12-31",
40098
+ reviewedOn: "2026-09-06",
40099
+ reviewDue: "2026-11-20",
40100
+ sourceIds: ["payroll", "unemployment", "unemploymentScope", "tyel", "incomes-register"],
40101
+ values: {
40102
+ tyelEmployee: 7.3,
40103
+ health: 1.91,
40104
+ unemploymentEmployee: 0.89,
40105
+ unemploymentLower: 0.31,
40106
+ unemploymentUpper: 1.23,
40107
+ unemploymentThreshold: 2509500,
40108
+ unemploymentExemption: 1500,
40109
+ tyelMonthlyMinimum: 71.72
40110
+ }
40111
+ }
40112
+ ]
40113
+ },
40114
+ payrollEstimates: {
40115
+ kind: "estimate",
40116
+ units: {
40117
+ tyelBaseEmployer: "percent",
40118
+ tyelAverageEmployer: "percent",
40119
+ tyelTemporaryEmployer: "percent",
40120
+ accidentLow: "percent",
40121
+ accidentAverage: "percent",
40122
+ accidentHigh: "percent",
40123
+ accidentVeryHigh: "percent",
40124
+ groupLife: "percent",
40125
+ holidayBonus: "months/year",
40126
+ workingHours: "hours/month"
40127
+ },
40128
+ conditions: "TyEL-vaihtoehdot ovat mallin arvioita (perusmaksu ilman yhti\xF6kohtaista hoitokustannusta, keskiarvo, tilap\xE4inen). Tapaturma- ja ryhm\xE4henkivakuutus ovat sopimuskohtaisia. Lomaraha mallinnetaan puolen kuukausipalkan suuruisena, ei lakis\xE4\xE4teisen\xE4 etuna.",
40129
+ periods: [
40130
+ {
40131
+ validFrom: "2026-01-01",
40132
+ validUntil: "2026-12-31",
40133
+ reviewedOn: "2026-09-05",
40134
+ reviewDue: "2026-11-20",
40135
+ sourceIds: ["tyel", "payroll"],
40136
+ values: {
40137
+ tyelBaseEmployer: 17.55,
40138
+ tyelAverageEmployer: 17.1,
40139
+ tyelTemporaryEmployer: 18.55,
40140
+ accidentLow: 0.25,
40141
+ accidentAverage: 0.51,
40142
+ accidentHigh: 1.5,
40143
+ accidentVeryHigh: 2.8,
40144
+ groupLife: 0.06,
40145
+ holidayBonus: 0.5,
40146
+ workingHours: 158
40147
+ }
40148
+ }
40149
+ ]
40150
+ },
40151
+ companySizes: {
40152
+ kind: "statutory",
40153
+ conditions: "Kokoluokka, toiminimen tilinp\xE4\xE4t\xF6svelvollisuus ja kirjanpitotapa ovat eri testej\xE4. Rajan saavuttaminen ei ole ylitys. Toiminimen tilinp\xE4\xE4t\xF6svelvollisuus: v\xE4hint\xE4\xE4n kaksi mikroyrityksen rajaa ylittyy sek\xE4 p\xE4\xE4ttyneell\xE4 ett\xE4 edelt\xE4v\xE4ll\xE4 tilikaudella; my\xF6s kalenterivuodesta poikkeava tilikausi edellytt\xE4\xE4 tilinp\xE4\xE4t\xF6st\xE4. Vakiintunut pienyritysluokka vaihtuu vasta kahden per\xE4kk\xE4isen tilikauden saman suuntaisen rajamuutoksen j\xE4lkeen (KILA 1982/2018, 2.2); yksi ylitysvuosi ei yksin poista pienyritysasemaa. Aiempi kokoluokka ja todellinen ensimm\xE4inen tilikausi ratkaistaan erikseen. Rekisterin 2026 kattavuus ei ole lain 605/2024 voimaantulo: uudet kokorajat ovat pakolliset 1.1.2025 alkaville tilikausille, valinnaiset 1.1.2024 alkaville. Kahdenkertaista kirjanpitoa edellytet\xE4\xE4n, jos v\xE4hint\xE4\xE4n kaksi sen omaa rajaa ylittyy p\xE4\xE4ttyneell\xE4 TAI edelt\xE4v\xE4ll\xE4 tilikaudella. Tilintarkastusvelvollisuus arvioidaan erikseen.",
40154
+ units: {
40155
+ soleTraderStatements: "euroa / henkil\xF6\xE4; katso avaimet",
40156
+ smallCompany: "euroa / henkil\xF6\xE4; katso avaimet",
40157
+ singleEntry: "euroa / henkil\xF6\xE4; katso avaimet"
40158
+ },
40159
+ periods: [
40160
+ {
40161
+ validFrom: "2026-01-01",
40162
+ validUntil: null,
40163
+ reviewedOn: "2026-09-06",
40164
+ reviewDue: "2026-12-01",
40165
+ sourceIds: [
40166
+ "company-size",
40167
+ "bookkeeping-act",
40168
+ "bookkeeping-snapshot",
40169
+ "small-company-transition"
40170
+ ],
40171
+ values: {
40172
+ soleTraderStatements: {
40173
+ assets: 45e4,
40174
+ revenue: 9e5,
40175
+ employees: 10
40176
+ },
40177
+ smallCompany: {
40178
+ assets: 75e5,
40179
+ revenue: 15e6,
40180
+ employees: 50
40181
+ },
40182
+ singleEntry: {
40183
+ assets: 1e5,
40184
+ revenue: 2e5,
40185
+ employees: 3
40186
+ }
40187
+ }
40188
+ }
40189
+ ]
40190
+ }
40191
+ }
40192
+ };
40193
+
40194
+ // src/compliance/date-local.ts
40195
+ var DATE_LOCAL_SHAPE = /^\d{4}-\d{2}-\d{2}$/;
40196
+ function parseDateLocal(dateStr) {
40197
+ if (!DATE_LOCAL_SHAPE.test(dateStr)) return new Date(Number.NaN);
40198
+ const [y, m, d] = dateStr.split("-").map(Number);
40199
+ if (y === void 0 || m === void 0 || d === void 0) return new Date(Number.NaN);
40200
+ return new Date(y, m - 1, d);
40201
+ }
40202
+ function formatDateLocal(date5) {
40203
+ const y = date5.getFullYear();
40204
+ const m = String(date5.getMonth() + 1).padStart(2, "0");
40205
+ const d = String(date5.getDate()).padStart(2, "0");
40206
+ return `${y}-${m}-${d}`;
40207
+ }
40208
+
40209
+ // src/compliance/purchase-vat-basis.ts
40210
+ var DAY_KINDS = /* @__PURE__ */ new Set([
40211
+ "supply",
40212
+ "advance_payment",
40213
+ "continuous_period_end",
40214
+ "continuous_year_end",
40215
+ "customs_acceptance"
40216
+ ]);
40217
+ var MONTH_KINDS = /* @__PURE__ */ new Set(["supply_month", "eu_acquisition_month"]);
40218
+ var CATEGORIES = /* @__PURE__ */ new Set([
40219
+ "domestic",
40220
+ "eu_goods",
40221
+ "eu_services",
40222
+ "non_eu_services",
40223
+ "import",
40224
+ "construction",
40225
+ "scrap_metal"
40226
+ ]);
40227
+ function isDay(value) {
40228
+ if (typeof value !== "string") return false;
40229
+ const date5 = parseDateLocal(value);
40230
+ return !Number.isNaN(date5.getTime()) && formatDateLocal(date5) === value;
40231
+ }
40232
+ function isMonth(value) {
40233
+ return typeof value === "string" && /^\d{4}-(0[1-9]|1[0-2])$/.test(value) && isDay(`${value}-01`);
40234
+ }
40235
+ function hasText(value, max = 500) {
40236
+ return typeof value === "string" && value.trim().length > 0 && value.length <= max;
40237
+ }
40238
+ function purchaseVatBasisError(value, category) {
40239
+ if (!CATEGORIES.has(category)) return "Valitse tunnettu oston ALV-k\xE4sittely.";
40240
+ if (!value || typeof value !== "object" || Array.isArray(value))
40241
+ return "Vahvista oston ALV-kannan peruste ennen kirjausta.";
40242
+ const basis = value;
40243
+ if (basis.version !== 1 || typeof basis.kind !== "string")
40244
+ return "ALV-kannan peruste on puutteellinen tai sen versio ei ole tuettu.";
40245
+ const dayKind = DAY_KINDS.has(basis.kind);
40246
+ const monthKind = MONTH_KINDS.has(basis.kind);
40247
+ const original = basis.kind === "original_document_rate";
40248
+ if (!dayKind && !monthKind && !original) return "Valitse tunnettu ALV-kannan peruste.";
40249
+ const keys = /* @__PURE__ */ new Set([
40250
+ "version",
40251
+ "kind",
40252
+ "evidence_reference",
40253
+ ...dayKind ? ["date"] : monthKind ? ["month"] : ["correction_reason"]
40254
+ ]);
40255
+ if (Object.keys(basis).some((key) => !keys.has(key)))
40256
+ return "ALV-kannan perusteessa on ristiriitaisia tai tuntemattomia tietoja.";
40257
+ if (!hasText(basis.evidence_reference, 2e3))
40258
+ return "Yksil\xF6i ALV-kannan perusteen tosite tai muu selvitys (enint\xE4\xE4n 2000 merkki\xE4).";
40259
+ if (dayKind && !isDay(basis.date)) return "Anna ALV-kannan perusteen todellinen p\xE4iv\xE4m\xE4\xE4r\xE4.";
40260
+ if (monthKind && !isMonth(basis.month))
40261
+ return "Anna ALV-kannan perusteen kuukausi muodossa VVVV-KK.";
40262
+ if (original && !hasText(basis.correction_reason))
40263
+ return "Kerro oikaisun syy ja yksil\xF6i alkuper\xE4inen tosite tai hyvitetty ajanjakso.";
40264
+ if (!original) {
40265
+ if (category === "eu_goods" !== (basis.kind === "eu_acquisition_month"))
40266
+ return "EU-tavaraoston kanta m\xE4\xE4r\xE4ytyy yhteis\xF6hankinnan kohdistuskuukaudelta. Tarkista vastaanotto ja laskun saaminen.";
40267
+ if (category === "import" !== (basis.kind === "customs_acceptance"))
40268
+ return "Maahantuonnin kanta vaatii tulli-ilmoituksen hyv\xE4ksymisen perusteen. Tarkista tullausasiakirja.";
40269
+ }
40270
+ if (basis.kind === "continuous_year_end" && (!["eu_services", "non_eu_services"].includes(category) || !basis.date.endsWith("-12-31")))
40271
+ return "Vuoden lopun peruste koskee yleiss\xE4\xE4nn\xF6n jatkuvaa ulkomaanpalvelua ilman tilityksi\xE4 tai maksuja; p\xE4iv\xE4ksi tulee 31.12.";
40272
+ return null;
40273
+ }
40274
+ function purchaseVatRatesForBasis(basis) {
40275
+ let from;
40276
+ let until;
40277
+ if ("date" in basis) from = until = basis.date;
40278
+ if ("month" in basis) {
40279
+ from = `${basis.month}-01`;
40280
+ const first = parseDateLocal(from);
40281
+ until = formatDateLocal(new Date(first.getFullYear(), first.getMonth() + 1, 0));
40282
+ }
40283
+ const periods = MCP_REGISTRY.rules.vat.periods.filter(
40284
+ (period) => !from || period.validFrom <= from && (!period.validUntil || until !== void 0 && period.validUntil >= until)
40285
+ );
40286
+ return [
40287
+ ...new Set(
40288
+ periods.flatMap((period) => [
40289
+ period.values.general,
40290
+ period.values.reduced,
40291
+ period.values.newspapers
40292
+ ])
40293
+ )
40294
+ ].sort((a, b) => b - a);
40295
+ }
40296
+ function purchaseVatRateError(value, category, rate) {
40297
+ const shape = purchaseVatBasisError(value, category);
40298
+ if (shape) return shape;
40299
+ if (!Number.isFinite(rate) || !purchaseVatRatesForBasis(value).includes(rate))
40300
+ return "ALV-kanta ei vastaa annettua perustetta tai sen ajankohtaa ei ole tuetussa verokantahistoriassa. Tarkista tosite ja kanta.";
40301
+ return null;
40302
+ }
40303
+
39800
40304
  // src/accounts.ts
39801
40305
  var ACCOUNTS = {
39802
40306
  // ── Assets (2xxx) ──
@@ -39842,7 +40346,7 @@ var ACCOUNTS = {
39842
40346
 
39843
40347
  // src/compliance/vat-guard.ts
39844
40348
  var ALV_REGISTERED_SALE_REFUSAL = "This company is VAT-registered (ALV). A sale booked as a journal voucher never reaches the ALV report (fields 301-303 and 309), so Kirjapro refuses it. Create the sale with kirjapro_create_invoice, deliver it with kirjapro_send_invoice and record the payment with kirjapro_record_payment or bank matching.";
39845
- var NOT_VAT_REGISTERED_PURCHASE_REFUSAL = 'This company is not VAT-registered (ALV), so it deducts no VAT on purchases: book the purchase gross with vatRate "0". A service bought from a foreign seller (in another EU country or outside the EU) still makes the company liable for the self-assessed VAT: tell the user to add it as a non-deductible row on the Ostojen ALV page of the Kirjapro web app, to book the same VAT as a voucher debiting the expense account and crediting 4210 (ALV-velka), for example with kirjapro_create_transaction, and to check whether the company must register for VAT. Customs (Tulli) collects the import VAT of such a company. If the company is VAT-registered, update its settings in the Kirjapro web app first.';
40349
+ var NOT_VAT_REGISTERED_PURCHASE_REFUSAL = "This company is not VAT-registered, so it cannot deduct VAT. General-rule services from a foreign supplier can still incur full buyer VAT: use kirjapro_book_purchase with reverseCharge, eu_services/non_eu_services, deductionPercent 0 and documented vatRateBasis, or the atomic purchase form in the web app. Do not add a separate VAT row or duplicate voucher. EU goods, construction and scrap-metal registration requirements need separate assessment; do not change ordinary registration settings merely to bypass this guard. Customs collects import VAT for a buyer outside the VAT register.";
39846
40350
  var REVERSE_CHARGE_CATEGORIES = [
39847
40351
  "eu_goods",
39848
40352
  "eu_services",
@@ -39852,8 +40356,8 @@ var REVERSE_CHARGE_CATEGORIES = [
39852
40356
  "scrap_metal"
39853
40357
  ];
39854
40358
  var REVERSE_CHARGE_CATEGORY_REQUIRED = "reverseChargeCategory is required: eu_goods (fields 305/313), eu_services (306/314), non_eu_services (301-303), import (304/310), construction or scrap_metal (318/320).";
39855
- var REVERSE_CHARGE_RATE_REQUIRED = "A reverse-charge purchase needs the Finnish rate the buyer self-assesses (25.5, 13.5 or 10).";
39856
- var PURCHASE_TEMPLATE_NOT_VAT_REGISTERED_REFUSAL = 'This company is not VAT-registered (ALV), so a purchase template with VAT (2320) cannot be used. Book the purchase gross with kirjapro_book_purchase and vatRate "0".';
40359
+ var REVERSE_CHARGE_RATE_REQUIRED = "A reverse-charge purchase needs the positive Finnish rate established by vatRateBasis, including applicable historical rates.";
40360
+ var PURCHASE_TEMPLATE_NOT_VAT_REGISTERED_REFUSAL = 'This company is not VAT-registered (ALV), so a purchase template with VAT (2320) cannot be used. Use kirjapro_book_purchase with the source VAT rate, its basis and deductionPercent 0; a genuinely exempt purchase uses vatRate "0".';
39857
40361
  function purchaseTemplateBatchRefusal(indexes) {
39858
40362
  return `Purchase templates with VAT (index ${indexes.join(", ")}) are booked one at a time with kirjapro_create_from_template or kirjapro_book_purchase so the VAT reaches the ALV report; nothing was written.`;
39859
40363
  }
@@ -39873,8 +40377,8 @@ async function readCompanyVat(supabase2, companyId2) {
39873
40377
  function splitVat(gross, vatRate) {
39874
40378
  if (vatRate === 0) return { net: roundCents(gross), vat: 0 };
39875
40379
  const net = roundCents(gross / (1 + vatRate / 100));
39876
- const vat = roundCents(gross - net);
39877
- return { net, vat };
40380
+ const vat2 = roundCents(gross - net);
40381
+ return { net, vat: vat2 };
39878
40382
  }
39879
40383
  function applyOverrides(entries, overrides) {
39880
40384
  if (!overrides) return entries;
@@ -39924,7 +40428,7 @@ async function validateAccountOverrides(overrides) {
39924
40428
  }
39925
40429
  function saleCashTemplate(revenueAccount, vatRate) {
39926
40430
  return (amount, opts) => {
39927
- const { net, vat } = splitVat(amount, vatRate);
40431
+ const { net, vat: vat2 } = splitVat(amount, vatRate);
39928
40432
  const entries = [
39929
40433
  {
39930
40434
  accountNumber: ACCOUNTS.KASSA,
@@ -39933,11 +40437,11 @@ function saleCashTemplate(revenueAccount, vatRate) {
39933
40437
  },
39934
40438
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
39935
40439
  ];
39936
- if (vat > 0) {
40440
+ if (vat2 > 0) {
39937
40441
  entries.push({
39938
40442
  accountNumber: ACCOUNTS.ALV_VELKA,
39939
40443
  debitAmount: 0,
39940
- creditAmount: vat
40444
+ creditAmount: vat2
39941
40445
  });
39942
40446
  }
39943
40447
  return applyOverrides(entries, opts?.accountOverrides);
@@ -39945,7 +40449,7 @@ function saleCashTemplate(revenueAccount, vatRate) {
39945
40449
  }
39946
40450
  function saleBankTemplate(revenueAccount, vatRate) {
39947
40451
  return (amount, opts) => {
39948
- const { net, vat } = splitVat(amount, vatRate);
40452
+ const { net, vat: vat2 } = splitVat(amount, vatRate);
39949
40453
  const entries = [
39950
40454
  {
39951
40455
  accountNumber: ACCOUNTS.PANKKITILI,
@@ -39954,11 +40458,11 @@ function saleBankTemplate(revenueAccount, vatRate) {
39954
40458
  },
39955
40459
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
39956
40460
  ];
39957
- if (vat > 0) {
40461
+ if (vat2 > 0) {
39958
40462
  entries.push({
39959
40463
  accountNumber: ACCOUNTS.ALV_VELKA,
39960
40464
  debitAmount: 0,
39961
- creditAmount: vat
40465
+ creditAmount: vat2
39962
40466
  });
39963
40467
  }
39964
40468
  return applyOverrides(entries, opts?.accountOverrides);
@@ -40023,7 +40527,7 @@ var TEMPLATES = [
40023
40527
  description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
40024
40528
  vatRate: 25.5,
40025
40529
  expand: (amount, opts) => {
40026
- const { net, vat } = splitVat(amount, 25.5);
40530
+ const { net, vat: vat2 } = splitVat(amount, 25.5);
40027
40531
  return applyOverrides(
40028
40532
  [
40029
40533
  {
@@ -40032,7 +40536,7 @@ var TEMPLATES = [
40032
40536
  creditAmount: 0
40033
40537
  },
40034
40538
  { accountNumber: ACCOUNTS.MYYNTI_25_5, debitAmount: 0, creditAmount: net },
40035
- { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat }
40539
+ { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }
40036
40540
  ],
40037
40541
  opts?.accountOverrides
40038
40542
  );
@@ -40045,11 +40549,11 @@ var TEMPLATES = [
40045
40549
  description: "Osto ALV 25,5% (pankkitililt\xE4)",
40046
40550
  vatRate: 25.5,
40047
40551
  expand: (amount, opts) => {
40048
- const { net, vat } = splitVat(amount, 25.5);
40552
+ const { net, vat: vat2 } = splitVat(amount, 25.5);
40049
40553
  return applyOverrides(
40050
40554
  [
40051
40555
  { accountNumber: ACCOUNTS.OSTOT, debitAmount: net, creditAmount: 0 },
40052
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40556
+ { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat2, creditAmount: 0 },
40053
40557
  {
40054
40558
  accountNumber: ACCOUNTS.PANKKITILI,
40055
40559
  debitAmount: 0,
@@ -40066,11 +40570,11 @@ var TEMPLATES = [
40066
40570
  description: "Ulkopuolinen palvelu ALV 25,5%",
40067
40571
  vatRate: 25.5,
40068
40572
  expand: (amount, opts) => {
40069
- const { net, vat } = splitVat(amount, 25.5);
40573
+ const { net, vat: vat2 } = splitVat(amount, 25.5);
40070
40574
  return applyOverrides(
40071
40575
  [
40072
40576
  { accountNumber: ACCOUNTS.OSTOT_PALVELUT, debitAmount: net, creditAmount: 0 },
40073
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40577
+ { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat2, creditAmount: 0 },
40074
40578
  {
40075
40579
  accountNumber: ACCOUNTS.PANKKITILI,
40076
40580
  debitAmount: 0,
@@ -40236,6 +40740,14 @@ async function preparePosting(params) {
40236
40740
  return { fiscalYearId, journalEntries: toJournalEntries(params.entries, accountResult) };
40237
40741
  }
40238
40742
  async function resolveAndCreatePurchase(params) {
40743
+ for (const row of params.vatRows) {
40744
+ const basisError = purchaseVatRateError(row.vat_rate_basis, row.category, row.vat_rate);
40745
+ if (basisError) return toErrorContent(basisError);
40746
+ }
40747
+ const capability = await getSupabase().rpc("purchase_vat_contract_version");
40748
+ if (capability.error || capability.data !== 1) return toErrorContent(
40749
+ capability.error && capability.error.code !== "PGRST202" ? sanitizeDbError("ALV-perusteiden tarkistus ep\xE4onnistui", capability.error) : "Ostojen ALV-perusteiden p\xE4ivitys on viel\xE4 kesken. Yrit\xE4 uudelleen p\xE4ivityksen j\xE4lkeen."
40750
+ );
40239
40751
  const prepared = await preparePosting(params);
40240
40752
  if ("isError" in prepared) return prepared;
40241
40753
  const { data, error: error61 } = await getSupabase().rpc("book_purchase_with_vat", {
@@ -40314,6 +40826,7 @@ function registerTemplateTools(server2) {
40314
40826
  amount: external_exports.number().finite().positive().max(1e9).describe("Gross amount in EUR"),
40315
40827
  description: external_exports.string().min(1).max(500).describe("Transaction description"),
40316
40828
  documentNumber: external_exports.string().optional().describe("Document/receipt number"),
40829
+ vatRateBasis: PURCHASE_VAT_BASIS_SCHEMA.optional(),
40317
40830
  accountOverrides: external_exports.record(external_exports.string(), external_exports.string()).optional().describe('Account number swaps, e.g. {"2320": "2330"}'),
40318
40831
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
40319
40832
  },
@@ -40350,10 +40863,11 @@ function registerTemplateTools(server2) {
40350
40863
  return toErrorContent(result.error);
40351
40864
  }
40352
40865
  const inputVat = template?.kind === "purchase" ? inputVatOf(result.entries) : 0;
40353
- if (inputVat !== 0 && alvRegistered === false) {
40866
+ const recordsVat = template?.kind === "purchase" && inputVatOf(template.expand(100, { accountOverrides: params.accountOverrides })) !== 0;
40867
+ if (recordsVat && alvRegistered === false) {
40354
40868
  return toErrorContent(PURCHASE_TEMPLATE_NOT_VAT_REGISTERED_REFUSAL);
40355
40869
  }
40356
- const createResult = inputVat !== 0 ? await resolveAndCreatePurchase({
40870
+ const createResult = recordsVat ? await resolveAndCreatePurchase({
40357
40871
  fiscalYearId: params.fiscalYearId,
40358
40872
  transactionDate: params.transactionDate,
40359
40873
  description: params.description,
@@ -40362,6 +40876,7 @@ function registerTemplateTools(server2) {
40362
40876
  vatRows: [
40363
40877
  {
40364
40878
  category: "domestic",
40879
+ vat_rate_basis: params.vatRateBasis,
40365
40880
  vat_rate: template?.vatRate ?? 0,
40366
40881
  tax_base: roundCents(
40367
40882
  result.entries.filter((e) => e.accountNumber !== ACCOUNTS.ALV_SAAMISET).reduce((sum, e) => sum + e.debitAmount, 0)
@@ -40419,10 +40934,7 @@ function registerTemplateTools(server2) {
40419
40934
  const vatPurchaseIndexes = params.transactions.map((tx, index) => {
40420
40935
  const template = TEMPLATE_MAP.get(tx.template);
40421
40936
  if (template?.kind !== "purchase") return -1;
40422
- const expanded = expandTemplate(tx.template, tx.amount, {
40423
- accountOverrides: tx.accountOverrides
40424
- });
40425
- return "entries" in expanded && inputVatOf(expanded.entries) !== 0 ? index : -1;
40937
+ return inputVatOf(template.expand(100, { accountOverrides: tx.accountOverrides })) !== 0 ? index : -1;
40426
40938
  }).filter((index) => index >= 0);
40427
40939
  if (vatPurchaseIndexes.length > 0) {
40428
40940
  return toErrorContent(purchaseTemplateBatchRefusal(vatPurchaseIndexes));
@@ -40616,49 +41128,98 @@ function registerTemplateTools(server2) {
40616
41128
  );
40617
41129
  }
40618
41130
 
41131
+ // src/tools/bookings.ts
41132
+ import { randomUUID as randomUUID2 } from "node:crypto";
41133
+
41134
+ // src/compliance/purchase-vat-allocation.ts
41135
+ function allocatePurchaseVat(net, vat2, deductionPercent) {
41136
+ if (![net, vat2, deductionPercent].every(Number.isFinite) || net < 0 || vat2 < 0 || deductionPercent < 0 || deductionPercent > 100) {
41137
+ throw new Error("V\xE4hennysosuuden on oltava 0\u2013100 % ja ostosummien v\xE4hint\xE4\xE4n nolla.");
41138
+ }
41139
+ const netCents = Math.round(net * 100);
41140
+ const vatCents = Math.round(vat2 * 100);
41141
+ const deductibleVatCents = Math.round(vatCents * deductionPercent / 100 + 1e-9);
41142
+ const deductibleNetCents = Math.round(netCents * deductionPercent / 100 + 1e-9);
41143
+ return {
41144
+ deductibleVat: deductibleVatCents / 100,
41145
+ nonDeductibleVat: (vatCents - deductibleVatCents) / 100,
41146
+ deductibleNet: deductibleNetCents / 100,
41147
+ nonDeductibleNet: (netCents - deductibleNetCents) / 100,
41148
+ expense: (netCents + vatCents - deductibleVatCents) / 100,
41149
+ gross: (netCents + vatCents) / 100
41150
+ };
41151
+ }
41152
+
40619
41153
  // src/compliance/rates.ts
41154
+ function periodForRule(rule, date5) {
41155
+ const periods = MCP_REGISTRY.rules[rule].periods;
41156
+ const period = periods.find(
41157
+ (candidate) => candidate.validFrom <= date5 && (!candidate.validUntil || candidate.validUntil >= date5)
41158
+ );
41159
+ if (!period) throw new Error(`No verified ${rule} rule for ${date5}`);
41160
+ return period;
41161
+ }
41162
+ var vat = periodForRule("vat", "2026-01-01").values;
41163
+ var payrollPeriod = periodForRule("payroll", "2026-01-01");
41164
+ var payroll = payrollPeriod.values;
41165
+ var estimates = periodForRule("payrollEstimates", "2026-01-01").values;
41166
+ var total = (employer) => Math.round((employer + payroll.tyelEmployee) * 100) / 100;
40620
41167
  var FINNISH_RATES_2026 = {
40621
41168
  year: 2026,
40622
- validFrom: "2026-01-01",
40623
- validUntil: "2026-12-31",
40624
- sourcesVerifiedAt: "2026-03-15",
40625
- source: "AVL 1501/1993, ETK 2026, Elo 2026, vero.fi 2026",
40626
- vat: {
40627
- standard: 25.5,
40628
- reduced: 13.5,
40629
- secondReduced: 10,
40630
- zero: 0
40631
- },
41169
+ validFrom: payrollPeriod.validFrom,
41170
+ validUntil: payrollPeriod.validUntil,
41171
+ vat: { standard: vat.general, reduced: vat.reduced, secondReduced: vat.newspapers, zero: vat.zero },
40632
41172
  social: {
40633
- tyelEmployee: 7.3,
40634
- tyelEmployer: 17.55,
40635
- tyelEmployerAverage: 17.1,
40636
- tyelKeskimaarainen: 24.4,
40637
- tyelPerusmaksu: 24.85,
40638
- sotu: 1.91,
40639
- ttvEmployer: 0.31,
40640
- ttvEmployerLarge: 1.23,
40641
- ttvEmployerLargeThreshold: 2509500,
40642
- ttvEmployee: 0.89,
40643
- tapaturmaAverage: 0.51,
40644
- ryhmahenki: 0.06
40645
- }
40646
- };
40647
- function freshnessMeta(rates = FINNISH_RATES_2026, today = /* @__PURE__ */ new Date()) {
40648
- const validUntil = /* @__PURE__ */ new Date(`${rates.validUntil}T23:59:59Z`);
40649
- const msPerDay = 864e5;
40650
- const daysUntilExpiry = Math.ceil(
40651
- (validUntil.getTime() - today.getTime()) / msPerDay
40652
- );
41173
+ tyelEmployee: payroll.tyelEmployee,
41174
+ tyelEmployer: estimates.tyelBaseEmployer,
41175
+ tyelEmployerAverage: estimates.tyelAverageEmployer,
41176
+ tyelKeskimaarainen: total(estimates.tyelAverageEmployer),
41177
+ tyelPerusmaksu: total(estimates.tyelBaseEmployer),
41178
+ tyelTemporary: total(estimates.tyelTemporaryEmployer),
41179
+ sotu: payroll.health,
41180
+ ttvEmployer: payroll.unemploymentLower,
41181
+ ttvEmployerLarge: payroll.unemploymentUpper,
41182
+ ttvEmployerLargeThreshold: payroll.unemploymentThreshold,
41183
+ ttvEmployee: payroll.unemploymentEmployee,
41184
+ tapaturmaAverage: estimates.accidentAverage,
41185
+ ryhmahenki: estimates.groupLife
41186
+ }
41187
+ };
41188
+ function regulatoryFreshness(rule, today = /* @__PURE__ */ new Date()) {
41189
+ const period = periodForRule(rule, "2026-01-01");
41190
+ const day = helsinkiDateString(today);
41191
+ const expired = period.validUntil !== null && day > period.validUntil;
40653
41192
  return {
40654
- published_at: rates.sourcesVerifiedAt,
40655
- valid_from: rates.validFrom,
40656
- valid_through: rates.validUntil,
40657
- sources_verified_at: rates.sourcesVerifiedAt,
40658
- stale: daysUntilExpiry < 0,
40659
- days_until_expiry: daysUntilExpiry
41193
+ rule,
41194
+ kind: MCP_REGISTRY.rules[rule].kind,
41195
+ conditions: MCP_REGISTRY.rules[rule].conditions,
41196
+ coverage_meaning: "Bundled registry coverage interval, not necessarily enactment or first legal effective date; check event/fiscal-year applicability separately",
41197
+ valid_from: period.validFrom,
41198
+ valid_through: period.validUntil,
41199
+ sources_verified_at: period.reviewedOn,
41200
+ review_due: period.reviewDue,
41201
+ review_overdue: day > period.reviewDue,
41202
+ stale: expired || day > period.reviewDue || day < period.validFrom,
41203
+ expired,
41204
+ not_yet_effective: day < period.validFrom,
41205
+ days_until_expiry: period.validUntil === null ? null : Math.round(
41206
+ (Date.parse(`${period.validUntil}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) / 864e5
41207
+ ),
41208
+ sources: period.sourceIds.map((id) => ({
41209
+ id,
41210
+ ...MCP_REGISTRY.sources[id]
41211
+ }))
40660
41212
  };
40661
41213
  }
41214
+ var VAT_RATE_HISTORY = MCP_REGISTRY.rules.vat.periods.map((period) => ({
41215
+ valid_from: period.validFrom,
41216
+ valid_through: period.validUntil,
41217
+ standard: period.values.general,
41218
+ reduced: period.values.reduced,
41219
+ second_reduced: period.values.newspapers,
41220
+ note: "Rate amounts only: product categories changed separately, including on 1.1.2025. Select by supply, advance payment or original corrected event; do not apply current categories backwards."
41221
+ }));
41222
+ var COMPANY_SIZE_RULE = periodForRule("companySizes", "2026-01-01");
40662
41223
 
40663
41224
  // src/compliance/vat-settlement-date.ts
40664
41225
  var pad = (value) => String(value).padStart(2, "0");
@@ -40693,14 +41254,14 @@ function defaultSettlementDate(month, vatPeriod) {
40693
41254
  function expenseAccountWarning(account) {
40694
41255
  const first = account.trim().charAt(0);
40695
41256
  if (first && !"6789".includes(first)) {
40696
- return `expenseAccount "${account}" is outside the YSE 2017 expense range (6xxx materials/services, 7xxx\u20139xxx expenses). Verify it is really an expense account \u2014 booking a purchase to a non-expense account misclassifies the income statement.`;
41257
+ return `expenseAccount "${account}" is outside the Kirjapro default expense range (6xxx materials/services, 7xxx\u20139xxx expenses). Verify it is really an expense account \u2014 booking a purchase to a non-expense account misclassifies the income statement.`;
40697
41258
  }
40698
41259
  return null;
40699
41260
  }
40700
41261
  function revenueAccountWarning(account) {
40701
41262
  const first = account.trim().charAt(0);
40702
41263
  if (first && first !== "5") {
40703
- return `revenueAccount "${account}" is outside the YSE 2017 revenue range (5xxx Liikevaihto). Verify it is really a revenue account \u2014 booking a sale to a non-revenue account misclassifies the income statement.`;
41264
+ return `revenueAccount "${account}" is outside the Kirjapro default revenue range (5xxx Liikevaihto). Verify it is really a revenue account \u2014 booking a sale to a non-revenue account misclassifies the income statement.`;
40704
41265
  }
40705
41266
  return null;
40706
41267
  }
@@ -40715,21 +41276,24 @@ function registerBookingTools(server2) {
40715
41276
  server2.registerTool(
40716
41277
  "kirjapro_book_purchase",
40717
41278
  {
40718
- description: "Book a purchase (osto) with automatic VAT splitting. For a VAT-registered (ALV) company the VAT is recorded as a VAT fact, so it reaches field 307 (and the reverse-charge fields). Reverse charge (reverseCharge=true, with reverseChargeCategory) self-assesses the VAT for EU goods and services, services from outside the EU, import, construction and scrap metal. Entertainment (edustus, AVL 114\xA7) is booked gross. Statutorily non-deductible accounts (wages, insurance, interest, depreciation, taxes, representation) are refused with VAT: book them with vatRate 0. A company that is not VAT-registered books purchases gross with vatRate 0. Rate rules: food/restaurants 13.5%, insurance 0% (exempt), alcohol 25.5%.",
41279
+ description: "Book a purchase (osto) with automatic VAT splitting and a confirmed event basis. For a VAT-registered (ALV) company the VAT fact is recorded with the voucher: deductible VAT reaches field 307 and full assessed reverse-charge VAT reaches its category fields. Reverse charge (reverseCharge=true, with reverseChargeCategory) self-assesses the VAT for EU goods and services, services from outside the EU, import, construction and scrap metal. Domestic entertainment (edustus, AVL 114\xA7) is booked gross. With reverseCharge=true and edustus=true the full buyer VAT is payable, deduction is zero and that VAT increases the expense. Statutorily non-deductible accounts cannot claim input VAT; a genuine exempt purchase uses vatRate 0, but reverse-charge liability must not be hidden by a zero rate. A non-registered company cannot deduct VAT; general-rule foreign services still incur buyer VAT and use deductionPercent 0. The import category records ONLY customs VAT on the documented full customs tax base, without another supplier expense/payment. Rate rules: food/restaurants 13.5%, insurance 0% (exempt), alcohol 25.5%.",
40719
41280
  inputSchema: {
40720
41281
  amount: external_exports.number().finite().positive().max(1e9).describe(
40721
- "EUR amount. Standard purchase: GROSS (VAT-inclusive) \u2014 VAT is split out of this. Reverse charge (reverseCharge=true): NET foreign-invoice amount \u2014 the foreign supplier does not charge Finnish VAT, so the parameter must be net. VAT is self-assessed on top per AVL 9\xA7 producing equal debit to ALV-saamiset and credit to ALV-velka. Entertainment (edustus=true): GROSS (full amount to expense, no VAT deduction per AVL 114\xA7)."
41282
+ "EUR amount. Standard purchase: GROSS (VAT-inclusive) \u2014 VAT is split out of this. Reverse charge (reverseCharge=true): NET supplier amount; import instead uses the full customs tax base and records only tax. Buyer VAT is assessed on top. With edustus=true the expense includes that VAT, ALV-velka records it in full and there is no input VAT deduction; otherwise the deductionPercent chooses the deductible share. Domestic entertainment (edustus=true, reverseCharge=false): GROSS supplier amount, no input VAT deduction."
40722
41283
  ),
40723
41284
  description: external_exports.string().min(1).max(500).describe("Transaction description"),
40724
- date: ISO_DATE_SCHEMA.describe("Transaction date (YYYY-MM-DD)"),
40725
- vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).describe("Finnish VAT rate"),
41285
+ date: ISO_DATE_SCHEMA.describe("Posting/reporting date (YYYY-MM-DD). Choose the lawful reporting period separately from vatRateBasis; this tool does not derive it."),
41286
+ vatRate: external_exports.enum(["25.5", "13.5", "10", "0", "24", "14"]).describe("Finnish VAT rate on the source, including historical 24/14"),
41287
+ vatRateBasis: PURCHASE_VAT_BASIS_SCHEMA.optional(),
41288
+ deductionPercent: external_exports.number().finite().min(0).max(100).optional().describe("Deductible business-use share of original VAT, 0\u2013100. Default 100, or 0 for edustus/non-registered company. Separate private expenditure first."),
41289
+ deductionReason: external_exports.string().trim().min(1).max(500).optional().describe("Required documented allocation/use basis if deduction is below 100%; edustus and non-registration provide their stated basis."),
40726
41290
  expenseAccount: external_exports.string().describe('Expense account number (e.g. "8080")'),
40727
- reverseCharge: external_exports.boolean().default(false).describe(`Reverse charge (k\xE4\xE4nnetty verovelvollisuus): D expense + D ${ACCOUNTS.ALV_SAAMISET} ALV-saamiset + C ${ACCOUNTS.ALV_VELKA} ALV-velka + C bank, with a VAT fact of reverseChargeCategory`),
41291
+ reverseCharge: external_exports.boolean().default(false).describe(`Reverse charge (k\xE4\xE4nnetty verovelvollisuus): C ${ACCOUNTS.ALV_VELKA} for full assessed VAT and a VAT fact of reverseChargeCategory. With edustus=true, debit expense including non-deductible VAT; debit ${ACCOUNTS.ALV_SAAMISET} only for the deductible share and expense for the rest. Credit net supplier payment to bank/payable, except import: tax assessment only, no supplier payment`),
40728
41292
  reverseChargeCategory: external_exports.enum(REVERSE_CHARGE_CATEGORIES).optional().describe(
40729
41293
  "Required with reverseCharge: eu_goods (fields 305/313), eu_services (306/314), non_eu_services (services from outside the EU, 301-303), import (304/310), construction or scrap_metal (318/320)"
40730
41294
  ),
40731
41295
  supplierName: external_exports.string().max(200).optional().describe("Supplier name, stored on the VAT fact"),
40732
- edustus: external_exports.boolean().default(false).describe("Entertainment expense (AVL 114\xA7): 0% VAT deduction, full gross to expense"),
41296
+ edustus: external_exports.boolean().default(false).describe("Entertainment expense (AVL 114\xA7): no input VAT deduction. Domestic supplier amount is gross; with reverseCharge=true supplier amount is net and self-assessed VAT is added to expense and VAT payable"),
40733
41297
  paymentAccount: external_exports.string().default(ACCOUNTS.PANKKITILI).describe(`Payment account (default "${ACCOUNTS.PANKKITILI}" Pankkitili)`),
40734
41298
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
40735
41299
  documentNumber: external_exports.string().optional().describe("Document/receipt number"),
@@ -40739,97 +41303,109 @@ function registerBookingTools(server2) {
40739
41303
  annotations: WRITE_ANNOTATIONS
40740
41304
  },
40741
41305
  async (params) => {
40742
- const rate = Number.parseFloat(params.vatRate);
40743
- const gross = roundCents(params.amount);
40744
- let entries;
40745
- let vatRows = [];
40746
- const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
40747
- if ("isError" in companyVat) return companyVat;
40748
- if (!params.edustus && !companyVat.alvRegistered && (rate !== 0 || params.reverseCharge)) {
40749
- return toErrorContent(NOT_VAT_REGISTERED_PURCHASE_REFUSAL);
40750
- }
40751
- if (params.reverseCharge && !params.edustus) {
40752
- if (!params.reverseChargeCategory) return toErrorContent(REVERSE_CHARGE_CATEGORY_REQUIRED);
40753
- if (rate === 0) return toErrorContent(REVERSE_CHARGE_RATE_REQUIRED);
40754
- }
40755
- if (params.edustus) {
40756
- entries = [
40757
- { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
40758
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40759
- ];
40760
- } else if (params.reverseCharge) {
40761
- const vat = roundCents(gross * rate / 100);
40762
- entries = [
40763
- { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
40764
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40765
- // ALV-saamiset (input)
40766
- { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat },
40767
- // ALV-velka (output)
40768
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40769
- ];
40770
- vatRows = [
40771
- {
40772
- category: params.reverseChargeCategory ?? "eu_services",
40773
- vat_rate: rate,
40774
- tax_base: gross,
40775
- vat_amount: vat
40776
- }
40777
- ];
40778
- } else if (rate === 0) {
40779
- entries = [
40780
- { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
40781
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40782
- ];
40783
- } else {
40784
- const { net, vat } = splitVat(gross, rate);
40785
- entries = [
40786
- { accountNumber: params.expenseAccount, debitAmount: net, creditAmount: 0 },
40787
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40788
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40789
- ];
40790
- vatRows = [{ category: "domestic", vat_rate: rate, tax_base: net, vat_amount: vat }];
40791
- }
40792
- const purchaseAccountWarning = expenseAccountWarning(params.expenseAccount);
40793
- if (params.dryRun) {
40794
- return toDryRunResponse("kirjapro_book_purchase", {
41306
+ const execute = async () => {
41307
+ 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.");
41310
+ const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
41311
+ if ("isError" in companyVat) return companyVat;
41312
+ if (params.reverseCharge && !params.reverseChargeCategory) return toErrorContent(REVERSE_CHARGE_CATEGORY_REQUIRED);
41313
+ if (params.reverseCharge && rate === 0) return toErrorContent(REVERSE_CHARGE_RATE_REQUIRED);
41314
+ const category = params.reverseCharge ? params.reverseChargeCategory : "domestic";
41315
+ const assessmentOnly = category === "import";
41316
+ const percent = params.edustus || !companyVat.alvRegistered ? 0 : params.deductionPercent ?? 100;
41317
+ if ((params.edustus || !companyVat.alvRegistered) && (params.deductionPercent ?? 0) > 0)
41318
+ return toErrorContent("Edustuksesta tai rekister\xF6im\xE4tt\xF6m\xE4n yrityksen ostosta ei tehd\xE4 ALV-v\xE4hennyst\xE4. Korjaa v\xE4hennysosuus nollaksi.");
41319
+ if (!companyVat.alvRegistered && params.reverseCharge && !["eu_services", "non_eu_services"].includes(category))
41320
+ return toErrorContent(NOT_VAT_REGISTERED_PURCHASE_REFUSAL);
41321
+ const reason = params.edustus ? "Edustuskulu, AVL 114 \xA7 1 mom. 3 kohta" : !companyVat.alvRegistered ? "Yritys ei ole arvonlis\xE4verovelvollinen" : params.deductionReason;
41322
+ if (rate > 0 && percent < 100 && !reason)
41323
+ return toErrorContent("Kirjoita v\xE4hennysosuuden jakoperuste. Yksityiskulut erotetaan erikseen.");
41324
+ if (rate > 0) {
41325
+ const basisError = purchaseVatRateError(params.vatRateBasis, category, rate);
41326
+ if (basisError) return toErrorContent(basisError);
41327
+ }
41328
+ const basis = params.vatRateBasis;
41329
+ const { net, vat: vat2 } = params.reverseCharge ? { net: amount, vat: roundCents(amount * rate / 100) } : splitVat(amount, rate);
41330
+ const allocation = allocatePurchaseVat(net, vat2, percent);
41331
+ const keepFacts = rate > 0 && (params.reverseCharge || companyVat.alvRegistered && !params.edustus);
41332
+ const vatAllocation = percent > 0 && percent < 100 ? {
41333
+ version: 1,
41334
+ group_id: randomUUID2(),
41335
+ net,
41336
+ vat: vat2,
41337
+ deduction_percent: percent,
41338
+ reason
41339
+ } : void 0;
41340
+ const fact = {
41341
+ category,
41342
+ vat_rate: rate,
41343
+ vat_rate_basis: basis,
41344
+ ...vatAllocation ? { vat_allocation: vatAllocation } : {}
41345
+ };
41346
+ const vatRows = keepFacts ? [
41347
+ ...percent > 0 ? [{ ...fact, tax_base: allocation.deductibleNet, vat_amount: allocation.deductibleVat, is_deductible: true }] : [],
41348
+ ...percent < 100 ? [{ ...fact, tax_base: allocation.nonDeductibleNet, vat_amount: allocation.nonDeductibleVat, is_deductible: false, non_deductible_reason: reason }] : []
41349
+ ].filter((row) => row.tax_base !== 0 || row.vat_amount !== 0) : [];
41350
+ const expense = assessmentOnly ? allocation.nonDeductibleVat : allocation.expense;
41351
+ const entries = [
41352
+ { accountNumber: params.expenseAccount, debitAmount: expense, creditAmount: 0 },
41353
+ { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: allocation.deductibleVat, creditAmount: 0 },
41354
+ ...params.reverseCharge ? [{ accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }] : [],
41355
+ ...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net : amount }] : []
41356
+ ].filter((entry) => entry.debitAmount !== 0 || entry.creditAmount !== 0);
41357
+ if (entries.length < 2) return toErrorContent("Tositteen veron on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41358
+ const purchaseAccountWarning = expenseAccountWarning(params.expenseAccount);
41359
+ const calculation = {
41360
+ amount_basis: assessmentOnly ? "customs_tax_base" : params.reverseCharge ? "supplier_net" : "supplier_gross",
41361
+ ...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net : amount },
41362
+ tax_base: net,
41363
+ output_vat: params.reverseCharge ? vat2 : 0,
41364
+ source_vat: vat2,
41365
+ deductible_vat: allocation.deductibleVat,
41366
+ expense_total: expense,
41367
+ deduction_percent: percent,
41368
+ ...reason ? { deduction_reason: reason } : {}
41369
+ };
41370
+ if (params.dryRun) return toDryRunResponse("kirjapro_book_purchase", {
40795
41371
  action: "book_purchase",
40796
- gross,
41372
+ gross: amount,
40797
41373
  vat_rate: params.vatRate,
40798
41374
  description: params.description,
40799
41375
  date: params.date,
40800
41376
  entries,
40801
- ...vatRows.length > 0 && { vat_rows: vatRows },
40802
- ...purchaseAccountWarning && { account_warning: purchaseAccountWarning }
41377
+ calculation,
41378
+ ...params.reverseCharge ? { reverse_charge: calculation } : {},
41379
+ ...vatRows.length ? { vat_rows: vatRows } : {},
41380
+ ...purchaseAccountWarning ? { account_warning: purchaseAccountWarning } : {}
40803
41381
  });
40804
- }
41382
+ const posting = {
41383
+ fiscalYearId: params.fiscalYearId,
41384
+ transactionDate: params.date,
41385
+ description: params.description,
41386
+ documentNumber: params.documentNumber,
41387
+ entries
41388
+ };
41389
+ const result = vatRows.length ? await resolveAndCreatePurchase({ ...posting, vatRows, supplierName: params.supplierName }) : await resolveAndCreate(posting);
41390
+ if ("isError" in result) return result;
41391
+ return toResult({
41392
+ message: assessmentOnly ? "Customs VAT assessment booked" : "Purchase booked",
41393
+ transaction_id: result.id,
41394
+ entries,
41395
+ calculation,
41396
+ ...params.reverseCharge ? { reverse_charge: calculation } : {},
41397
+ ..."vatEntryIds" in result ? { vat_entry_ids: result.vatEntryIds } : {},
41398
+ ...purchaseAccountWarning ? { warning: purchaseAccountWarning } : {}
41399
+ });
41400
+ };
41401
+ if (params.dryRun) return execute();
40805
41402
  return withIdempotency(
40806
41403
  getSupabase(),
40807
41404
  getCompanyId(),
40808
41405
  "kirjapro_book_purchase",
40809
41406
  params.idempotencyKey,
40810
41407
  params,
40811
- async () => {
40812
- const posting = {
40813
- fiscalYearId: params.fiscalYearId,
40814
- transactionDate: params.date,
40815
- description: params.description,
40816
- documentNumber: params.documentNumber,
40817
- entries
40818
- };
40819
- const result = vatRows.length > 0 ? await resolveAndCreatePurchase({
40820
- ...posting,
40821
- vatRows,
40822
- supplierName: params.supplierName
40823
- }) : await resolveAndCreate(posting);
40824
- if ("isError" in result) return result;
40825
- return toResult({
40826
- message: "Purchase booked",
40827
- transaction_id: result.id,
40828
- entries,
40829
- ..."vatEntryIds" in result && { vat_entry_ids: result.vatEntryIds },
40830
- ...purchaseAccountWarning && { warning: purchaseAccountWarning }
40831
- });
40832
- }
41408
+ execute
40833
41409
  );
40834
41410
  }
40835
41411
  );
@@ -40983,21 +41559,21 @@ function registerBookingTools(server2) {
40983
41559
  { accountNumber: params.revenueAccount, debitAmount: 0, creditAmount: gross }
40984
41560
  ];
40985
41561
  } else {
40986
- const { net: net2, vat: vat2 } = splitVat(gross, rate);
41562
+ const { net: net2, vat: vat3 } = splitVat(gross, rate);
40987
41563
  entries = [
40988
41564
  { accountNumber: debitAccount, debitAmount: gross, creditAmount: 0 },
40989
41565
  { accountNumber: params.revenueAccount, debitAmount: 0, creditAmount: net2 },
40990
- { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }
41566
+ { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat3 }
40991
41567
  ];
40992
41568
  }
40993
- const { net, vat } = rate > 0 ? splitVat(gross, rate) : { net: gross, vat: 0 };
41569
+ const { net, vat: vat2 } = rate > 0 ? splitVat(gross, rate) : { net: gross, vat: 0 };
40994
41570
  const saleAccountWarning = revenueAccountWarning(params.revenueAccount);
40995
41571
  if (params.dryRun) {
40996
41572
  return toDryRunResponse("kirjapro_book_sale", {
40997
41573
  action: "book_sale",
40998
41574
  gross,
40999
41575
  net,
41000
- vat,
41576
+ vat: vat2,
41001
41577
  vat_rate: `${params.vatRate} %`,
41002
41578
  payment_type: params.paymentType,
41003
41579
  debit_account: debitAccount,
@@ -41027,7 +41603,7 @@ function registerBookingTools(server2) {
41027
41603
  transaction_id: result.id,
41028
41604
  gross,
41029
41605
  net,
41030
- vat,
41606
+ vat: vat2,
41031
41607
  vat_rate: `${params.vatRate} %`,
41032
41608
  payment_type: params.paymentType,
41033
41609
  debit_account: debitAccount,
@@ -41041,6 +41617,81 @@ function registerBookingTools(server2) {
41041
41617
  );
41042
41618
  }
41043
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
+
41044
41695
  // src/tools/offers.ts
41045
41696
  var OFFER_DETAIL_OUTPUT_SHAPE = {
41046
41697
  id: external_exports.string(),
@@ -41162,6 +41813,7 @@ function registerOfferTools(server2) {
41162
41813
  "reverse_scrap",
41163
41814
  "reverse_eu_service",
41164
41815
  "export_outside_eu",
41816
+ "service_outside_eu",
41165
41817
  "intra_eu_goods",
41166
41818
  "exempt_healthcare",
41167
41819
  "exempt_social_welfare",
@@ -41169,7 +41821,7 @@ function registerOfferTools(server2) {
41169
41821
  "exempt_financial",
41170
41822
  "exempt_insurance",
41171
41823
  "exempt_other"
41172
- ]).optional().describe("Structured VAT treatment. Required when VAT rate is 0.")
41824
+ ]).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.")
41173
41825
  })).min(1).max(MAX_DOCUMENT_LINES).describe(`Line items (max ${MAX_DOCUMENT_LINES})`),
41174
41826
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
41175
41827
  },
@@ -41238,7 +41890,7 @@ function registerOfferTools(server2) {
41238
41890
  const enteredTotal = roundCents(line.quantity * line.unitPrice);
41239
41891
  const vatAmount = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(enteredTotal * vatRate / (100 + vatRate)) : roundCents(enteredTotal * (vatRate / 100));
41240
41892
  const subtotal2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(enteredTotal - vatAmount) : enteredTotal;
41241
- const total2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? enteredTotal : roundCents(subtotal2 + vatAmount);
41893
+ const total3 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? enteredTotal : roundCents(subtotal2 + vatAmount);
41242
41894
  return {
41243
41895
  line_number: i + 1,
41244
41896
  description: line.description,
@@ -41256,12 +41908,12 @@ function registerOfferTools(server2) {
41256
41908
  eu_goods_transport_confirmed: params.euGoodsTransportConfirmed,
41257
41909
  subtotal: subtotal2,
41258
41910
  vat_amount: vatAmount,
41259
- total: total2
41911
+ total: total3
41260
41912
  };
41261
41913
  });
41262
41914
  const subtotal = roundCents(calculatedLines.reduce((s, l) => s + l.subtotal, 0));
41263
41915
  const vatTotal = roundCents(calculatedLines.reduce((s, l) => s + l.vat_amount, 0));
41264
- const total = roundCents(subtotal + vatTotal);
41916
+ const total2 = roundCents(subtotal + vatTotal);
41265
41917
  const { data: offerId, error: rpcError } = await supabase2.rpc("create_offer_with_lines", {
41266
41918
  p_company_id: companyId2,
41267
41919
  p_customer_id: params.customerId || null,
@@ -41275,7 +41927,7 @@ function registerOfferTools(server2) {
41275
41927
  p_notes: params.notes || null,
41276
41928
  p_subtotal: subtotal,
41277
41929
  p_vat_total: vatTotal,
41278
- p_total: total,
41930
+ p_total: total2,
41279
41931
  p_lines: calculatedLines
41280
41932
  });
41281
41933
  if (rpcError) {
@@ -41289,7 +41941,7 @@ function registerOfferTools(server2) {
41289
41941
  title: params.title,
41290
41942
  subtotal,
41291
41943
  vat_total: vatTotal,
41292
- total,
41944
+ total: total2,
41293
41945
  lines: calculatedLines
41294
41946
  });
41295
41947
  }
@@ -42351,7 +43003,7 @@ var VAT_RATE_METADATA = [
42351
43003
  { rate: FINNISH_RATES_2026.vat.reduced, label: "Alennettu verokanta", description: "Food, animal feed, restaurants, transport, accommodation, books, medicine, sports, culture, performing artists, sanitary products, broadcasting (from 1.1.2026, was 14%)", basis: "AVL 85\xA7, 85a\xA7" },
42352
43004
  { rate: FINNISH_RATES_2026.vat.secondReduced, label: "Lehdet ja aikakauslehdet", description: "Newspapers and magazines (print and digital) only", basis: "AVL 85a\xA7" },
42353
43005
  { rate: FINNISH_RATES_2026.vat.zero, label: "Nollaverokanta", description: "Exports (AVL 70\xA7), intra-EU B2B supplies (AVL 72a\xA7), vessels, international transport. Input VAT deductible", basis: "AVL 55-72h\xA7" },
42354
- { rate: null, label: "Verovapaat myynnit", description: "Healthcare (AVL 34-36\xA7), education (AVL 39-40\xA7), financial services (AVL 41-42\xA7), insurance (AVL 44\xA7). NO input VAT deduction", basis: "AVL 34-44\xA7" }
43006
+ { rate: null, label: "Verovapaat myynnit", description: "Healthcare (AVL 34-36\xA7), education (AVL 39-40\xA7), financial services (AVL 41-42\xA7), insurance (AVL 44\xA7). Normally no input VAT deduction; assess statutory refund exceptions separately", basis: "AVL 34-44\xA7" }
42355
43007
  ];
42356
43008
  function registerSchemaResources(server2) {
42357
43009
  server2.registerResource(
@@ -42373,7 +43025,7 @@ function registerSchemaResources(server2) {
42373
43025
  "vat-rates",
42374
43026
  "kirjapro://vat-rates",
42375
43027
  {
42376
- description: "Finnish VAT rates (2026). Rates: 25.5% (general), 13.5% (food/restaurants/books/medicine/transport/accommodation/sports), 10% (newspapers/magazines only), 0% (exports/vessels \u2014 input VAT deductible), exempt (healthcare/education/finance/insurance \u2014 NO input VAT deduction).",
43028
+ description: "Finnish VAT rates (2026). Rates: 25.5% (general), 13.5% (food/restaurants/books/medicine/transport/accommodation/sports), 10% (newspapers/magazines only), 0% (exports/vessels \u2014 input VAT deductible), exempt (normally no input VAT deduction; statutory refund exceptions apply).",
42377
43029
  mimeType: "application/json"
42378
43030
  },
42379
43031
  async (uri) => ({
@@ -42382,7 +43034,7 @@ function registerSchemaResources(server2) {
42382
43034
  mimeType: "application/json",
42383
43035
  text: JSON.stringify({
42384
43036
  rates: VAT_RATE_METADATA,
42385
- _meta: freshnessMeta(FINNISH_RATES_2026)
43037
+ _meta: regulatoryFreshness("vat")
42386
43038
  })
42387
43039
  }]
42388
43040
  })
@@ -42463,6 +43115,9 @@ function registerCompanyResources(server2) {
42463
43115
  }
42464
43116
 
42465
43117
  // src/resources/compliance.ts
43118
+ var exampleOutputVat = FINNISH_RATES_2026.vat.standard;
43119
+ var exampleDeductibleVat = roundCents(exampleOutputVat * 0.6);
43120
+ var exampleNonDeductibleVat = roundCents(exampleOutputVat - exampleDeductibleVat);
42466
43121
  var VAT_COMPLIANCE = {
42467
43122
  rates: {
42468
43123
  current_year: FINNISH_RATES_2026.year,
@@ -42472,40 +43127,40 @@ var VAT_COMPLIANCE = {
42472
43127
  zero: { rate: FINNISH_RATES_2026.vat.zero, finnish: "Nollaverokanta", basis: "AVL 55-72h\xA7", examples: "Exports outside EU, intra-community supplies, vessels (sale/rental/chartering), international transport" },
42473
43128
  exempt: { rate: null, finnish: "Verovapaat myynnit", basis: "AVL 34-44\xA7", examples: "Healthcare (AVL 34-36\xA7), education (AVL 39-40\xA7), financial services (AVL 41-42\xA7), insurance (AVL 44\xA7), social services (AVL 37-38\xA7), real estate transfers (AVL 27-28\xA7)" }
42474
43129
  },
42475
- rate_history: [
42476
- { period: "1.1.2026 \u2192", standard: FINNISH_RATES_2026.vat.standard, reduced: FINNISH_RATES_2026.vat.reduced, second_reduced: FINNISH_RATES_2026.vat.secondReduced, note: "Reduced rate unified from 14% \u2192 13.5%" },
42477
- { period: "1.9.2024 \u2192 31.12.2025", standard: 25.5, reduced: 14, second_reduced: 10, note: "Standard raised from 24% \u2192 25.5%" },
42478
- { period: "1.1.2013 \u2192 31.8.2024", standard: 24, reduced: 14, second_reduced: 10 }
42479
- ],
43130
+ rate_history: VAT_RATE_HISTORY,
43131
+ historical_rates: "Choose the rate from delivery/performance, or receipt of an advance payment. A 2025 supply or a 2025 advance can carry 14% on a 2026 document. Genuine credits and bad-debt adjustments retain the original rate. A return period is not a rate-selection date. Preserve historical facts.",
43132
+ deduction_preconditions: "Deduct only VAT legally due on a purchase for deduction-entitled activity, supported by the invoice and actual use. A business expense is not automatically VAT-deductible. Exempt activity, private use and AVL 114 restrictions are assessed separately; use the actual deductible share.",
42480
43133
  deduction_rules: [
42481
- { type: "Edustuskulut (entertainment)", vat_deduction: "0%", income_tax: "50%", basis: "AVL 114\xA7", booking: "Full gross to expense, NO ALV-saamiset entry" },
42482
- { type: "Neuvottelukulut (meetings)", vat_deduction: "100%", income_tax: "100%", basis: "AVL 102\xA7", booking: "Normal VAT split" },
42483
- { type: "Henkil\xF6st\xF6kulut (staff events)", vat_deduction: "100%", income_tax: "100%", basis: "AVL 102\xA7", booking: "Normal VAT split" },
42484
- { type: "Insurance premiums", vat_deduction: "0% (exempt)", income_tax: "100%", basis: "AVL 44\xA7", booking: "Full gross to expense, no VAT (vakuutusmaksuvero is separate)" },
42485
- { type: "Healthcare (staff)", vat_deduction: "0% (exempt)", income_tax: "100%", basis: "AVL 34-36\xA7" },
42486
- { type: "Financial services", vat_deduction: "0% (exempt)", income_tax: "100%", basis: "AVL 41-42\xA7" },
42487
- { type: "Henkil\xF6auto (passenger car M1)", vat_deduction: "100% if exclusive business use, 0% if ANY private use (incl. commuting)", income_tax: "Proportional", basis: "AVL 114\xA7 1 mom 4 kohta", booking: "Ajop\xE4iv\xE4kirja (mileage logbook) required as proof" },
42488
- { type: "Pakettiauto/kuorma-auto (van N1+)", vat_deduction: "Proportional to business use", income_tax: "Proportional", basis: "AVL 102\xA7, 117\xA7" },
42489
- { type: "Markkinointitilaisuudet (marketing events)", vat_deduction: "100%", income_tax: "100%", basis: "AVL 102\xA7, KHO 2013:63", booking: "Product launches, trade shows \u2014 NOT client entertainment" }
43134
+ { type: "Edustuskulut (entertainment)", vat_deduction: "0%", income_tax: "50% under EVL; different tax regimes require their own assessment", basis: "AVL 114 \xA7; EVL 8 \xA7 1 mom 8 kohta", booking: "Full gross to expense, NO ALV-saamiset entry" },
43135
+ { type: "Neuvottelukulut (meetings)", vat_deduction: "Deduction-entitled business share; reasonable ordinary refreshments, not entertainment", income_tax: "Assess business purpose and nature separately", basis: "AVL 102, 114 and 117 \xA7", booking: "Keep participants, purpose and programme; the account label alone proves nothing" },
43136
+ { type: "Henkil\xF6st\xF6kulut (staff events)", vat_deduction: "Deduction-entitled share of a genuine employer-organised joint event for all staff or an entire unit", income_tax: "Assess business purpose and any employee benefit separately", basis: "AVL 102, 117 \xA7; KHO 2015:98", booking: "No mandatory business programme. Private benefits and mixed activity need separate assessment" },
43137
+ { type: "Insurance premiums", vat_deduction: "No VAT to deduct when the insurance service is exempt", income_tax: "Depends on insurance and business purpose", basis: "AVL 44 \xA7", booking: "Do not extract input VAT from vakuutusmaksuvero; separately taxable services require their own classification" },
43138
+ { type: "Healthcare (staff)", vat_deduction: "No VAT on healthcare satisfying the exemption conditions", income_tax: "Assess occupational healthcare and other benefit rules separately", basis: "AVL 34\u201336 \xA7" },
43139
+ { type: "Financial services", vat_deduction: "No VAT on exempt financial services; software and technical services may be taxable", income_tax: "Assess expense and business purpose separately", basis: "AVL 41\u201342 \xA7", booking: "Classify the invoiced service, not the supplier brand. A payment processor can invoice both exempt and taxable services" },
43140
+ { type: "Henkil\xF6auto (passenger car M1)", vat_deduction: "Ordinary company car: exclusive deduction-entitled use required; any private, commuting or other non-deductible use removes the deduction", income_tax: "Separate business-use assessment", basis: "AVL 114 \xA7 1 mom 5 kohta and 2 mom", exceptions: "Resale, rental, professional passenger transport and driving tuition: deductible use under those exceptions can remain proportional. Other business use is not automatically deductible when private use also occurs", booking: "Mileage log or other reliable equivalent evidence" },
43141
+ { type: "Pakettiauto/kuorma-auto (van N1+)", vat_deduction: "Proportional to deduction-entitled use; excludes exempt business and private use", income_tax: "Separate business-use assessment", basis: "AVL 102, 117 \xA7" },
43142
+ { type: "Markkinointitilaisuudet (marketing events)", vat_deduction: "Deduction-entitled marketing programme and ordinary incidental hospitality; entertainment parts are not deductible", income_tax: "Assess the actual programme and purpose", basis: "AVL 102, 114, 117 \xA7; KHO 2013:63\u201365", booking: "Allocate mixed events by content and participants; an invitation-only event is not automatically entertainment" }
42490
43143
  ],
42491
43144
  reverse_charge: {
42492
- rule: "Buyer self-assesses both output and input VAT. Applies to: (1) foreign B2B providers not charging Finnish VAT, (2) domestic construction services, (3) metal scrap, (4) emissions allowances, (5) investment gold",
43145
+ rule: "Buyer assesses output VAT only when the relevant reverse-charge conditions apply. Input VAT deduction is a separate test. A foreign invoice without Finnish VAT is not by itself proof of reverse charge: check place of supply, supplier establishment and applicable registration/exemptions.",
42493
43146
  basis: "AVL 8a-8d\xA7, 9\xA7, 65\xA7; EU VAT Directive Art. 196, Art. 199",
42494
43147
  scenarios: [
42495
- { type: "Foreign B2B", basis: "AVL 9\xA7", note: "Most common: EU and non-EU service providers" },
42496
- { type: "Construction services", basis: "AVL 8c\xA7", note: 'Since 1.4.2011. Buyer must itself sell/rent construction services "not just occasionally". Regular company commissioning renovation: NO reverse charge' },
43148
+ { type: "Foreign B2B", basis: "AVL 9, 65 \xA7", note: "General-rule services received by a business establishment in Finland from an eligible foreign seller; special place-of-supply rules and the seller\u2019s Finnish establishment can change treatment" },
43149
+ { type: "Construction services", basis: "AVL 8c\xA7", note: 'Since 1.4.2011. Buyer must itself sell/rent construction services "not just occasionally". An ordinary buyer commissioning renovation does not qualify under this domestic construction rule alone; foreign-seller reverse charge under AVL 9 \xA7 is a separate test' },
42497
43150
  { type: "Metal scrap and waste", basis: "AVL 8d\xA7", note: "Since 1.1.2015. Between VAT-registered entities" },
42498
43151
  { type: "Emissions allowances", basis: "AVL 8b\xA7", note: "Greenhouse gas emission allowances" },
42499
43152
  { type: "Investment gold", basis: "AVL 8a\xA7", note: "Taxable investment gold, gold materials, gold semi-finished products" }
42500
43153
  ],
42501
- booking: "4 entries: D expense + D ALV-saamiset + C bank + C ALV-velka (k\xE4\xE4nnetty)",
42502
- note: "Net VAT effect is zero but BOTH must be reported on ALV-ilmoitus. Invoice must NOT show VAT rate or VAT amount",
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.",
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.",
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.",
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 },
42503
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."
42504
43159
  },
42505
43160
  common_mistakes: [
42506
- "Food/coffee at 25.5% \u2014 should be 13.5% (AVL 85\xA7)",
42507
- "Train ticket at 10% \u2014 should be 13.5% (transport = reduced rate)",
42508
- "Using 14% after 1.1.2026 \u2014 rate changed to 13.5%",
43161
+ "For a qualifying domestic food/coffee supply in 2026, use the current reduced rate, not the standard rate; original events and advances can use an earlier rate",
43162
+ "For a domestic passenger-transport supply in 2026, use the current reduced rate; older supplies, advances and credits retain their event-date rate",
43163
+ "Replacing a historical 14% rate merely because the document or return is dated in 2026 \u2014 retain the original supply/advance/credit treatment",
42509
43164
  "Restaurant alcohol at 13.5% \u2014 alcohol is 25.5% even at restaurants",
42510
43165
  "Newspaper/magazine at 13.5% \u2014 periodicals are 10%",
42511
43166
  "Splitting VAT from insurance premium \u2014 insurance is VAT-exempt, not zero-rated",
@@ -42519,40 +43174,44 @@ var INVOICE_REQUIREMENTS = {
42519
43174
  mandatory_fields: [
42520
43175
  { kohta: 1, field: "Laskun antamisp\xE4iv\xE4", english: "Invoice date", status: "implemented", relevance: "always" },
42521
43176
  { kohta: 2, field: "Juokseva tunniste", english: "Sequential invoice number", status: "implemented", relevance: "always", note: "Gaps must be detectable per Vero requirement" },
42522
- { kohta: 3, field: "Myyj\xE4n ALV-tunniste", english: "Seller VAT ID (FI + Y-tunnus)", status: "implemented", relevance: "always", note: "Y-tunnus always required; FI-format strictly mandatory for EU cross-border" },
43177
+ { kohta: 3, field: "Myyj\xE4n ALV-tunniste", english: "Seller VAT ID (FI + Y-tunnus)", status: "implemented", relevance: "conditional", note: "Y-tunnus identifies the Finnish business on business documents. Use the applicable VAT registration identifier; FI-format is required for the intra-EU transactions specified by Vero 5.1.3. Do not present an unregistered seller as VAT-registered" },
42523
43178
  { kohta: 4, field: "Ostajan ALV-tunniste", english: "Buyer VAT ID", status: "implemented", relevance: "conditional", note: "Required for reverse charge and EU B2B" },
42524
43179
  { kohta: 5, field: "Myyj\xE4n ja ostajan nimi ja osoite", english: "Names and addresses", status: "implemented", relevance: "always" },
42525
43180
  { kohta: 6, field: "Tavaroiden/palvelujen kuvaus ja m\xE4\xE4r\xE4", english: "Description and quantity", status: "implemented", relevance: "always" },
42526
43181
  { kohta: 7, field: "Toimitusp\xE4iv\xE4 / suoritusp\xE4iv\xE4", english: "Delivery date", status: "partial", relevance: "conditional", note: "NOT required when same as invoice date (Vero.fi 5.1.7). Required when different" },
42527
43182
  { kohta: 8, field: "Veron peruste, yksikk\xF6hinta, alennukset", english: "Tax base, unit price, discounts", status: "implemented", relevance: "always" },
42528
- { kohta: 9, field: "Verokanta", english: "VAT rate", status: "implemented", relevance: "always" },
42529
- { kohta: 10, field: "Suoritettavan veron m\xE4\xE4r\xE4", english: "VAT amount", status: "implemented", relevance: "always" },
42530
- { kohta: 11, field: "Merkint\xE4 verottomuudesta", english: "Tax exemption notation", status: "gap", relevance: "conditional", note: 'Simple "Veroton myynti" suffices. Citing AVL section is recommended but not mandatory (Vero.fi 5.1.11)' },
42531
- { kohta: 12, field: "Merkint\xE4 k\xE4\xE4nnetyst\xE4 verovelvollisuudesta", english: "Reverse charge notation", status: "gap", relevance: "conditional", note: '"K\xE4\xE4nnetty verovelvollisuus" phrase is mandatory. Legal basis citation is recommended, not required' },
43183
+ { kohta: 9, field: "Verokanta", english: "VAT rate", status: "implemented", relevance: "conditional", note: "State each applicable rate when the seller owes VAT. Do not state a VAT rate on a reverse-charge invoice (Vero 5.1.9)" },
43184
+ { kohta: 10, field: "Suoritettavan veron m\xE4\xE4r\xE4", english: "VAT amount", status: "implemented", relevance: "conditional", note: "State VAT payable by the seller; do not state a VAT amount on a reverse-charge or qualifying margin-scheme invoice. An unregistered seller must not state VAT/rate; pending registration has the explicit Vero 5.1.10 notation exception" },
43185
+ { kohta: 11, field: "Merkint\xE4 verottomuudesta", english: "Tax exemption notation", status: "implemented_for_supported_treatments", relevance: "conditional", note: 'PDF and supported Finvoice categories render stored VAT notes. Simple "Veroton myynti" suffices. Citing AVL section is recommended but not mandatory (Vero.fi 5.1.11)' },
43186
+ { kohta: 12, field: "Merkint\xE4 k\xE4\xE4nnetyst\xE4 verovelvollisuudesta", english: "Reverse charge notation", status: "implemented_for_supported_treatments", relevance: "conditional", note: 'PDF and Finvoice render the stored reverse-charge note. "K\xE4\xE4nnetty verovelvollisuus" phrase is mandatory. Legal basis citation is recommended, not required' },
42532
43187
  { kohta: 13, field: "Merkint\xE4 itselaskutuksesta", english: "Self-billing notation", status: "n/a", relevance: "situational", note: "When buyer issues invoice on seller behalf" },
42533
43188
  { kohta: 14, field: "Uuden kuljetusv\xE4lineen tiedot", english: "New means of transport details", status: "n/a", relevance: "situational" },
42534
43189
  { kohta: 15, field: "Voittomarginaalij\xE4rjestelm\xE4", english: "Margin scheme (used goods)", status: "n/a", relevance: "situational" },
42535
43190
  { kohta: 16, field: "Voittomarginaalij\xE4rjestelm\xE4 \u2014 matkatoimistot", english: "Margin scheme (travel agents)", status: "n/a", relevance: "situational" },
42536
43191
  { kohta: 17, field: "Verollisen sijoituskullan myynti", english: "Taxable investment gold", status: "n/a", relevance: "situational" },
42537
- { kohta: 18, field: "Viittaus alkuper\xE4iseen laskuun", english: "Credit note reference (muutoslasku)", status: "not_implemented", relevance: "conditional", note: "Only mandatory requirement: unambiguous reference to original invoice" }
43192
+ { kohta: 18, field: "Viittaus alkuper\xE4iseen laskuun", english: "Credit note reference (muutoslasku)", status: "implemented", relevance: "conditional", note: "Credit-note PDF and Finvoice include original invoice number and date; identify the changes as well as the original invoice (AVL 209 e \xA7 18 kohta)" }
42538
43193
  ],
42539
43194
  simplified_invoice: {
42540
43195
  threshold: 400,
42541
- note: "Invoices \u2264 400 \u20AC may use simplified format (AVL 209 f \xA7). Also: retail to consumers, restaurant/catering, passenger transport \u2014 regardless of amount"
43196
+ note: "Gross invoice total \u2264 400 \u20AC. Also qualifying retail/near-exclusive consumer sales, restaurant/catering and passenger transport (not services bought for resale), and parking-meter or similar machine receipts. Not available for intra-EU goods supplies, distance sales or another EU state\u2019s reverse-charge supplies (AVL 209 f \xA7).",
43197
+ required: ["Issue date", "Seller name and VAT identifier", "Quantity/type of goods or type of service", "VAT amount or tax base by rate", "For a correction: original invoice reference and the changed information"]
42542
43198
  },
42543
43199
  muutoslasku: {
42544
- status: "not_implemented",
42545
- mandatory: "Unambiguous reference to original invoice (number or period)",
42546
- recommended: "Hyvityslasku label, reason for credit, same numbering sequence \u2014 common practice, not legally mandated"
43200
+ status: "implemented",
43201
+ mandatory: "Unambiguous reference to the original invoice and the information being changed (AVL 209 e \xA7 18 kohta)",
43202
+ recommended: "Label and reason help interpretation. A separate systematic credit-note numbering series is allowed; the original rate is preserved.",
43203
+ implementation: "Canonical credit-note flow, SPA and server PDF original reference, and Finvoice OriginalInvoiceNumber/OriginalInvoiceDate. service_outside_eu PDF is supported; its Finvoice O category remains blocked pending complete validation."
42547
43204
  }
42548
43205
  };
42549
43206
  var FILING_DEADLINES = {
42550
43207
  vat_filing: {
42551
43208
  thresholds: [
42552
- { revenue: "< 30,000 \u20AC", periods: ["monthly", "quarterly", "annually"], note: "Annual: deadline last day of February of following year (28.2 or 29.2 in leap year)" },
42553
- { revenue: "30,000\u2013100,000 \u20AC", periods: ["monthly", "quarterly"], note: "Quarterly: 12th of 2nd month after quarter" },
43209
+ { revenue: "\u2264 30,000 \u20AC", periods: ["monthly", "quarterly", "annually"], note: "Annual: deadline last day of February of following year (28.2 or 29.2 in leap year)" },
43210
+ { revenue: "> 30,000 and \u2264 100,000 \u20AC", periods: ["monthly", "quarterly"], note: "Quarterly: 12th of 2nd month after quarter" },
42554
43211
  { revenue: "> 100,000 \u20AC", periods: ["monthly"], note: "Monthly: 12th of 2nd month after reporting month" }
42555
43212
  ],
43213
+ eligibility: "Calendar-year turnover or equivalent revenue, including foreign turnover. Monthly is the default. A longer period requires eligibility and registration/application; follow the Tax Administration decision and its effective date. Do not switch from turnover alone. Primary production and visual artists have separate rules.",
43214
+ calendar: "When a due date falls on a weekend or Finnish holiday, use the next permitted business day. Shortened transition periods can have their own due date.",
42556
43215
  monthly_example: "January 2026 VAT \u2192 deadline 12.3.2026",
42557
43216
  quarterly_deadlines: {
42558
43217
  Q1: "12.5",
@@ -42562,24 +43221,24 @@ var FILING_DEADLINES = {
42562
43221
  }
42563
43222
  },
42564
43223
  tyel: {
42565
- monthly_report: "5th of following month \u2192 pension company",
42566
- payment: "20th of following month \u2192 pension company"
43224
+ monthly_report: "Report earnings and pension-insurance details to Tulorekisteri within 5 calendar days after each payday; the pension company receives the data from the register",
43225
+ payment: "Pay the pension insurer\u2019s invoice by its agreed due date. For example, Ilmarinen normally uses the last day of the month after payday; a later agreed date can include insurance premium interest. No universal 20th-day TyEL deadline"
42567
43226
  },
42568
43227
  tulorekisteri: {
42569
43228
  palkkatietoilmoitus: "5 calendar days from payment date \u2192 Tulorekisteri",
42570
43229
  tyonantajan_erillisilmoitus: "5th of following month \u2192 Tulorekisteri (sotu totals)"
42571
43230
  },
42572
- sairausvakuutusmaksu_payment: "12th of following month \u2192 Vero (OmaVero, oma-aloitteinen)",
43231
+ sairausvakuutusmaksu_payment: "Monthly employer period: 12th of the month after payday; an approved quarterly employer period uses the 12th of the month after the quarter. Reporting to Tulorekisteri still follows its own deadlines; apply business-day adjustment",
42573
43232
  tilinpaatos: "4 months from fiscal year end (KPL 3:6)",
42574
43233
  prh_filing: "8 months from fiscal year end (OYL 8:10)",
42575
43234
  pienyrittaja: {
42576
43235
  threshold: 2e4,
42577
- note: "Revenue under 20,000 \u20AC/year: no VAT registration obligation (from 1.1.2025, was 15,000 \u20AC)",
42578
- alarajahuojennus: "REMOVED from 1.1.2025 \u2014 hard threshold, no progressive relief"
43236
+ note: "From 1.1.2025: qualifying domestic small-business sales can be exempt if turnover is at most 20,000 \u20AC in both the current and preceding calendar year. Registration begins at the sale crossing the current-year threshold, including that whole sale. Voluntary registration and special purchase/import obligations are separate; no general exemption from foreign-service reverse charge",
43237
+ alarajahuojennus: "Removed for periods from 1.1.2025; historical relief for earlier periods is not rewritten"
42579
43238
  },
42580
43239
  maksuperusteinen_alv: {
42581
43240
  threshold: 5e5,
42582
- note: "Turnover up to 500,000 \u20AC per calendar year: domestic sales VAT may be reported when payment is received and purchase VAT deducted when paid (cash basis), consistently for all domestic sales and purchases; imports, exports, intra-EU sales and acquisitions and cross-border reverse-charge services stay on the accrual basis. Kirjapro reports on the accrual basis only.",
43241
+ note: "Turnover up to 500,000 \u20AC per calendar year: domestic sales VAT may be reported when payment is received and purchase VAT deducted when paid (cash basis), consistently for all domestic sales and purchases; imports, exports, intra-EU sales and acquisitions and cross-border reverse-charge services stay on the accrual basis. Check the actual company basis and every posting/reporting path before using this legal option; this resource does not switch the application\u2019s VAT basis.",
42583
43242
  application: "No application: the business applies the cash basis in its bookkeeping. Since 2025 the VAT return has no cash-basis field.",
42584
43243
  source: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/pienyrityksen-maksuperusteinen-alv/",
42585
43244
  sources_verified_at: "2026-10-02"
@@ -42624,18 +43283,17 @@ var FILING_DEADLINES = {
42624
43283
  var SOCIAL_INSURANCE_2026 = {
42625
43284
  year: FINNISH_RATES_2026.year,
42626
43285
  sources: [
42627
- "https://www.etk.fi/ajankohtaista/tyoelakemaksut-vuonna-2026/",
42628
- "https://www.elo.fi/fi-fi/tyonantaja/tyel-vakuuttaminen/tyel-maksu/sosiaalivakuutusmaksut-2026",
42629
- "https://tyel-vakuutus.fi/tyonantajan-sivukulut/"
43286
+ ...regulatoryFreshness("payroll").sources.map((source) => source.url),
43287
+ ...regulatoryFreshness("payrollEstimates").sources.map((source) => source.url)
42630
43288
  ],
42631
43289
  tyel: {
42632
43290
  keskimaarainen: FINNISH_RATES_2026.social.tyelKeskimaarainen,
42633
43291
  perusmaksu: FINNISH_RATES_2026.social.tyelPerusmaksu,
42634
- tilapaistyonantaja: 25.85,
43292
+ tilapaistyonantaja: FINNISH_RATES_2026.social.tyelTemporary,
42635
43293
  employee: FINNISH_RATES_2026.social.tyelEmployee,
42636
43294
  employer_average: FINNISH_RATES_2026.social.tyelEmployerAverage,
42637
43295
  employer_perusmaksu: FINNISH_RATES_2026.social.tyelEmployer,
42638
- note: "Perusmaksu 24.85% is what pension companies bill before company-specific adjustments. Keskim\xE4\xE4r\xE4inen 24.40% is the actual average. Age differentiation eliminated in 2026 (ETK)"
43296
+ note: "Employee share is statutory for the applicable 2026 insured employee. Base, temporary-employer and average total premiums are different models. The actual employer share follows the insurance contract/invoice, including company-specific expenses and adjustments; do not book the average as an exact liability"
42639
43297
  },
42640
43298
  sotu: {
42641
43299
  rate: FINNISH_RATES_2026.social.sotu,
@@ -42647,20 +43305,19 @@ var SOCIAL_INSURANCE_2026 = {
42647
43305
  employer_large: FINNISH_RATES_2026.social.ttvEmployerLarge,
42648
43306
  employer_large_threshold: FINNISH_RATES_2026.social.ttvEmployerLargeThreshold,
42649
43307
  employee: FINNISH_RATES_2026.social.ttvEmployee,
42650
- note: "Ty\xF6tt\xF6myysvakuutus. Employer rate 0.31% for payroll \u2264 2,509,500 \u20AC; 1.23% above. Employee up from 0.59% in 2025"
43308
+ note: "Apply the lower employer share up to the calendar-year payroll threshold and the upper share only to the excess. Age, insurance scope, owner status and exemption conditions are separate; these figures describe an ordinary insured employee."
42651
43309
  },
42652
43310
  tapaturma: {
42653
43311
  average: FINNISH_RATES_2026.social.tapaturmaAverage,
42654
- range: "0.05\u20135.00%",
42655
43312
  payer: "employer",
42656
43313
  note: "Company-specific rate based on industry and claims history"
42657
43314
  },
42658
43315
  ryhmahenkivakuutus: {
42659
43316
  rate: FINNISH_RATES_2026.social.ryhmahenki,
42660
43317
  payer: "employer",
42661
- note: "Standardized via collective agreement"
43318
+ note: "Planning estimate only; obligation and actual premium depend on the applicable collective agreement and insurance contract"
42662
43319
  },
42663
- total_employer_side_costs: "~20% of gross salary (incl. TyEL employer share)",
43320
+ total_employer_side_costs: "Calculate from the applicable employee scope and actual contract premiums; there is no single exact statutory total for every employer",
42664
43321
  // Account numbers are built from the canonical chart (src/accounts.ts) so they
42665
43322
  // can never drift from the YSE 2017 numbering the agent actually books against.
42666
43323
  // DO NOT hardcode literals here — a wrong-but-existing number (e.g. 2320, which
@@ -42680,124 +43337,172 @@ var SOCIAL_INSURANCE_2026 = {
42680
43337
  var BOOKKEEPING_RULES = {
42681
43338
  legal_basis: "Kirjanpitolaki 1336/1997",
42682
43339
  core_principles: [
42683
- { rule: "Double-entry (kahdenkertainen kirjanpito)", basis: "KPL 2:1", note: "Every entry: total debits = total credits" },
43340
+ { rule: "Double-entry (kahdenkertainen kirjanpito)", basis: "KPL 1:2 and 1:1a", note: "Kirjapro uses double-entry: total debits = total credits. Certain natural-person businesses may qualify for statutory single-entry bookkeeping under separate limits" },
42684
43341
  { rule: "Voucher principle (tositeperiaate)", basis: "KPL 2:5", note: "Every entry must be based on a numbered voucher" },
42685
43342
  { rule: "Audit trail (kirjausketju)", basis: "KPL 2:6", note: "Tosite \u2192 tositenumero \u2192 p\xE4iv\xE4kirja \u2192 p\xE4\xE4kirja \u2192 tilinp\xE4\xE4t\xF6s \u2014 unbroken chain" },
42686
43343
  { rule: "Sequential numbering", basis: "KPL 2:5", note: "Gap detection must be possible from the numbering sequence" },
42687
- { rule: "Closed period immutability", basis: "KPL 2:7", note: "Corrections to closed periods must use new entries in current period" }
43344
+ { rule: "Closed period immutability", basis: "KPL 2:7", note: "Kirjapro prevents direct changes in closed fiscal years and preserves correction links. Determine the proper accounting period and any original VAT return correction separately; a current-period voucher does not by itself correct an earlier tax return" }
42688
43345
  ],
42689
43346
  retention: [
42690
- { type: "Accounting records (kirjanpitokirjat)", years: 10, basis: "KPL 2:10 1 mom" },
42691
- { type: "Financial statements (tilinp\xE4\xE4t\xF6s)", years: 10, basis: "KPL 3:9" },
42692
- { type: "Vouchers, invoices (tositteet)", years: 6, basis: "KPL 2:10 2 mom" },
42693
- { type: "E-invoice XML originals", years: 6, basis: "KPL 2:7" }
43347
+ { type: "Accounting ledgers, chart and list of records", years: 10, starts: "From the fiscal-year closing date", basis: "KPL 2:10 1 mom" },
43348
+ { type: "Financial statements and management report", years: 10, starts: "From the fiscal-year closing date", basis: "KPL 2:10 1 mom" },
43349
+ { type: "Vouchers, invoices and business correspondence", years: 6, starts: "At the end of the calendar year during which the fiscal year ended", basis: "KPL 2:10 2 mom" },
43350
+ { type: "E-invoice contents and their audit trail", years: 6, starts: "At the end of the calendar year during which the fiscal year ended", basis: "KPL 2:7, 2:10 2 mom; AVL 209 n\u2013p \xA7", note: "Preserve readable, reproducible information and integrity; keeping source XML is a Kirjapro archive choice, not a universal statutory file-format requirement" }
42694
43351
  ],
43352
+ retention_exceptions: "Other rules can require longer retention: real-estate VAT adjustment invoices/records generally 13 years from the end of the calendar year in which the adjustment period began (AVL 209 q \xA7). Do not delete solely from the general six-year limit.",
42695
43353
  fiscal_year: {
42696
- normal_duration: "12 months",
42697
- max_duration: "18 months (first/changed period only)",
43354
+ normal_duration: "12 months; a sole trader normally uses the calendar year. A different year requires double-entry bookkeeping and financial statements under KPL 1:4.",
43355
+ max_duration: "18 months when starting, ending or changing the fiscal-year end (KPL 1:4)",
42698
43356
  tilinpaatos_deadline: "4 months from fiscal year end",
42699
43357
  prh_deadline: "8 months from fiscal year end (Oy, Oyj)"
42700
43358
  },
42701
43359
  company_sizes: [
42702
- { class: "Mikroyritys", revenue: "< 900K", balance: "< 450K", employees: "< 10", tilinpaatos: "Simplified (PMA)" },
42703
- { class: "Pienyritys", revenue: "< 15M", balance: "< 7.5M", employees: "< 50", tilinpaatos: "Standard (PMA)" },
42704
- { class: "Keskisuuri", revenue: "< 50M", balance: "< 25M", employees: "< 250", tilinpaatos: "Full (KPA) + toimintakertomus for Oy/Osk" },
42705
- { class: "Suuryritys", revenue: "\u2265 50M", balance: "\u2265 25M", employees: "\u2265 250", tilinpaatos: "Full + toimintakertomus + rahoituslaskelma" }
42706
- ]
43360
+ { class: "Mikroyritys", revenue_limit: COMPANY_SIZE_RULE.values.soleTraderStatements.revenue, balance_limit: COMPANY_SIZE_RULE.values.soleTraderStatements.assets, employees_limit: COMPANY_SIZE_RULE.values.soleTraderStatements.employees, framework: "PMA micro if eligible and selected; the small-company PMA framework may be chosen as a complete alternative" },
43361
+ { class: "Pienyritys", revenue_limit: COMPANY_SIZE_RULE.values.smallCompany.revenue, balance_limit: COMPANY_SIZE_RULE.values.smallCompany.assets, employees_limit: COMPANY_SIZE_RULE.values.smallCompany.employees, framework: "PMA small when applicable" }
43362
+ ],
43363
+ classification: "Reaching a limit is not exceeding it. Count exceeded limits per fiscal year and inspect the preceding year and established size class. A continuing business changes its established class after two consecutive years with at least two limits exceeded, or two consecutive years with at most one exceeded on the way back down. KILA 1982/2018 covers the small-company two-year transition. Applying that continuity to micro companies remains an explicitly recorded agent interpretation, not accountant approval. First fiscal years and changes in the legal thresholds need their own dates. Using actual, rather than annualised, turnover for non-12-month micro periods is also an agent interpretation with lower confidence. These figures do not determine audit obligations.",
43364
+ threshold_applicability: "Act 605/2024: these increased limits are mandatory for fiscal years starting on or after 1.1.2025 and optional for those starting on or after 1.1.2024. The registry coverage start is not the first legal effective date.",
43365
+ framework: "Size classification, selected reporting framework, sole-trader financial-statement obligation and audit obligation are distinct. Micro does not mean that every disclosure or a complete statutory financial statement can be omitted."
43366
+ };
43367
+ var CONTENT_REVIEWS = {
43368
+ vat: [{
43369
+ checked_at: "2026-10-03",
43370
+ review_due: "2026-11-20",
43371
+ scope: "Historical rates, deduction conditions, vehicle/staff/marketing distinctions and separate reverse-charge liability/deduction; numerical rate provenance is in numeric_rules.",
43372
+ sources: [
43373
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/arvonlisaveroprosentit/Arvonlisaverokantojen-muutostilanteet/",
43374
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/335806/arvonlis%C3%A4verovelvollisen-opas/",
43375
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48679/palvelujen-ulkomaankaupan--arvonlis%C3%A4verotus2/",
43376
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48462/markkinointi--ja-edustustilaisuudet-arvonlis%C3%A4verotuksessa/",
43377
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ajoneuvon-ja-sen-kayttokulujen-arvonlisavero/",
43378
+ "https://kho.fi/fi/index/paatokset/ennakkopaatokset/1434454271753.html",
43379
+ "https://www.finlex.fi/fi/laki/ajantasa/1993/19931501",
43380
+ "https://www.finlex.fi/fi/laki/ajantasa/1968/19680360"
43381
+ ]
43382
+ }],
43383
+ invoicing: [{
43384
+ checked_at: "2026-10-03",
43385
+ review_due: "2026-11-20",
43386
+ scope: "AVL 209 e/f markings, simplified-invoice exclusions and correction requirements; implementation status is separately checked against the local source tree, not a claim about an already deployed release.",
43387
+ sources: [INVOICE_REQUIREMENTS.source]
43388
+ }],
43389
+ deadlines: [{
43390
+ checked_at: "2026-10-03",
43391
+ review_due: "2026-11-20",
43392
+ scope: "Inclusive 30,000/100,000 limits, application/effective dates, small-business two-calendar-year rule, earnings reports and contractual TyEL payment dates.",
43393
+ sources: [
43394
+ "https://www.finlex.fi/fi/laki/ajantasa/2016/20160768",
43395
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ilmoitus-ja-maksuohjeet/verokauden-muutos/",
43396
+ "https://www.vero.fi/tietoa-verohallinnosta/yhteystiedot-ja-asiointi/lomakkeet/kuvaus/hakemus_omaaloitteisten_verojen_verokau/",
43397
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48658/arvonlisaveroton-vahainen-toiminta/",
43398
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/yritys_tyonantajana/nain-ilmoitat-palkat-ja-tyonantajasuoritukset/",
43399
+ "https://www.ilmarinen.fi/ajankohtaista/uutiset-ja-tiedotteet/2026/tyel-vakuutusmaksukorko-nousee-270-prosenttiin-1.7.2026-alkaen/"
43400
+ ]
43401
+ }, {
43402
+ checked_at: "2026-10-02",
43403
+ review_due: "2026-11-20",
43404
+ valid_through: "2026-12-31",
43405
+ scope: "Earlier review retained: cash-basis scope, late filing, 2026 late-payment interest and VSRALVKV v1.10 field map. A newer deadline edit does not reverify these claims.",
43406
+ sources: [FILING_DEADLINES.maksuperusteinen_alv.source, FILING_DEADLINES.late_penalties.source, FILING_DEADLINES.omavero_field_codes.source_url]
43407
+ }],
43408
+ "social-insurance": [{
43409
+ checked_at: "2026-10-03",
43410
+ review_due: "2026-11-20",
43411
+ scope: "Distinction between statutory employee contributions and employer contract/average estimates. Numerical source checks retain their registry dates.",
43412
+ sources: ["https://www.ilmarinen.fi/tyonantaja/tyel-maksut/", "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/yritys_tyonantajana/sosiaalivakuutusmaksut/"]
43413
+ }],
43414
+ bookkeeping: [{
43415
+ checked_at: "2026-10-03",
43416
+ review_due: "2026-12-01",
43417
+ scope: "KPL retention start dates, AVL real-estate exception, size versus reporting framework and threshold equality. Micro continuity and non-12-month turnover remain labelled interpretations.",
43418
+ sources: [
43419
+ "https://www.finlex.fi/fi/laki/ajantasa/1997/19971336",
43420
+ "https://www.finlex.fi/fi/laki/ajantasa/1993/19931501",
43421
+ "https://kirjanpitolautakunta.fi/-/pienyrityksen-ja-pienkonsernin-raja-arvojen-arvioimises-1",
43422
+ "https://kirjanpitolautakunta.fi/-/konsernikokorajojen-arvioimisesta",
43423
+ "https://www.finlex.fi/fi/lainsaadanto/saadoskokoelma/2024/605"
43424
+ ]
43425
+ }]
42707
43426
  };
42708
43427
  var TOPICS = {
42709
43428
  vat: {
42710
- description: "Finnish VAT compliance: rates (25.5/13.5/10/0%/exempt), rate history, deduction rules (edustus/neuvottelu/insurance), reverse charge, and common mistakes. CRITICAL: 0% (zero-rated, input VAT deductible) \u2260 exempt (no input VAT deduction). Legal basis: AVL 1501/1993.",
42711
- dated: true,
43429
+ description: "Finnish VAT: current and historical rates, conditional deductions and reverse-charge liability separately from deduction. A 2026 document can lawfully carry historical VAT.",
43430
+ rules: ["vat"],
42712
43431
  body: VAT_COMPLIANCE
42713
43432
  },
42714
43433
  invoicing: {
42715
- description: "Invoice requirements per AVL 209 e \xA7: 18 mandatory markings (kohdat 1-18), muutoslasku/hyvityslasku rules, simplified invoice (\u2264400\u20AC + retail/restaurant). Shows implementation status. Source: vero.fi",
42716
- dated: false,
43434
+ description: "AVL 209 e/f invoice markings, simplified-invoice exceptions and credit-note requirements. Implementation status describes supported local code paths, not all possible VAT schemes.",
43435
+ rules: [],
42717
43436
  body: INVOICE_REQUIREMENTS
42718
43437
  },
42719
43438
  deadlines: {
42720
- description: "Finnish tax filing deadlines: VAT verokausi thresholds (<30K annual, <100K quarterly, >100K monthly), TyEL reporting, tilinp\xE4\xE4t\xF6s deadlines, pienyritt\xE4j\xE4 20K\u20AC threshold, maksuperusteinen ALV.",
42721
- dated: true,
43439
+ description: "Finnish filing periods: \u226430K annual / \u2264100K quarterly eligibility and application, VAT and earnings-report deadlines, TyEL invoice terms and two-year 20K small-business exemption.",
43440
+ rules: [],
42722
43441
  body: FILING_DEADLINES
42723
43442
  },
42724
43443
  "social-insurance": {
42725
- description: "2026 Finnish social insurance rates: TyEL (perusmaksu 24.85%, keskim\xE4\xE4r\xE4inen 24.40%, employee 7.30%), sotu (1.91%), TTV (0.31%/0.89%), tapaturma (~0.51%), ryhm\xE4henki (0.06%). Sources: ETK, Elo, tyel-vakuutus.fi",
42726
- dated: true,
43444
+ description: "2026 employee social-insurance rates and separately labelled employer premium estimates; use the actual insurance contract for exact employer liabilities.",
43445
+ rules: ["payroll", "payrollEstimates"],
42727
43446
  body: SOCIAL_INSURANCE_2026
42728
43447
  },
42729
43448
  bookkeeping: {
42730
- description: "KPL (Kirjanpitolaki) requirements: double-entry, audit trail, voucher principle, retention periods (10/6 years), fiscal year rules (max 18 months), company size classification (mikro/pien/keski/suuri).",
42731
- dated: false,
43449
+ description: "Finnish bookkeeping principles, 10/6-year retention with different start dates, fiscal-year requirements and micro/small size boundaries separate from the selected reporting framework.",
43450
+ rules: ["companySizes"],
42732
43451
  body: BOOKKEEPING_RULES
42733
43452
  }
42734
43453
  };
42735
- function wrapWithMeta(body, dated) {
42736
- const meta3 = freshnessMeta();
42737
- return JSON.stringify({
42738
- _meta: {
42739
- published_at: meta3.published_at,
42740
- valid_from: meta3.valid_from,
42741
- valid_through: dated ? meta3.valid_through : null,
42742
- sources_verified_at: meta3.sources_verified_at,
42743
- stale: dated ? meta3.stale : false,
42744
- days_until_expiry: dated ? meta3.days_until_expiry : null,
42745
- dated_rates: dated
42746
- },
42747
- ...body
42748
- });
43454
+ function complianceTopicMeta(slug, today = /* @__PURE__ */ new Date()) {
43455
+ const topic = TOPICS[slug];
43456
+ const day = helsinkiDateString(today);
43457
+ const rules = topic.rules.map((rule) => regulatoryFreshness(rule, today));
43458
+ const reviews = CONTENT_REVIEWS[slug];
43459
+ const dates = [...rules.map((rule) => rule.sources_verified_at), ...reviews.map((review) => review.checked_at)];
43460
+ const expiries = [...rules.map((rule) => rule.valid_through), ...reviews.map((review) => review.valid_through)].filter((date5) => !!date5).sort();
43461
+ const expiry = expiries[0] ?? null;
43462
+ const reviewDue = [...rules.map((rule) => rule.review_due), ...reviews.map((review) => review.review_due)].sort()[0];
43463
+ return {
43464
+ published_at: null,
43465
+ dated_rates: slug === "vat" || slug === "social-insurance" || slug === "deadlines",
43466
+ sources_verified_at: dates.sort()[0],
43467
+ verification_date_meaning: "Oldest included review, not a topic-wide claim that every fact was rechecked on one day. Consult each scope and numeric rule.",
43468
+ valid_from: null,
43469
+ valid_through: expiry,
43470
+ review_due: reviewDue,
43471
+ review_overdue: day > reviewDue,
43472
+ stale: rules.some((rule) => rule.stale) || day > reviewDue || expiry !== null && day > expiry,
43473
+ days_until_expiry: expiry === null ? null : Math.round((Date.parse(`${expiry}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) / 864e5),
43474
+ content_reviews: reviews,
43475
+ numeric_rules: rules,
43476
+ accountant_confirmed: false,
43477
+ decision_status: "Agent-reviewed primary sources; interpretations are provisional and separately identified. Not an accountant approval or a guarantee for a particular transaction.",
43478
+ implementation_verified_at: slug === "invoicing" ? "2026-10-03" : null
43479
+ };
43480
+ }
43481
+ function complianceResource(slug, today = /* @__PURE__ */ new Date()) {
43482
+ return { _meta: complianceTopicMeta(slug, today), ...TOPICS[slug].body };
42749
43483
  }
42750
43484
  function registerComplianceResources(server2) {
42751
- for (const [slug, topic] of Object.entries(TOPICS)) {
43485
+ for (const slug of Object.keys(TOPICS)) {
42752
43486
  server2.registerResource(
42753
43487
  `compliance-${slug}`,
42754
43488
  `kirjapro://compliance/${slug}`,
42755
- {
42756
- description: topic.description,
42757
- mimeType: "application/json"
42758
- },
42759
- async (uri) => ({
42760
- contents: [{
42761
- uri: uri.href,
42762
- mimeType: "application/json",
42763
- text: wrapWithMeta(topic.body, topic.dated)
42764
- }]
42765
- })
43489
+ { description: TOPICS[slug].description, mimeType: "application/json" },
43490
+ async (uri) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(complianceResource(slug)) }] })
42766
43491
  );
42767
43492
  }
42768
43493
  server2.registerResource(
42769
43494
  "compliance-freshness",
42770
43495
  "kirjapro://compliance/_freshness",
42771
- {
42772
- description: "Freshness index for every dated compliance chunk. Read this at session start. Returns valid_through/stale/days_until_expiry for each topic so the agent can detect expired rates before relying on them.",
42773
- mimeType: "application/json"
42774
- },
42775
- async (uri) => {
42776
- const meta3 = freshnessMeta();
42777
- const entries = Object.entries(TOPICS).map(([slug, topic]) => ({
42778
- topic: slug,
42779
- uri: `kirjapro://compliance/${slug}`,
42780
- dated_rates: topic.dated,
42781
- valid_through: topic.dated ? meta3.valid_through : null,
42782
- stale: topic.dated ? meta3.stale : false,
42783
- days_until_expiry: topic.dated ? meta3.days_until_expiry : null
42784
- }));
42785
- return {
42786
- contents: [{
42787
- uri: uri.href,
42788
- mimeType: "application/json",
42789
- text: JSON.stringify({
42790
- _meta: {
42791
- sources_verified_at: meta3.sources_verified_at,
42792
- published_at: meta3.published_at,
42793
- rates_module: "src/compliance/rates.ts",
42794
- source: FINNISH_RATES_2026.source
42795
- },
42796
- topics: entries
42797
- })
42798
- }]
42799
- };
42800
- }
43496
+ { description: "Per-topic legal validity, review dates, overdue checks, sources and interpretation status. A review date is not a law\u2019s effective date; indefinite validity does not mean reviewed forever.", mimeType: "application/json" },
43497
+ async (uri) => ({ contents: [{
43498
+ uri: uri.href,
43499
+ mimeType: "application/json",
43500
+ text: JSON.stringify({
43501
+ _meta: { published_at: null, sources_verified_at: null, provenance: "Review dates and validity are scoped per topic below" },
43502
+ registry: "regulatory/registry.json (generated package-local subset)",
43503
+ topics: Object.keys(TOPICS).map((slug) => ({ topic: slug, uri: `kirjapro://compliance/${slug}`, ...complianceTopicMeta(slug) }))
43504
+ })
43505
+ }] })
42801
43506
  );
42802
43507
  }
42803
43508
 
@@ -42984,7 +43689,7 @@ var INSTRUCTIONS_FULL_ADDENDUM = `
42984
43689
 
42985
43690
  ### Booking Shortcuts
42986
43691
  Faster than composing journal entries via \`kirjapro_create_transaction\`:
42987
- - \`kirjapro_book_sale\` / \`kirjapro_book_purchase\` \u2014 one-liner with auto VAT splitting. A VAT-registered (ALV) company books sales as invoices (\`kirjapro_create_invoice\` + \`kirjapro_send_invoice\`): \`kirjapro_book_sale\` and \`kirjapro_book_purchase\` with \`reverseCharge\` are refused for it
43692
+ - \`kirjapro_book_sale\` / \`kirjapro_book_purchase\` \u2014 one-liner with auto VAT splitting. A VAT-registered (ALV) company books sales as invoices (\`kirjapro_create_invoice\` + \`kirjapro_send_invoice\`): \`kirjapro_book_sale\` is refused for it. Purchases use \`kirjapro_book_purchase\` with source \`vatRateBasis\` and a documented deduction share; reverse charge records the full buyer tax, including when deduction is zero. Import means customs-tax assessment only on the complete customs base, with no duplicate supplier purchase/payment
42988
43693
  - \`kirjapro_book_vat_settlement\` \u2014 settles the company's VAT period from ALV-saamiset/ALV-velka. Call \`kirjapro_get_alv_summary\` for the period first to preview the net and its reconciliation.
42989
43694
  - **Payroll is intentionally NOT exposed.** Direct users to Palkkaus.fi / Palkka.fi for salary calculation; the payslip can then be booked manually via \`kirjapro_create_transaction\`.
42990
43695
  - \`kirjapro_list_templates\` \u2192 \`kirjapro_create_from_template\` for common patterns (built-in templates; the sale templates are hidden and refused for a VAT-registered company)
@@ -43140,15 +43845,19 @@ async function main() {
43140
43845
  });
43141
43846
  registerResources(server);
43142
43847
  registerPrompts(server);
43143
- const freshness = freshnessMeta(FINNISH_RATES_2026);
43144
- if (freshness.stale) {
43145
- console.error(
43146
- `[kirjapro-mcp] WARNING: Finnish rates dataset is STALE (valid through ${freshness.valid_through}). Running on expired VAT and social-insurance rates. Upgrade @kirjapro/mcp-server immediately.`
43147
- );
43148
- } else if (freshness.days_until_expiry < 30) {
43149
- console.error(
43150
- `[kirjapro-mcp] NOTICE: Finnish rates dataset expires in ${freshness.days_until_expiry} days (${freshness.valid_through}). Upgrade @kirjapro/mcp-server before year-end to avoid stale rates.`
43151
- );
43848
+ for (const rule of ["vat", "payroll", "payrollEstimates"]) {
43849
+ const freshness = regulatoryFreshness(rule);
43850
+ if (freshness.expired || freshness.not_yet_effective) {
43851
+ console.error(
43852
+ `[kirjapro-mcp] WARNING: Bundled ${rule} rules do not cover today (valid ${freshness.valid_from} through ${freshness.valid_through ?? "no scheduled end"}). Verify the applicable event-date rule before booking; upgrade the package for current rules.`
43853
+ );
43854
+ } else if (freshness.review_overdue) {
43855
+ console.error(
43856
+ `[kirjapro-mcp] WARNING: Source review for ${rule} is overdue (${freshness.review_due}). Recheck the listed primary sources; an overdue review does not itself mean the law has changed.`
43857
+ );
43858
+ } else if (freshness.days_until_expiry !== null && freshness.days_until_expiry < 30) {
43859
+ console.error(`[kirjapro-mcp] NOTICE: Bundled ${rule} rules cover ${freshness.days_until_expiry} more days (${freshness.valid_through}).`);
43860
+ }
43152
43861
  }
43153
43862
  const transport = new StdioServerTransport();
43154
43863
  transport.onclose = () => void shutdown();