@kirjapro/mcp-server 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4625,7 +4625,7 @@ var require_core = __commonJS({
4625
4625
  errorsText(errors = this.errors, { separator = ", ", dataVar = "data" } = {}) {
4626
4626
  if (!errors || errors.length === 0)
4627
4627
  return "No errors";
4628
- return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text, msg) => text + separator + msg);
4628
+ return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text2, msg) => text2 + separator + msg);
4629
4629
  }
4630
4630
  $dataMetaSchema(metaSchema, keywordsJsonPointers) {
4631
4631
  const rules = this.RULES.all;
@@ -19919,8 +19919,8 @@ function ko_default() {
19919
19919
  }
19920
19920
 
19921
19921
  // node_modules/zod/v4/locales/lt.js
19922
- var capitalizeFirstCharacter = (text) => {
19923
- return text.charAt(0).toUpperCase() + text.slice(1);
19922
+ var capitalizeFirstCharacter = (text2) => {
19923
+ return text2.charAt(0).toUpperCase() + text2.slice(1);
19924
19924
  };
19925
19925
  function getUnitTypeFromNumber(number4) {
19926
19926
  const abs = Math.abs(number4);
@@ -35679,7 +35679,7 @@ var StdioServerTransport = class {
35679
35679
  import { createClient } from "@supabase/supabase-js";
35680
35680
 
35681
35681
  // src/version.ts
35682
- var VERSION = "0.12.0";
35682
+ var VERSION = "0.14.0";
35683
35683
 
35684
35684
  // src/auth.ts
35685
35685
  var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
@@ -36017,15 +36017,15 @@ function resultErrorCode(result) {
36017
36017
  const match = textPart?.text.match(/^([A-Z][A-Z0-9_]{1,63}):/);
36018
36018
  const candidate = match?.[1];
36019
36019
  if (candidate && SAFE_TOOL_ERROR_CODES.has(candidate)) return candidate;
36020
- const text = (textPart?.text ?? "").replace(/^MCP error -?\d+: /, "");
36021
- if (text.startsWith("Input validation error:")) return "INVALID_ARGUMENTS";
36022
- if (text.startsWith("Output validation error:")) {
36020
+ const text2 = (textPart?.text ?? "").replace(/^MCP error -?\d+: /, "");
36021
+ if (text2.startsWith("Input validation error:")) return "INVALID_ARGUMENTS";
36022
+ if (text2.startsWith("Output validation error:")) {
36023
36023
  return "OUTPUT_VALIDATION_ERROR";
36024
36024
  }
36025
- if (text.startsWith("Tool ") && text.endsWith(" not found")) {
36025
+ if (text2.startsWith("Tool ") && text2.endsWith(" not found")) {
36026
36026
  return "UNKNOWN_TOOL";
36027
36027
  }
36028
- if (text.startsWith("Tool ") && text.endsWith(" disabled")) {
36028
+ if (text2.startsWith("Tool ") && text2.endsWith(" disabled")) {
36029
36029
  return "TOOL_DISABLED";
36030
36030
  }
36031
36031
  return "TOOL_ERROR";
@@ -36217,11 +36217,11 @@ function instrumentToolCalls(server2, options) {
36217
36217
  }
36218
36218
 
36219
36219
  // src/utils.ts
36220
- function formatCurrency(amount) {
36220
+ function formatCurrency(amount2) {
36221
36221
  return new Intl.NumberFormat("fi-FI", {
36222
36222
  style: "currency",
36223
36223
  currency: "EUR"
36224
- }).format(amount);
36224
+ }).format(amount2);
36225
36225
  }
36226
36226
  function helsinkiDateString(now = /* @__PURE__ */ new Date()) {
36227
36227
  const parts = new Intl.DateTimeFormat("fi-FI", {
@@ -36363,8 +36363,8 @@ async function invokeEdgeFunction(supabase2, name, body) {
36363
36363
  }
36364
36364
  return { data, error: null };
36365
36365
  }
36366
- function roundCents(amount) {
36367
- return Math.round(amount * 100) / 100;
36366
+ function roundCents(amount2) {
36367
+ return Math.round(amount2 * 100) / 100;
36368
36368
  }
36369
36369
  var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
36370
36370
  var CONTROL = /[\u0000-\u001F\u007F]/g;
@@ -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}`;
@@ -36830,6 +36818,13 @@ function registerTransactionTools(server2) {
36830
36818
  "Failed to search transactions. Check your filters and try again."
36831
36819
  );
36832
36820
  }
36821
+ const knownCount = Number.isSafeInteger(count) ? count : null;
36822
+ if (hasAmountFilter && (knownCount === null || knownCount > AMOUNT_FILTER_CAP)) {
36823
+ const matches = knownCount !== null ? `${knownCount} match the other filters` : "the number matching the other filters is unknown";
36824
+ return toErrorContent(
36825
+ `FILTER_TOO_BROAD: Amount filters read at most ${AMOUNT_FILTER_CAP} transactions, and ${matches}. Narrow dateFrom/dateTo or query; no partial count is returned.`
36826
+ );
36827
+ }
36833
36828
  let results = (data || []).map(
36834
36829
  (t) => {
36835
36830
  const totalDebit = roundCents(
@@ -36870,10 +36865,9 @@ function registerTransactionTools(server2) {
36870
36865
  }
36871
36866
  const filteredTotal = results.length;
36872
36867
  const paginatedResults = results.slice(offset, offset + limit);
36873
- const cappedWarning = (count || 0) >= AMOUNT_FILTER_CAP ? " (results capped at 1000 rows \u2014 narrow your date range for complete results)" : "";
36874
36868
  return toResult({
36875
36869
  ...paginate(paginatedResults, filteredTotal, offset, limit),
36876
- amount_filter_note: `Filtered client-side from ${count || 0} transactions${cappedWarning}`
36870
+ amount_filter_note: `Filtered client-side from ${count} transactions`
36877
36871
  });
36878
36872
  }
36879
36873
  return toResult(paginate(results, count || 0, offset, limit));
@@ -37307,45 +37301,133 @@ var VALID_TRANSITIONS = {
37307
37301
  cancelled: []
37308
37302
  };
37309
37303
 
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;
37304
+ // src/shared/invoice-draft.ts
37305
+ var INVOICE_VAT_TREATMENT_CODES = [
37306
+ "standard_25_5",
37307
+ "standard_13_5",
37308
+ "standard_10",
37309
+ "not_vat_registered",
37310
+ "reverse_construction",
37311
+ "reverse_scrap",
37312
+ "reverse_eu_service",
37313
+ "export_outside_eu",
37314
+ "service_outside_eu",
37315
+ "intra_eu_goods",
37316
+ "exempt_healthcare",
37317
+ "exempt_social_welfare",
37318
+ "exempt_education",
37319
+ "exempt_financial",
37320
+ "exempt_insurance",
37321
+ "exempt_other",
37322
+ "legacy_standard_24",
37323
+ "legacy_standard_14"
37324
+ ];
37325
+ var INVOICE_VAT_RATES = ["25.5", "13.5", "10", "0", "24", "14"];
37326
+ var INVOICE_DRAFT_MAX_LINES = 100;
37327
+ var INVOICE_DRAFT_CUSTOMER_COLUMNS = "id, name, payment_term_days, customer_kind, business_id, vat_number, country";
37328
+ var ERROR_MESSAGES = {
37329
+ INVALID_INPUT: (detail) => `Laskun tiedoissa on virhe: ${detail}.`,
37330
+ CUSTOMER_NOT_FOUND: (detail) => `Asiakasta ei l\xF6ydy t\xE4st\xE4 yrityksest\xE4${detail ? ` (${detail})` : ""}.`,
37331
+ CUSTOMER_AMBIGUOUS: (detail) => `Asiakas pit\xE4\xE4 valita t\xE4sm\xE4llisesti: ${detail}. K\xE4yt\xE4 t\xE4sm\xE4llist\xE4 nime\xE4 tai asiakasnumeroa.`,
37332
+ LOOKUP_FAILED: () => "Laskun tietojen haku ep\xE4onnistui. Mit\xE4\xE4n ei tallennettu; voit yritt\xE4\xE4 uudelleen.",
37333
+ FISCAL_YEAR_REQUIRED: () => "Ei tilikautta valittuna.",
37334
+ FISCAL_YEAR_NOT_FOUND: () => "Tilikautta ei l\xF6ydy. Tarkista valinta.",
37335
+ FISCAL_YEAR_CLOSED: () => "Tilikausi on suljettu. Laskua ei voi luoda.",
37336
+ VAT_TREATMENT_REQUIRED: () => "0 % ALV vaatii t\xE4sm\xE4llisen ALV-k\xE4sittelyn syyn.",
37337
+ VAT_SELLER_NOT_REGISTERED: () => "ALV-rekisteriin kuulumaton myyj\xE4 voi k\xE4ytt\xE4\xE4 vain Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
37338
+ VAT_SELLER_REGISTERED: () => "ALV-rekisteriss\xE4 oleva myyj\xE4 ei voi k\xE4ytt\xE4\xE4 Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
37339
+ VAT_RATE_NOT_IN_FORCE: (detail) => `ALV-kanta ${detail}.`,
37340
+ VAT_RATE_MISMATCH: (detail) => `ALV-kanta ja ALV-k\xE4sittely ovat ristiriidassa (${detail}). Anna vain k\xE4sittely tai sit\xE4 vastaava kanta.`,
37341
+ VAT_REQUIRES_BUSINESS_CUSTOMER: () => "Valittu ALV-k\xE4sittely vaatii yritysasiakkaan.",
37342
+ VAT_REQUIRES_FINNISH_BUSINESS_ID: () => "Kotimainen k\xE4\xE4nnetty verovelvollisuus vaatii ostajan kelvollisen Y-tunnuksen.",
37343
+ VAT_REQUIRES_EU_VAT_NUMBER: () => "EU-myynti vaatii muun EU-maan maakoodin ja sit\xE4 vastaavan ostajan ALV-tunnisteen.",
37344
+ VAT_REQUIRES_NON_EU_COUNTRY: () => "EU:n ulkopuolinen myynti vaatii ostajalle EU:n ulkopuolisen maan.",
37345
+ INVOICE_NUMBER_CONFLICT: () => "Laskunumero on jo k\xE4yt\xF6ss\xE4 (samanaikainen luonti). Yrit\xE4 uudelleen.",
37346
+ INVALID_LINE: () => "Laskurivi ei kelpaa. Tarkista m\xE4\xE4r\xE4, hinta ja ALV-k\xE4sittely.",
37347
+ PERMISSION_DENIED: () => "Ei oikeutta luoda laskua t\xE4lle yritykselle.",
37348
+ MFA_REQUIRED: () => "Vahvista kaksivaiheinen tunnistautuminen Kirjaprossa ja yrit\xE4 sitten uudelleen.",
37349
+ WRITE_REJECTED: () => "Laskun luonti hyl\xE4ttiin. Mit\xE4\xE4n ei tallennettu.",
37350
+ OUTCOME_UNKNOWN: () => "Laskun luonnin lopputulosta ei voitu varmistaa. Tarkista laskulista ennen uutta yrityst\xE4."
37351
+ };
37352
+ function retryFor(code) {
37353
+ if (code === "OUTCOME_UNKNOWN") return "reconcile";
37354
+ return code === "LOOKUP_FAILED" || code === "INVOICE_NUMBER_CONFLICT" ? "safe" : "never";
37355
+ }
37356
+ function invoiceDraftError(code, detail = "") {
37357
+ return { code, message: ERROR_MESSAGES[code](detail), retry: retryFor(code) };
37358
+ }
37359
+ function roundNumericMoney(value) {
37360
+ if (!Number.isFinite(value)) return value;
37361
+ if (Math.abs(value) >= 9e13) return Math.round(value * 100) / 100;
37362
+ const digits = Math.abs(value).toPrecision(15);
37363
+ if (digits.includes("e")) return 0;
37364
+ const [whole = "0", fraction = ""] = digits.split(".");
37365
+ const cents3 = Number(whole) * 100 + Number(fraction.slice(0, 2).padEnd(2, "0")) + (Number(fraction.charAt(2) || "0") >= 5 ? 1 : 0);
37366
+ if (cents3 === 0) return 0;
37367
+ return value < 0 ? -cents3 / 100 : cents3 / 100;
37368
+ }
37369
+ function calculateLineTotals(quantity, unitPrice, vatRate, priceEntryMode = "vat_exclusive", discountPercent = 0) {
37370
+ const discountFactor = 1 - discountPercent / 100;
37371
+ if (priceEntryMode === "vat_inclusive" && vatRate > 0) {
37372
+ const total3 = roundNumericMoney(quantity * unitPrice * discountFactor);
37373
+ const vatAmount2 = roundNumericMoney(total3 * vatRate / (100 + vatRate));
37374
+ const subtotal2 = roundNumericMoney(total3 - vatAmount2);
37375
+ return { subtotal: subtotal2, vatAmount: vatAmount2, total: total3 };
37376
+ }
37377
+ const subtotal = roundNumericMoney(quantity * unitPrice * discountFactor);
37378
+ const vatAmount = roundNumericMoney(subtotal * (vatRate / 100));
37379
+ const total2 = roundNumericMoney(subtotal + vatAmount);
37380
+ return { subtotal, vatAmount, total: total2 };
37381
+ }
37382
+ function calculateInvoiceTotals(lines) {
37383
+ const subtotalCents = lines.reduce((sum, line) => sum + Math.round(line.subtotal * 100), 0);
37384
+ const vatTotalCents = lines.reduce((sum, line) => sum + Math.round(line.vat_amount * 100), 0);
37385
+ const totalCents = lines.reduce((sum, line) => sum + Math.round(line.total * 100), 0);
37386
+ return {
37387
+ subtotal: subtotalCents / 100,
37388
+ vatTotal: vatTotalCents / 100,
37389
+ total: totalCents / 100
37390
+ };
37345
37391
  }
37346
-
37347
- // src/vat.ts
37348
- var EU_COUNTRY_CODES = /* @__PURE__ */ new Set([
37392
+ var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
37393
+ function dateParts(date5) {
37394
+ const match = ISO_DATE.exec(date5);
37395
+ if (!match) return null;
37396
+ const year = Number(match[1]);
37397
+ const month = Number(match[2]);
37398
+ const day = Number(match[3]);
37399
+ const utc = new Date(Date.UTC(year, month - 1, day));
37400
+ return utc.getUTCFullYear() === year && utc.getUTCMonth() === month - 1 && utc.getUTCDate() === day ? [year, month, day] : null;
37401
+ }
37402
+ function addDaysIso(date5, days) {
37403
+ const parts = dateParts(date5);
37404
+ if (!parts) throw new RangeError(`Invalid date: ${date5}`);
37405
+ const [year, month, day] = parts;
37406
+ return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
37407
+ }
37408
+ var TREATMENT_RATES = {
37409
+ standard_25_5: 25.5,
37410
+ standard_13_5: 13.5,
37411
+ standard_10: 10,
37412
+ legacy_standard_24: 24,
37413
+ legacy_standard_14: 14
37414
+ };
37415
+ var STANDARD_TREATMENTS = {
37416
+ "25.5": "standard_25_5",
37417
+ "13.5": "standard_13_5",
37418
+ "10": "standard_10",
37419
+ "24": "legacy_standard_24",
37420
+ "14": "legacy_standard_14"
37421
+ };
37422
+ function resolveInvoiceVatTreatment(vatRate, code) {
37423
+ const resolved = code ?? (vatRate === void 0 ? void 0 : STANDARD_TREATMENTS[vatRate]);
37424
+ if (!resolved) return invoiceDraftError("VAT_TREATMENT_REQUIRED");
37425
+ const rate = TREATMENT_RATES[resolved] ?? 0;
37426
+ if (vatRate !== void 0 && Number(vatRate) !== rate)
37427
+ return invoiceDraftError("VAT_RATE_MISMATCH", `${vatRate} % ja ${resolved}`);
37428
+ return { code: resolved, rate };
37429
+ }
37430
+ var EU_COUNTRY_CODES = [
37349
37431
  "AT",
37350
37432
  "BE",
37351
37433
  "BG",
@@ -37373,51 +37455,174 @@ var EU_COUNTRY_CODES = /* @__PURE__ */ new Set([
37373
37455
  "SE",
37374
37456
  "SI",
37375
37457
  "SK"
37376
- ]);
37377
- function normalizedVatNumber(value) {
37378
- return (value ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
37379
- }
37380
- function validateVatTreatmentEligibility({
37381
- code,
37382
- sellerAlvRegistered,
37383
- customer,
37384
- documentDate
37385
- }) {
37386
- if (!sellerAlvRegistered && code !== "not_vat_registered") {
37387
- return "A seller outside the VAT register can only use not_vat_registered.";
37388
- }
37389
- if (sellerAlvRegistered && code === "not_vat_registered") {
37390
- return "A VAT-registered seller cannot use not_vat_registered.";
37391
- }
37392
- if (code === "standard_25_5" && documentDate < "2024-09-01") {
37393
- return "standard_25_5 is not valid before 2024-09-01.";
37394
- }
37395
- if (code === "standard_13_5" && documentDate < "2026-01-01") {
37396
- return "standard_13_5 is not valid before 2026-01-01.";
37397
- }
37398
- const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu";
37399
- if (requiresBusiness && customer?.customer_kind !== "business") {
37400
- return "The selected VAT treatment requires a business customer.";
37401
- }
37402
- if ((code === "reverse_construction" || code === "reverse_scrap") && !isValidFinnishBusinessId(customer?.business_id ?? "")) {
37403
- return "Domestic reverse charge requires a Finnish buyer business ID in the form 1234567-8.";
37404
- }
37458
+ ];
37459
+ function isFinnishBusinessId(value) {
37460
+ const match = /^(\d{7})-(\d)$/.exec(value.replace(/\s/g, ""));
37461
+ if (!match) return false;
37462
+ const digits = match[1] ?? "";
37463
+ const weights = [7, 9, 10, 5, 8, 4, 2];
37464
+ let sum = 0;
37465
+ for (let index = 0; index < weights.length; index += 1)
37466
+ sum += Number(digits.charAt(index)) * (weights[index] ?? 0);
37467
+ const remainder = sum % 11;
37468
+ if (remainder === 1) return false;
37469
+ return Number(match[2]) === (remainder === 0 ? 0 : 11 - remainder);
37470
+ }
37471
+ function invoiceVatTreatmentError(code, sellerAlvRegistered, customer, documentDate) {
37472
+ if (!sellerAlvRegistered && code !== "not_vat_registered")
37473
+ return invoiceDraftError("VAT_SELLER_NOT_REGISTERED");
37474
+ if (sellerAlvRegistered && code === "not_vat_registered")
37475
+ return invoiceDraftError("VAT_SELLER_REGISTERED");
37476
+ if (code === "standard_25_5" && documentDate < "2024-09-01")
37477
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "25,5 % ei ole voimassa ennen 1.9.2024");
37478
+ if (code === "standard_13_5" && documentDate < "2026-01-01")
37479
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "13,5 % ei ole voimassa ennen 1.1.2026");
37480
+ if (code === "legacy_standard_24" && documentDate > "2024-08-31")
37481
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "24 % on voimassa vain 31.8.2024 asti");
37482
+ if (code === "legacy_standard_14" && documentDate > "2025-12-31")
37483
+ return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "14 % on voimassa vain 31.12.2025 asti");
37484
+ if ((code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu") && customer.customer_kind !== "business")
37485
+ return invoiceDraftError("VAT_REQUIRES_BUSINESS_CUSTOMER");
37486
+ if ((code === "reverse_construction" || code === "reverse_scrap") && !isFinnishBusinessId(customer.business_id ?? ""))
37487
+ return invoiceDraftError("VAT_REQUIRES_FINNISH_BUSINESS_ID");
37488
+ const country = (customer.country ?? "").trim().toUpperCase();
37405
37489
  if (code === "reverse_eu_service" || code === "intra_eu_goods") {
37406
- const country = (customer?.country ?? "").trim().toUpperCase();
37407
- const vatNumber = normalizedVatNumber(customer?.vat_number);
37490
+ const vatNumber = (customer.vat_number ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
37408
37491
  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
- }
37492
+ if (!EU_COUNTRY_CODES.includes(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix))
37493
+ return invoiceDraftError("VAT_REQUIRES_EU_VAT_NUMBER");
37412
37494
  }
37413
- if (code === "export_outside_eu" || code === "service_outside_eu") {
37414
- const country = (customer?.country ?? "").trim().toUpperCase();
37415
- if (!country || EU_COUNTRY_CODES.has(country)) {
37416
- return "Export outside the EU requires a buyer country outside the EU.";
37417
- }
37495
+ if ((code === "export_outside_eu" || code === "service_outside_eu") && (!country || EU_COUNTRY_CODES.includes(country)))
37496
+ return invoiceDraftError("VAT_REQUIRES_NON_EU_COUNTRY");
37497
+ return null;
37498
+ }
37499
+ function inputError(input2) {
37500
+ if (!dateParts(input2.invoiceDate)) return "laskun p\xE4iv\xE4m\xE4\xE4r\xE4 ei ole kelvollinen";
37501
+ if (input2.lines.length < 1 || input2.lines.length > INVOICE_DRAFT_MAX_LINES)
37502
+ return `laskulla pit\xE4\xE4 olla 1\u2013${INVOICE_DRAFT_MAX_LINES} rivi\xE4`;
37503
+ const term = input2.paymentTermDays;
37504
+ if (term !== void 0 && (!Number.isInteger(term) || term < 0 || term > 365))
37505
+ return "maksuehdon pit\xE4\xE4 olla 0\u2013365 kokonaista p\xE4iv\xE4\xE4";
37506
+ if ((input2.notes ?? "").length > 2e3) return "lis\xE4tiedot ovat yli 2000 merkki\xE4";
37507
+ for (let index = 0; index < input2.lines.length; index += 1) {
37508
+ const line = input2.lines[index];
37509
+ const row = `rivi ${index + 1}`;
37510
+ if (!line) return `${row} puuttuu`;
37511
+ if (!line.description.trim() || line.description.length > 500)
37512
+ return `${row}: kuvauksen pit\xE4\xE4 olla 1\u2013500 merkki\xE4`;
37513
+ if (line.unit.length > 20) return `${row}: yksikk\xF6 on yli 20 merkki\xE4`;
37514
+ if (!Number.isFinite(line.quantity) || line.quantity <= 0 || line.quantity > 1e6)
37515
+ return `${row}: m\xE4\xE4r\xE4n pit\xE4\xE4 olla yli 0 ja enint\xE4\xE4n 1 000 000`;
37516
+ if (!Number.isFinite(line.unitPrice) || line.unitPrice < 0 || line.unitPrice > 1e9)
37517
+ return `${row}: yksikk\xF6hinnan pit\xE4\xE4 olla 0\u20131 000 000 000 \u20AC`;
37518
+ if (line.quantity * line.unitPrice > 1e12) return `${row}: rivin summa on liian suuri`;
37519
+ const discount = line.discountPercent;
37520
+ if (discount !== void 0 && (!Number.isFinite(discount) || discount < 0 || discount > 100 || Math.abs(discount * 100 - Math.round(discount * 100)) > 1e-9))
37521
+ return `${row}: alennuksen pit\xE4\xE4 olla 0\u2013100 % enint\xE4\xE4n kahdella desimaalilla`;
37418
37522
  }
37419
37523
  return null;
37420
37524
  }
37525
+ function prepareInvoiceDraft(input2, facts) {
37526
+ const invalid = inputError(input2);
37527
+ if (invalid) return { ok: false, error: invoiceDraftError("INVALID_INPUT", invalid) };
37528
+ const { customer } = facts;
37529
+ const lines = [];
37530
+ for (const [index, line] of input2.lines.entries()) {
37531
+ const treatment = resolveInvoiceVatTreatment(line.vatRate, line.vatTreatmentCode);
37532
+ if ("message" in treatment) return { ok: false, error: treatment };
37533
+ const ineligible = invoiceVatTreatmentError(
37534
+ treatment.code,
37535
+ facts.sellerAlvRegistered,
37536
+ customer,
37537
+ input2.invoiceDate
37538
+ );
37539
+ if (ineligible) return { ok: false, error: ineligible };
37540
+ const discountPercent = line.discountPercent ?? 0;
37541
+ const amounts = calculateLineTotals(
37542
+ line.quantity,
37543
+ line.unitPrice,
37544
+ treatment.rate,
37545
+ input2.priceEntryMode,
37546
+ discountPercent
37547
+ );
37548
+ lines.push({
37549
+ line_number: index + 1,
37550
+ description: line.description,
37551
+ quantity: line.quantity,
37552
+ unit: line.unit,
37553
+ unit_price: line.unitPrice,
37554
+ entered_unit_price: line.unitPrice,
37555
+ discount_percent: discountPercent,
37556
+ vat_rate: treatment.rate,
37557
+ vat_treatment_code: treatment.code,
37558
+ vat_treatment_overridden: true,
37559
+ price_entry_mode: input2.priceEntryMode,
37560
+ default_vat_treatment_code: treatment.code,
37561
+ reverse_charge_confirmed: input2.reverseChargeConfirmed ?? false,
37562
+ eu_goods_transport_confirmed: input2.euGoodsTransportConfirmed ?? false,
37563
+ subtotal: amounts.subtotal,
37564
+ vat_amount: amounts.vatAmount,
37565
+ total: amounts.total
37566
+ });
37567
+ }
37568
+ const totals = calculateInvoiceTotals(lines);
37569
+ return {
37570
+ ok: true,
37571
+ draft: {
37572
+ customerId: customer.id,
37573
+ customerName: customer.name,
37574
+ invoiceDate: input2.invoiceDate,
37575
+ dueDate: addDaysIso(
37576
+ input2.invoiceDate,
37577
+ input2.paymentTermDays ?? customer.payment_term_days ?? 14
37578
+ ),
37579
+ notes: input2.notes ?? "",
37580
+ lines,
37581
+ subtotal: totals.subtotal,
37582
+ vatTotal: totals.vatTotal,
37583
+ total: totals.total
37584
+ }
37585
+ };
37586
+ }
37587
+ function invoiceDraftRpcArgs(companyId2, draft, audit) {
37588
+ return {
37589
+ p_company_id: companyId2,
37590
+ p_customer_id: draft.customerId,
37591
+ p_invoice_number: null,
37592
+ p_reference_number: null,
37593
+ p_invoice_date: draft.invoiceDate,
37594
+ p_due_date: draft.dueDate,
37595
+ p_status: "draft",
37596
+ p_subtotal: draft.subtotal,
37597
+ p_vat_total: draft.vatTotal,
37598
+ p_total: draft.total,
37599
+ p_notes: draft.notes,
37600
+ p_lines: draft.lines,
37601
+ ...audit ? { p_audit_action: audit.action, p_audit_details: audit.details } : {}
37602
+ };
37603
+ }
37604
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
37605
+ var CONFIRMED_ERROR_CODE = /^(?:[0-9A-Z]{5}|PGRST\d{3})$/;
37606
+ function invoiceDraftWriteOutcome(response) {
37607
+ const { data, error: error61 } = response;
37608
+ if (error61) {
37609
+ const code = error61.code ?? "";
37610
+ if (!CONFIRMED_ERROR_CODE.test(code))
37611
+ return { ok: false, error: invoiceDraftError("OUTCOME_UNKNOWN") };
37612
+ if ((error61.message ?? "").startsWith("AAL2_REQUIRED"))
37613
+ return { ok: false, error: invoiceDraftError("MFA_REQUIRED") };
37614
+ const mapped = {
37615
+ "23505": "INVOICE_NUMBER_CONFLICT",
37616
+ "55000": "FISCAL_YEAR_CLOSED",
37617
+ "42501": "PERMISSION_DENIED",
37618
+ "23514": "INVALID_LINE"
37619
+ };
37620
+ return { ok: false, error: invoiceDraftError(mapped[code] ?? "WRITE_REJECTED") };
37621
+ }
37622
+ if (typeof data !== "string" || !UUID.test(data))
37623
+ return { ok: false, error: invoiceDraftError("OUTCOME_UNKNOWN") };
37624
+ return { ok: true, invoiceId: data };
37625
+ }
37421
37626
 
37422
37627
  // src/lib/invoice-balances.ts
37423
37628
  var CREDIT_PAGE_SIZE = 500;
@@ -37454,20 +37659,48 @@ function calculateInvoiceBalance(total2, payments, creditedTotal) {
37454
37659
  return { paidTotal, remainingBalance: roundCents(total2 - paidTotal - creditedTotal) };
37455
37660
  }
37456
37661
 
37457
- // src/tools/invoices.ts
37458
- function generateReferenceNumber(baseNumber) {
37459
- const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
37460
- if (digits.length < 3 || digits.length > 19) {
37461
- throw new Error("Base number must be 3-19 digits without leading zeros");
37462
- }
37463
- const weights = [7, 3, 1];
37464
- let sum = 0;
37465
- for (let i = digits.length - 1, w = 0; i >= 0; i--, w++) {
37466
- sum += parseInt(digits[i]) * weights[w % 3];
37662
+ // src/shared/invoice-delivery.ts
37663
+ var MESSAGES = {
37664
+ INVOICE_NOT_FOUND: "Laskua ei l\xF6ydy t\xE4st\xE4 yrityksest\xE4. Tarkista laskun numero.",
37665
+ INVOICE_NOT_DRAFT: "Vain luonnostilassa olevan laskun voi l\xE4hett\xE4\xE4. Laskua ei l\xE4hetetty.",
37666
+ DELIVERY_RECIPIENT_MISSING: "Asiakkaalta puuttuu valitun kanavan vastaanottaja (s\xE4hk\xF6postiosoite tai verkkolaskuosoite ja operaattori). Laskua ei l\xE4hetetty.",
37667
+ DELIVERY_CHANGED: "Laskun sis\xE4lt\xF6 tai vastaanottaja muuttui hyv\xE4ksynn\xE4n j\xE4lkeen. Laskua ei l\xE4hetetty; pyyd\xE4 l\xE4hetys uudelleen.",
37668
+ DELIVERY_IN_PROGRESS: "Laskun l\xE4hetys on jo k\xE4sittelyss\xE4 toisessa pyynn\xF6ss\xE4. Tarkista laskun tila ennen uutta yrityst\xE4.",
37669
+ DELIVERY_FAILED: "Laskun l\xE4hetys ep\xE4onnistui. Laskua ei l\xE4hetetty.",
37670
+ DELIVERY_OUTCOME_UNKNOWN: "L\xE4hetyksen tila on ep\xE4varma. \xC4l\xE4 l\xE4het\xE4 laskua uudelleen ennen kuin olet tarkistanut sen tilan.",
37671
+ DELIVERY_UNAVAILABLE: "Laskun l\xE4hetys ei ole k\xE4ytett\xE4viss\xE4 AI-chatissa.",
37672
+ DELIVERY_STOPPED: "L\xE4hetyst\xE4 ei tehty: keskustelun ty\xF6 p\xE4\xE4ttyi tai pys\xE4ytettiin ennen l\xE4hetyst\xE4."
37673
+ };
37674
+ function invoiceDeliveryMessage(code) {
37675
+ return MESSAGES[code];
37676
+ }
37677
+ var KNOWN_CODES = Object.keys(MESSAGES);
37678
+ function invoiceDeliveryErrorCode(value) {
37679
+ return typeof value === "string" && KNOWN_CODES.includes(value) ? value : "DELIVERY_FAILED";
37680
+ }
37681
+ var text = (value) => typeof value === "string" && value ? value : null;
37682
+ function classifyInvoiceDelivery(response) {
37683
+ const body = response.body && typeof response.body === "object" ? response.body : null;
37684
+ const deliveryId = text(body?.deliveryId);
37685
+ const unknown2 = { status: "unknown", deliveryId, errorCode: "DELIVERY_OUTCOME_UNKNOWN" };
37686
+ if (response.httpStatus === 200 && body) {
37687
+ if (body.deliveryStatus === "sent" && body.success === true)
37688
+ return {
37689
+ status: "sent",
37690
+ deliveryId,
37691
+ transmissionId: text(body.transmissionId),
37692
+ messageId: text(body.messageId)
37693
+ };
37694
+ if (body.deliveryStatus === "failed")
37695
+ return { status: "failed", deliveryId, errorCode: invoiceDeliveryErrorCode(body.errorCode) };
37696
+ return unknown2;
37467
37697
  }
37468
- const checkDigit = (10 - sum % 10) % 10;
37469
- return digits + checkDigit;
37698
+ if (response.httpStatus !== null && response.httpStatus >= 400 && response.httpStatus < 500)
37699
+ return { status: "failed", deliveryId, errorCode: "DELIVERY_FAILED" };
37700
+ return unknown2;
37470
37701
  }
37702
+
37703
+ // src/tools/invoices.ts
37471
37704
  var LIST_INVOICES_ROW_SHAPE = {
37472
37705
  id: external_exports.string().uuid(),
37473
37706
  document_type: external_exports.enum(["invoice", "credit_note"]),
@@ -37519,6 +37752,145 @@ var GET_INVOICE_OUTPUT_SHAPE = {
37519
37752
  credited_total: external_exports.number(),
37520
37753
  remaining_balance: external_exports.number()
37521
37754
  };
37755
+ var CREATE_INVOICE_INPUT_SHAPE = {
37756
+ customerId: external_exports.string().uuid().describe("Customer UUID"),
37757
+ invoiceDate: ISO_DATE_SCHEMA.describe("Invoice date (YYYY-MM-DD)"),
37758
+ paymentTermDays: external_exports.number().min(0).max(365).optional().describe("Payment term in days (default: the customer's payment term, else 14)"),
37759
+ notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
37760
+ priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
37761
+ reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
37762
+ euGoodsTransportConfirmed: external_exports.boolean().default(false).describe("Explicit confirmation that intra-EU goods are transported from Finland to another EU country"),
37763
+ lines: external_exports.array(
37764
+ external_exports.object({
37765
+ description: external_exports.string().min(1).max(500).describe("Line item description"),
37766
+ // DB enforces CHECK (quantity > 0); reject 0/negative/Infinity at
37767
+ // the boundary so the agent gets a clear error instead of an
37768
+ // opaque 23514 deep in create_invoice_with_lines.
37769
+ quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
37770
+ unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
37771
+ unitPrice: external_exports.number().finite().min(0).max(1e9).describe("Entered unit price in EUR (must be >= 0)"),
37772
+ vatRate: external_exports.enum(INVOICE_VAT_RATES).optional().describe("VAT rate (default 25.5 when no vatTreatmentCode is given). Must match the treatment's rate when both are given. 24 and 14 are historical rates (until 31.8.2024 and 31.12.2025)"),
37773
+ discountPercent: external_exports.number().min(0).max(100).optional().describe("Line discount in percent (0-100, at most two decimals), applied before VAT"),
37774
+ vatTreatmentCode: external_exports.enum(INVOICE_VAT_TREATMENT_CODES).optional().describe("Structured VAT treatment. Required when VAT rate is 0. service_outside_eu is ONLY for AVL 65 general-rule B2B services: verify business use, evidence of buyer status and receiving establishment outside the EU; special place-of-supply services are excluded. Check foreign buyer liability and invoice notes. PDF delivery only; e-invoice delivery is blocked.")
37775
+ })
37776
+ ).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
37777
+ idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37778
+ };
37779
+ async function createInvoiceDraftCommand(ctx, params) {
37780
+ const fail = (error62) => toErrorContent(`${error62.code}: ${error62.message}`);
37781
+ 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();
37782
+ if (customerError) return fail(invoiceDraftError("LOOKUP_FAILED"));
37783
+ if (!customer) {
37784
+ return fail(
37785
+ invoiceDraftError("CUSTOMER_NOT_FOUND", "hae tunniste kirjapro_list_customers-ty\xF6kalulla")
37786
+ );
37787
+ }
37788
+ const { data: company, error: companyError } = await ctx.supabase.from("companies").select("alv_registered").eq("id", ctx.companyId).single();
37789
+ if (companyError || !company) return fail(invoiceDraftError("LOOKUP_FAILED"));
37790
+ const prepared = prepareInvoiceDraft(
37791
+ {
37792
+ invoiceDate: params.invoiceDate,
37793
+ paymentTermDays: params.paymentTermDays,
37794
+ priceEntryMode: params.priceEntryMode,
37795
+ notes: params.notes,
37796
+ reverseChargeConfirmed: params.reverseChargeConfirmed,
37797
+ euGoodsTransportConfirmed: params.euGoodsTransportConfirmed,
37798
+ // A rate defaults to 25.5 only when neither a rate nor a treatment is given.
37799
+ lines: params.lines.map(
37800
+ (line) => line.vatRate === void 0 && line.vatTreatmentCode === void 0 ? { ...line, vatRate: "25.5" } : line
37801
+ )
37802
+ },
37803
+ { sellerAlvRegistered: company.alv_registered, customer }
37804
+ );
37805
+ if (!prepared.ok) return fail(prepared.error);
37806
+ const { draft } = prepared;
37807
+ const { data, error: error61, status } = await ctx.supabase.rpc(
37808
+ "create_invoice_with_lines",
37809
+ // NULL number/reference = DB allocation; generated types do not mark them nullable.
37810
+ invoiceDraftRpcArgs(ctx.companyId, draft)
37811
+ );
37812
+ const outcome = invoiceDraftWriteOutcome({ data, error: error61 });
37813
+ if (isWriteOutcomeUnknown(error61, status) || !outcome.ok && outcome.error.code === "OUTCOME_UNKNOWN") {
37814
+ return toWriteOutcomeUnknown("kirjapro_create_invoice", "kirjapro_list_invoices");
37815
+ }
37816
+ if (!outcome.ok) {
37817
+ console.error("[kirjapro-mcp] create_invoice rejected:", { code: error61?.code });
37818
+ return fail(outcome.error);
37819
+ }
37820
+ const { data: invoice, error: fetchError } = await ctx.supabase.from("invoices").select(
37821
+ "id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at"
37822
+ ).eq("id", outcome.invoiceId).eq("company_id", ctx.companyId).single();
37823
+ if (fetchError || !invoice) {
37824
+ return toResult({
37825
+ message: "Lasku luotiin, mutta sen tietojen haku ep\xE4onnistui.",
37826
+ invoice: {
37827
+ id: outcome.invoiceId,
37828
+ status: "draft",
37829
+ invoice_date: draft.invoiceDate,
37830
+ due_date: draft.dueDate,
37831
+ subtotal: draft.subtotal,
37832
+ vat_total: draft.vatTotal,
37833
+ total: draft.total,
37834
+ customer_name: customer.name,
37835
+ lines: draft.lines
37836
+ },
37837
+ warning: "Tarkista lasku kirjapro_get_invoice-ty\xF6kalulla."
37838
+ });
37839
+ }
37840
+ return toResult({
37841
+ message: `Invoice ${invoice.invoice_number} created for ${customer.name}`,
37842
+ invoice: {
37843
+ ...invoice,
37844
+ customer_name: customer.name,
37845
+ lines: draft.lines
37846
+ }
37847
+ });
37848
+ }
37849
+ async function sendInvoiceCommand(ctx, params) {
37850
+ const { data: invoice, error: invoiceError } = await ctx.supabase.from("invoices").select("id, invoice_number, status").eq("id", params.invoiceId).eq("company_id", ctx.companyId).maybeSingle();
37851
+ if (invoiceError || !invoice) {
37852
+ return toErrorContent(`Invoice ${params.invoiceId} not found.`);
37853
+ }
37854
+ if (invoice.status !== "draft") {
37855
+ return toErrorContent(
37856
+ `Invoice ${invoice.invoice_number} cannot be delivered from status "${invoice.status}". Only draft invoices can be sent.`
37857
+ );
37858
+ }
37859
+ let httpStatus = null;
37860
+ let body = null;
37861
+ try {
37862
+ const { data, error: error61, response } = await ctx.supabase.functions.invoke("dispatch-invoice", {
37863
+ body: { invoiceId: invoice.id, channel: params.channel }
37864
+ });
37865
+ const failed = error61?.context;
37866
+ if (!error61) {
37867
+ httpStatus = response?.status ?? 200;
37868
+ body = data;
37869
+ } else if (failed instanceof Response) {
37870
+ httpStatus = failed.status;
37871
+ body = await failed.json().catch(() => null);
37872
+ }
37873
+ } catch {
37874
+ }
37875
+ const outcome = classifyInvoiceDelivery({ httpStatus, body });
37876
+ if (outcome.status === "unknown") {
37877
+ return toWriteOutcomeUnknown("kirjapro_send_invoice", "kirjapro_get_invoice");
37878
+ }
37879
+ if (outcome.status === "failed") {
37880
+ const detail = body && typeof body === "object" && typeof body.error === "string" ? body.error : invoiceDeliveryMessage(outcome.errorCode);
37881
+ return toErrorContent(`${outcome.errorCode}: ${detail}`);
37882
+ }
37883
+ return toResult({
37884
+ message: `Invoice ${invoice.invoice_number} delivered via ${params.channel}`,
37885
+ invoice_id: invoice.id,
37886
+ invoice_number: invoice.invoice_number,
37887
+ channel: params.channel,
37888
+ delivery_id: outcome.deliveryId,
37889
+ transmission_id: outcome.transmissionId,
37890
+ message_id: outcome.messageId,
37891
+ status: "sent"
37892
+ });
37893
+ }
37522
37894
  function registerInvoiceTools(server2) {
37523
37895
  server2.registerTool(
37524
37896
  "kirjapro_list_invoices",
@@ -37570,196 +37942,18 @@ function registerInvoiceTools(server2) {
37570
37942
  "kirjapro_create_invoice",
37571
37943
  {
37572
37944
  description: "Create a new sales invoice with line items. Automatically generates invoice number, reference number (viitenumero), and calculates VAT. Returns the complete invoice.",
37573
- inputSchema: {
37574
- customerId: external_exports.string().uuid().describe("Customer UUID"),
37575
- invoiceDate: ISO_DATE_SCHEMA.describe("Invoice date (YYYY-MM-DD)"),
37576
- paymentTermDays: external_exports.number().min(0).max(365).default(14).describe("Payment term in days (default 14)"),
37577
- notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
37578
- priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
37579
- reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
37580
- euGoodsTransportConfirmed: external_exports.boolean().default(false).describe("Explicit confirmation that intra-EU goods are transported from Finland to another EU country"),
37581
- lines: external_exports.array(
37582
- external_exports.object({
37583
- description: external_exports.string().min(1).max(500).describe("Line item description"),
37584
- // DB enforces CHECK (quantity > 0); reject 0/negative/Infinity at
37585
- // the boundary so the agent gets a clear error instead of an
37586
- // opaque 23514 deep in create_invoice_with_lines.
37587
- quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
37588
- unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
37589
- unitPrice: external_exports.number().finite().min(0).max(1e9).describe("Entered unit price in EUR (must be >= 0)"),
37590
- vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).default("25.5").describe("VAT rate"),
37591
- vatTreatmentCode: external_exports.enum([
37592
- "standard_25_5",
37593
- "standard_13_5",
37594
- "standard_10",
37595
- "not_vat_registered",
37596
- "reverse_construction",
37597
- "reverse_scrap",
37598
- "reverse_eu_service",
37599
- "export_outside_eu",
37600
- "service_outside_eu",
37601
- "intra_eu_goods",
37602
- "exempt_healthcare",
37603
- "exempt_social_welfare",
37604
- "exempt_education",
37605
- "exempt_financial",
37606
- "exempt_insurance",
37607
- "exempt_other"
37608
- ]).optional().describe("Structured VAT treatment. Required when VAT rate is 0. service_outside_eu is ONLY for AVL 65 general-rule B2B services: verify business use, evidence of buyer status and receiving establishment outside the EU; special place-of-supply services are excluded. Check foreign buyer liability and invoice notes. PDF delivery only; e-invoice delivery is blocked.")
37609
- })
37610
- ).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
37611
- idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37612
- },
37945
+ inputSchema: CREATE_INVOICE_INPUT_SHAPE,
37613
37946
  annotations: WRITE_ANNOTATIONS
37614
37947
  },
37615
37948
  async (params) => {
37616
- const supabase2 = getSupabase();
37617
- const companyId2 = getCompanyId();
37949
+ const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
37618
37950
  return withIdempotency(
37619
- supabase2,
37620
- companyId2,
37951
+ ctx.supabase,
37952
+ ctx.companyId,
37621
37953
  "kirjapro_create_invoice",
37622
37954
  params.idempotencyKey,
37623
37955
  params,
37624
- async () => {
37625
- const { data: customer, error: custError } = await supabase2.from("customers").select("id, name, customer_kind, business_id, vat_number, country").eq("id", params.customerId).eq("company_id", companyId2).single();
37626
- if (custError || !customer) {
37627
- return toErrorContent(
37628
- `Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`
37629
- );
37630
- }
37631
- const { data: company, error: companyError } = await supabase2.from("companies").select("alv_registered").eq("id", companyId2).single();
37632
- if (companyError || !company) {
37633
- return toErrorContent("Failed to verify the seller VAT registration status.");
37634
- }
37635
- const { data: invoiceNumber, error: numError } = await supabase2.rpc(
37636
- "get_next_invoice_number",
37637
- {
37638
- p_company_id: companyId2
37639
- }
37640
- );
37641
- if (numError || !invoiceNumber) {
37642
- return toErrorContent(
37643
- numError ? sanitizeDbError("Failed to generate invoice number", numError) : "Failed to generate invoice number."
37644
- );
37645
- }
37646
- const referenceNumber = generateReferenceNumber(
37647
- invoiceNumber.replace(/\D/g, "")
37648
- );
37649
- const dueDateStr = addDaysToIsoDate(
37650
- params.invoiceDate,
37651
- params.paymentTermDays
37652
- );
37653
- const treatmentRates = {
37654
- standard_25_5: 25.5,
37655
- standard_13_5: 13.5,
37656
- standard_10: 10,
37657
- not_vat_registered: 0,
37658
- reverse_construction: 0,
37659
- reverse_scrap: 0,
37660
- reverse_eu_service: 0,
37661
- export_outside_eu: 0,
37662
- service_outside_eu: 0,
37663
- intra_eu_goods: 0,
37664
- exempt_healthcare: 0,
37665
- exempt_social_welfare: 0,
37666
- exempt_education: 0,
37667
- exempt_financial: 0,
37668
- exempt_insurance: 0,
37669
- exempt_other: 0
37670
- };
37671
- const resolvedTreatments = params.lines.map((line) => {
37672
- const inputRate = parseFloat(line.vatRate);
37673
- const code = line.vatTreatmentCode ?? (inputRate === 25.5 ? "standard_25_5" : inputRate === 13.5 ? "standard_13_5" : inputRate === 10 ? "standard_10" : null);
37674
- if (!code) return { error: "0 % VAT requires a structured vatTreatmentCode." };
37675
- const eligibilityError = validateVatTreatmentEligibility({
37676
- code,
37677
- sellerAlvRegistered: company.alv_registered,
37678
- customer,
37679
- documentDate: params.invoiceDate
37680
- });
37681
- if (eligibilityError) return { error: eligibilityError };
37682
- return { code, rate: treatmentRates[code] ?? inputRate };
37683
- });
37684
- const treatmentError = resolvedTreatments.find((item) => "error" in item);
37685
- if (treatmentError && "error" in treatmentError) {
37686
- return toErrorContent(treatmentError.error);
37687
- }
37688
- const calculatedLines = params.lines.map((line, i) => {
37689
- const resolved = resolvedTreatments[i];
37690
- if (!resolved || !("code" in resolved)) throw new Error("VAT treatment validation failed");
37691
- const vatRate = resolved.rate;
37692
- const vatTreatmentCode = resolved.code;
37693
- const gross = roundCents(line.quantity * line.unitPrice);
37694
- const vatAmount = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross * vatRate / (100 + vatRate)) : roundCents(gross * (vatRate / 100));
37695
- const subtotal2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross - vatAmount) : gross;
37696
- const total3 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? gross : roundCents(subtotal2 + vatAmount);
37697
- return {
37698
- line_number: i + 1,
37699
- description: line.description,
37700
- quantity: line.quantity,
37701
- unit: line.unit,
37702
- unit_price: line.unitPrice,
37703
- entered_unit_price: line.unitPrice,
37704
- vat_rate: vatRate,
37705
- vat_treatment_code: vatTreatmentCode,
37706
- vat_treatment_overridden: true,
37707
- price_entry_mode: params.priceEntryMode,
37708
- default_vat_treatment_code: vatTreatmentCode,
37709
- reverse_charge_confirmed: params.reverseChargeConfirmed,
37710
- reverse_charge_confirmation_text: params.reverseChargeConfirmed ? "Buyer qualification explicitly confirmed through MCP invoice creation." : null,
37711
- eu_goods_transport_confirmed: params.euGoodsTransportConfirmed,
37712
- subtotal: subtotal2,
37713
- vat_amount: vatAmount,
37714
- total: total3
37715
- };
37716
- });
37717
- const subtotal = roundCents(
37718
- calculatedLines.reduce((s, l) => s + l.subtotal, 0)
37719
- );
37720
- const vatTotal = roundCents(
37721
- calculatedLines.reduce((s, l) => s + l.vat_amount, 0)
37722
- );
37723
- const total2 = roundCents(subtotal + vatTotal);
37724
- const { data: invoiceId, error: invError } = await supabase2.rpc(
37725
- "create_invoice_with_lines",
37726
- {
37727
- p_company_id: companyId2,
37728
- p_customer_id: params.customerId,
37729
- p_invoice_number: invoiceNumber,
37730
- p_reference_number: referenceNumber,
37731
- p_invoice_date: params.invoiceDate,
37732
- p_due_date: dueDateStr,
37733
- p_status: "draft",
37734
- p_subtotal: subtotal,
37735
- p_vat_total: vatTotal,
37736
- p_total: total2,
37737
- p_notes: params.notes || "",
37738
- p_lines: calculatedLines
37739
- }
37740
- );
37741
- if (invError || !invoiceId) {
37742
- return toErrorContent(
37743
- invError ? sanitizeDbError("Failed to create invoice", invError) : "Failed to create invoice."
37744
- );
37745
- }
37746
- const { data: invoice, error: fetchError } = await supabase2.from("invoices").select(
37747
- "id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at"
37748
- ).eq("id", invoiceId).eq("company_id", companyId2).single();
37749
- if (fetchError || !invoice) {
37750
- return toErrorContent(
37751
- fetchError ? sanitizeDbError("Invoice created but fetch failed", fetchError) : "Invoice created but could not be retrieved."
37752
- );
37753
- }
37754
- return toResult({
37755
- message: `Invoice ${invoiceNumber} created for ${customer.name}`,
37756
- invoice: {
37757
- ...invoice,
37758
- customer_name: customer.name,
37759
- lines: calculatedLines
37760
- }
37761
- });
37762
- }
37956
+ () => createInvoiceDraftCommand(ctx, params)
37763
37957
  );
37764
37958
  }
37765
37959
  );
@@ -37772,45 +37966,18 @@ function registerInvoiceTools(server2) {
37772
37966
  channel: external_exports.enum(["email", "einvoice"]).describe("Delivery channel: email or einvoice"),
37773
37967
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
37774
37968
  },
37775
- annotations: WRITE_ANNOTATIONS
37969
+ // Delivers through email or Maventa: an external effect (openWorldHint).
37970
+ annotations: WRITE_EXTERNAL_ANNOTATIONS
37776
37971
  },
37777
37972
  async (params) => {
37778
- const supabase2 = getSupabase();
37779
- const companyId2 = getCompanyId();
37973
+ const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
37780
37974
  return withIdempotency(
37781
- supabase2,
37782
- companyId2,
37975
+ ctx.supabase,
37976
+ ctx.companyId,
37783
37977
  "kirjapro_send_invoice",
37784
37978
  params.idempotencyKey,
37785
37979
  params,
37786
- async () => {
37787
- const { data: invoice, error: invoiceError } = await supabase2.from("invoices").select("id, invoice_number, status").eq("id", params.invoiceId).eq("company_id", companyId2).maybeSingle();
37788
- if (invoiceError || !invoice) {
37789
- return toErrorContent(`Invoice ${params.invoiceId} not found.`);
37790
- }
37791
- if (invoice.status !== "draft") {
37792
- return toErrorContent(
37793
- `Invoice ${invoice.invoice_number} cannot be delivered from status "${invoice.status}". Only draft invoices can be sent.`
37794
- );
37795
- }
37796
- const { data, error: error61 } = await supabase2.functions.invoke("dispatch-invoice", {
37797
- body: { invoiceId: invoice.id, channel: params.channel }
37798
- });
37799
- if (error61 || !data?.success) {
37800
- return toErrorContent(
37801
- data?.error || error61?.message || `Failed to deliver invoice ${invoice.invoice_number}.`
37802
- );
37803
- }
37804
- return toResult({
37805
- message: `Invoice ${invoice.invoice_number} delivered via ${params.channel}`,
37806
- invoice_id: invoice.id,
37807
- invoice_number: invoice.invoice_number,
37808
- channel: data.channel,
37809
- transmission_id: data.transmissionId ?? null,
37810
- message_id: data.messageId ?? null,
37811
- status: "sent"
37812
- });
37813
- }
37980
+ () => sendInvoiceCommand(ctx, params)
37814
37981
  );
37815
37982
  }
37816
37983
  );
@@ -38132,6 +38299,43 @@ function registerInvoiceTools(server2) {
38132
38299
  );
38133
38300
  }
38134
38301
 
38302
+ // src/finnish-business-id.ts
38303
+ function validateFinnishBusinessIdCore(input2) {
38304
+ const cleaned = input2.replace(/\s/g, "").toUpperCase();
38305
+ const match = cleaned.match(/^(\d{7})-(\d)$/);
38306
+ if (!match) return { valid: false, reason: "format" };
38307
+ const digits = match[1];
38308
+ const actualCheckDigit = Number(match[2]);
38309
+ if (!digits || !Number.isInteger(actualCheckDigit)) {
38310
+ return { valid: false, reason: "format" };
38311
+ }
38312
+ const weights = [7, 9, 10, 5, 8, 4, 2];
38313
+ let sum = 0;
38314
+ for (let index = 0; index < weights.length; index += 1) {
38315
+ const digit = digits[index];
38316
+ const weight = weights[index];
38317
+ if (digit === void 0 || weight === void 0) {
38318
+ return { valid: false, reason: "format" };
38319
+ }
38320
+ sum += Number(digit) * weight;
38321
+ }
38322
+ const remainder = sum % 11;
38323
+ if (remainder === 1) return { valid: false, reason: "remainder_one" };
38324
+ const expectedCheckDigit = remainder === 0 ? 0 : 11 - remainder;
38325
+ if (actualCheckDigit !== expectedCheckDigit) {
38326
+ return {
38327
+ valid: false,
38328
+ reason: "checksum",
38329
+ expectedCheckDigit,
38330
+ actualCheckDigit
38331
+ };
38332
+ }
38333
+ return { valid: true };
38334
+ }
38335
+ function isValidFinnishBusinessId(input2) {
38336
+ return validateFinnishBusinessIdCore(input2).valid;
38337
+ }
38338
+
38135
38339
  // src/tools/customers.ts
38136
38340
  var CUSTOMER_STATS_RPC_SCHEMA = external_exports.object({
38137
38341
  invoice_count: external_exports.number().int().nonnegative(),
@@ -38169,9 +38373,9 @@ var PRH_LOOKUP_OUTPUT_SHAPE = {
38169
38373
  website: external_exports.string().nullable(),
38170
38374
  phone: external_exports.string().nullable()
38171
38375
  };
38172
- function sanitizeText(text) {
38173
- if (!text) return null;
38174
- return text.trim().replace(/\s+/g, " ").slice(0, 500);
38376
+ function sanitizeText(text2) {
38377
+ if (!text2) return null;
38378
+ return text2.trim().replace(/\s+/g, " ").slice(0, 500);
38175
38379
  }
38176
38380
  function registerCustomerTools(server2) {
38177
38381
  server2.registerTool(
@@ -38441,6 +38645,62 @@ function registerCustomerTools(server2) {
38441
38645
  );
38442
38646
  }
38443
38647
 
38648
+ // src/shared/ar-aging-report.ts
38649
+ var AR_AGING_TOOL = {
38650
+ name: "kirjapro_get_ar_aging",
38651
+ description: "Get an exact, bounded accounts-receivable aging summary as of today in Europe/Helsinki. Outstanding amounts account for partial payments and sent credit notes. Returns aggregate 0-30, 31-60, 61-90, 90+ and missing-due-date buckets; it intentionally does not return an unbounded invoice list.",
38652
+ // Equal to the MCP package's READ_ONLY_ANNOTATIONS (tool fingerprint).
38653
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
38654
+ };
38655
+ var NUMERIC_FIELDS = [
38656
+ "bucket_0_30_count",
38657
+ "bucket_0_30_amount",
38658
+ "bucket_31_60_count",
38659
+ "bucket_31_60_amount",
38660
+ "bucket_61_90_count",
38661
+ "bucket_61_90_amount",
38662
+ "bucket_90_plus_count",
38663
+ "bucket_90_plus_amount",
38664
+ "no_due_date_count",
38665
+ "no_due_date_amount",
38666
+ "total_count",
38667
+ "total_receivables",
38668
+ "weighted_average_days_overdue"
38669
+ ];
38670
+ var cents = (amount2) => Math.round(amount2 * 100) / 100;
38671
+ var eur = (amount2) => new Intl.NumberFormat("fi-FI", { style: "currency", currency: "EUR" }).format(amount2);
38672
+ var amount = (count, total2) => ({
38673
+ count,
38674
+ total: cents(total2),
38675
+ total_formatted: eur(total2)
38676
+ });
38677
+ function isRawArAging(data) {
38678
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
38679
+ const row = data;
38680
+ return typeof row.as_of_date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(row.as_of_date) && NUMERIC_FIELDS.every((field) => {
38681
+ const value = row[field];
38682
+ return typeof value === "number" && Number.isFinite(value);
38683
+ });
38684
+ }
38685
+ function toArAgingReport(data) {
38686
+ if (!isRawArAging(data)) return null;
38687
+ return {
38688
+ as_of_date: data.as_of_date,
38689
+ buckets: [
38690
+ { label: "0-30 days", ...amount(data.bucket_0_30_count, data.bucket_0_30_amount) },
38691
+ { label: "31-60 days", ...amount(data.bucket_31_60_count, data.bucket_31_60_amount) },
38692
+ { label: "61-90 days", ...amount(data.bucket_61_90_count, data.bucket_61_90_amount) },
38693
+ { label: "90+ days", ...amount(data.bucket_90_plus_count, data.bucket_90_plus_amount) }
38694
+ ],
38695
+ no_due_date: amount(data.no_due_date_count, data.no_due_date_amount),
38696
+ total_count: data.total_count,
38697
+ total_receivables: cents(data.total_receivables),
38698
+ total_receivables_formatted: eur(data.total_receivables),
38699
+ weighted_average_days_overdue: data.weighted_average_days_overdue,
38700
+ metric_note: "weighted_average_days_overdue is the amount-weighted age of currently outstanding receivables with a due date; it is not revenue-based DSO."
38701
+ };
38702
+ }
38703
+
38444
38704
  // src/compliance/omavero-fields.ts
38445
38705
  var OMAVERO_FIELD_NUMBERS = [
38446
38706
  "301",
@@ -38481,7 +38741,7 @@ var LEGACY_ALV_SUMMARY_KEYS = [
38481
38741
  "reverse_charge_base",
38482
38742
  "omavero"
38483
38743
  ];
38484
- function cents(value) {
38744
+ function cents2(value) {
38485
38745
  return Math.round(value * 100) / 100;
38486
38746
  }
38487
38747
  function num(value) {
@@ -38506,7 +38766,7 @@ function omaveroFieldsFromSummary(s) {
38506
38766
  "314": num(s.field_309_base),
38507
38767
  "318": num(s.output_vat_construction_scrap),
38508
38768
  "319": num(s.reverse_charge_sales_base),
38509
- "320": cents(num(s.field_310_base) + num(s.field_313_base))
38769
+ "320": cents2(num(s.field_310_base) + num(s.field_313_base))
38510
38770
  };
38511
38771
  const map2 = s.omavero;
38512
38772
  if (map2 && typeof map2 === "object") {
@@ -38698,8 +38958,8 @@ function alvReconciliationWarning(reconciliation) {
38698
38958
  if (!reconciliation || typeof reconciliation !== "object") return void 0;
38699
38959
  const r = reconciliation;
38700
38960
  if (r.status !== "mismatch" && r.status !== "explained") return void 0;
38701
- const amount = (value) => (Number(value) || 0).toFixed(2);
38702
- return `VAT amounts or the audit trail need review (output VAT difference ${amount(r.output_difference)} \u20AC, input VAT difference ${amount(r.input_difference)} \u20AC). Equal amounts do not link a manual VAT row to a voucher. Review reconciliation before filing; do not duplicate the purchase expense.`;
38961
+ const amount2 = (value) => (Number(value) || 0).toFixed(2);
38962
+ return `VAT amounts or the audit trail need review (output VAT difference ${amount2(r.output_difference)} \u20AC, input VAT difference ${amount2(r.input_difference)} \u20AC). Equal amounts do not link a manual VAT row to a voucher. Review reconciliation before filing; do not duplicate the purchase expense.`;
38703
38963
  }
38704
38964
  var BALANCE_SHEET_ROW_SHAPE = external_exports.object({
38705
38965
  account_number: external_exports.string(),
@@ -39051,12 +39311,12 @@ function registerReportTools(server2) {
39051
39311
  }
39052
39312
  );
39053
39313
  server2.registerTool(
39054
- "kirjapro_get_ar_aging",
39314
+ AR_AGING_TOOL.name,
39055
39315
  {
39056
- description: "Get an exact, bounded accounts-receivable aging summary as of today in Europe/Helsinki. Outstanding amounts account for partial payments and sent credit notes. Returns aggregate 0-30, 31-60, 61-90, 90+ and missing-due-date buckets; it intentionally does not return an unbounded invoice list.",
39316
+ description: AR_AGING_TOOL.description,
39057
39317
  inputSchema: {},
39058
39318
  outputSchema: AR_AGING_OUTPUT_SHAPE,
39059
- annotations: READ_ONLY_ANNOTATIONS
39319
+ annotations: AR_AGING_TOOL.annotations
39060
39320
  },
39061
39321
  async () => {
39062
39322
  const supabase2 = getSupabase();
@@ -39067,32 +39327,8 @@ function registerReportTools(server2) {
39067
39327
  if (error61) {
39068
39328
  return toErrorContent(sanitizeDbError("Failed to get AR aging", error61));
39069
39329
  }
39070
- const aging = data;
39071
- const buckets = [
39072
- { label: "0-30 days", count: aging.bucket_0_30_count, total: aging.bucket_0_30_amount },
39073
- { label: "31-60 days", count: aging.bucket_31_60_count, total: aging.bucket_31_60_amount },
39074
- { label: "61-90 days", count: aging.bucket_61_90_count, total: aging.bucket_61_90_amount },
39075
- { label: "90+ days", count: aging.bucket_90_plus_count, total: aging.bucket_90_plus_amount }
39076
- ];
39077
- return toResult({
39078
- as_of_date: aging.as_of_date,
39079
- buckets: buckets.map((b) => ({
39080
- label: b.label,
39081
- count: b.count,
39082
- total: roundCents(b.total),
39083
- total_formatted: formatCurrency(b.total)
39084
- })),
39085
- no_due_date: {
39086
- count: aging.no_due_date_count,
39087
- total: roundCents(aging.no_due_date_amount),
39088
- total_formatted: formatCurrency(aging.no_due_date_amount)
39089
- },
39090
- total_count: aging.total_count,
39091
- total_receivables: roundCents(aging.total_receivables),
39092
- total_receivables_formatted: formatCurrency(aging.total_receivables),
39093
- weighted_average_days_overdue: aging.weighted_average_days_overdue,
39094
- metric_note: "weighted_average_days_overdue is the amount-weighted age of currently outstanding receivables with a due date; it is not revenue-based DSO."
39095
- });
39330
+ const report = toArAgingReport(data);
39331
+ return report ? toResult(report) : toErrorContent("Failed to get AR aging. Please try again.");
39096
39332
  }
39097
39333
  );
39098
39334
  server2.registerTool(
@@ -39224,7 +39460,7 @@ function registerReportTools(server2) {
39224
39460
  const warnings = [
39225
39461
  alvReconciliationWarning(raw.reconciliation),
39226
39462
  Number(raw.unclassified_legacy_reverse_charge_base) ? LEGACY_REVERSE_CHARGE_WARNING : void 0
39227
- ].filter((text) => Boolean(text));
39463
+ ].filter((text2) => Boolean(text2));
39228
39464
  return toResult({
39229
39465
  ...safeResult,
39230
39466
  ...warnings.length > 0 && { warning: warnings.join(" ") },
@@ -39695,7 +39931,7 @@ function validateFinnishIBAN(input2) {
39695
39931
  }
39696
39932
  return { valid: true };
39697
39933
  }
39698
- function generateReferenceNumber2(baseNumber) {
39934
+ function generateReferenceNumber(baseNumber) {
39699
39935
  const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
39700
39936
  if (digits.length < 3 || digits.length > 19) {
39701
39937
  return { reference: "", error: "Base number must be 3-19 digits without leading zeros." };
@@ -39756,7 +39992,7 @@ function registerValidationTools(server2) {
39756
39992
  annotations: READ_ONLY_ANNOTATIONS
39757
39993
  },
39758
39994
  async (params) => {
39759
- const result = generateReferenceNumber2(params.baseNumber);
39995
+ const result = generateReferenceNumber(params.baseNumber);
39760
39996
  return toResult({
39761
39997
  input: params.baseNumber,
39762
39998
  ...result
@@ -40311,12 +40547,12 @@ async function validateAccountOverrides(overrides) {
40311
40547
  return { ok: true };
40312
40548
  }
40313
40549
  function saleCashTemplate(revenueAccount, vatRate) {
40314
- return (amount, opts) => {
40315
- const { net, vat: vat2 } = splitVat(amount, vatRate);
40550
+ return (amount2, opts) => {
40551
+ const { net, vat: vat2 } = splitVat(amount2, vatRate);
40316
40552
  const entries = [
40317
40553
  {
40318
40554
  accountNumber: ACCOUNTS.KASSA,
40319
- debitAmount: roundCents(amount),
40555
+ debitAmount: roundCents(amount2),
40320
40556
  creditAmount: 0
40321
40557
  },
40322
40558
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
@@ -40332,12 +40568,12 @@ function saleCashTemplate(revenueAccount, vatRate) {
40332
40568
  };
40333
40569
  }
40334
40570
  function saleBankTemplate(revenueAccount, vatRate) {
40335
- return (amount, opts) => {
40336
- const { net, vat: vat2 } = splitVat(amount, vatRate);
40571
+ return (amount2, opts) => {
40572
+ const { net, vat: vat2 } = splitVat(amount2, vatRate);
40337
40573
  const entries = [
40338
40574
  {
40339
40575
  accountNumber: ACCOUNTS.PANKKITILI,
40340
- debitAmount: roundCents(amount),
40576
+ debitAmount: roundCents(amount2),
40341
40577
  creditAmount: 0
40342
40578
  },
40343
40579
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
@@ -40410,13 +40646,13 @@ var TEMPLATES = [
40410
40646
  kind: "sale",
40411
40647
  description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
40412
40648
  vatRate: 25.5,
40413
- expand: (amount, opts) => {
40414
- const { net, vat: vat2 } = splitVat(amount, 25.5);
40649
+ expand: (amount2, opts) => {
40650
+ const { net, vat: vat2 } = splitVat(amount2, 25.5);
40415
40651
  return applyOverrides(
40416
40652
  [
40417
40653
  {
40418
40654
  accountNumber: ACCOUNTS.MYYNTISAAMISET,
40419
- debitAmount: roundCents(amount),
40655
+ debitAmount: roundCents(amount2),
40420
40656
  creditAmount: 0
40421
40657
  },
40422
40658
  { accountNumber: ACCOUNTS.MYYNTI_25_5, debitAmount: 0, creditAmount: net },
@@ -40432,8 +40668,8 @@ var TEMPLATES = [
40432
40668
  kind: "purchase",
40433
40669
  description: "Osto ALV 25,5% (pankkitililt\xE4)",
40434
40670
  vatRate: 25.5,
40435
- expand: (amount, opts) => {
40436
- const { net, vat: vat2 } = splitVat(amount, 25.5);
40671
+ expand: (amount2, opts) => {
40672
+ const { net, vat: vat2 } = splitVat(amount2, 25.5);
40437
40673
  return applyOverrides(
40438
40674
  [
40439
40675
  { accountNumber: ACCOUNTS.OSTOT, debitAmount: net, creditAmount: 0 },
@@ -40441,7 +40677,7 @@ var TEMPLATES = [
40441
40677
  {
40442
40678
  accountNumber: ACCOUNTS.PANKKITILI,
40443
40679
  debitAmount: 0,
40444
- creditAmount: roundCents(amount)
40680
+ creditAmount: roundCents(amount2)
40445
40681
  }
40446
40682
  ],
40447
40683
  opts?.accountOverrides
@@ -40453,8 +40689,8 @@ var TEMPLATES = [
40453
40689
  kind: "purchase",
40454
40690
  description: "Ulkopuolinen palvelu ALV 25,5%",
40455
40691
  vatRate: 25.5,
40456
- expand: (amount, opts) => {
40457
- const { net, vat: vat2 } = splitVat(amount, 25.5);
40692
+ expand: (amount2, opts) => {
40693
+ const { net, vat: vat2 } = splitVat(amount2, 25.5);
40458
40694
  return applyOverrides(
40459
40695
  [
40460
40696
  { accountNumber: ACCOUNTS.OSTOT_PALVELUT, debitAmount: net, creditAmount: 0 },
@@ -40462,7 +40698,7 @@ var TEMPLATES = [
40462
40698
  {
40463
40699
  accountNumber: ACCOUNTS.PANKKITILI,
40464
40700
  debitAmount: 0,
40465
- creditAmount: roundCents(amount)
40701
+ creditAmount: roundCents(amount2)
40466
40702
  }
40467
40703
  ],
40468
40704
  opts?.accountOverrides
@@ -40475,17 +40711,17 @@ var TEMPLATES = [
40475
40711
  kind: "other",
40476
40712
  description: "Vuokranmaksu (pankkitililt\xE4)",
40477
40713
  vatRate: null,
40478
- expand: (amount, opts) => applyOverrides(
40714
+ expand: (amount2, opts) => applyOverrides(
40479
40715
  [
40480
40716
  {
40481
40717
  accountNumber: ACCOUNTS.TOIMITILAVUOKRAT,
40482
- debitAmount: roundCents(amount),
40718
+ debitAmount: roundCents(amount2),
40483
40719
  creditAmount: 0
40484
40720
  },
40485
40721
  {
40486
40722
  accountNumber: ACCOUNTS.PANKKITILI,
40487
40723
  debitAmount: 0,
40488
- creditAmount: roundCents(amount)
40724
+ creditAmount: roundCents(amount2)
40489
40725
  }
40490
40726
  ],
40491
40727
  opts?.accountOverrides
@@ -40497,17 +40733,17 @@ var TEMPLATES = [
40497
40733
  kind: "other",
40498
40734
  description: "Tilisiirto kassasta pankkitilille",
40499
40735
  vatRate: null,
40500
- expand: (amount, opts) => applyOverrides(
40736
+ expand: (amount2, opts) => applyOverrides(
40501
40737
  [
40502
40738
  {
40503
40739
  accountNumber: ACCOUNTS.PANKKITILI,
40504
- debitAmount: roundCents(amount),
40740
+ debitAmount: roundCents(amount2),
40505
40741
  creditAmount: 0
40506
40742
  },
40507
40743
  {
40508
40744
  accountNumber: ACCOUNTS.KASSA,
40509
40745
  debitAmount: 0,
40510
- creditAmount: roundCents(amount)
40746
+ creditAmount: roundCents(amount2)
40511
40747
  }
40512
40748
  ],
40513
40749
  opts?.accountOverrides
@@ -40530,17 +40766,17 @@ function isVatPurchaseTemplate(templateId) {
40530
40766
  function availableTemplates(alvRegistered) {
40531
40767
  return alvRegistered ? TEMPLATES.filter((t) => t.kind !== "sale") : TEMPLATES.filter((t) => !isVatPurchaseTemplate(t.id));
40532
40768
  }
40533
- function expandTemplate(templateId, amount, opts) {
40769
+ function expandTemplate(templateId, amount2, opts) {
40534
40770
  const template = TEMPLATE_MAP.get(templateId);
40535
40771
  if (!template) {
40536
40772
  return {
40537
40773
  error: `Unknown template "${templateId}". Use kirjapro_list_templates to see available templates.`
40538
40774
  };
40539
40775
  }
40540
- if (amount <= 0) {
40776
+ if (amount2 <= 0) {
40541
40777
  return { error: "Amount must be positive." };
40542
40778
  }
40543
- const entries = template.expand(amount, opts);
40779
+ const entries = template.expand(amount2, opts);
40544
40780
  return { entries };
40545
40781
  }
40546
40782
  async function validateFiscalYearForDate(fiscalYearId, transactionDate) {
@@ -41189,8 +41425,8 @@ function registerBookingTools(server2) {
41189
41425
  async (params) => {
41190
41426
  const execute = async () => {
41191
41427
  const rate = Number(params.vatRate);
41192
- const amount = roundCents(params.amount);
41193
- if (amount <= 0) return toErrorContent("Oston summan on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41428
+ const amount2 = roundCents(params.amount);
41429
+ if (amount2 <= 0) return toErrorContent("Oston summan on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41194
41430
  const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
41195
41431
  if ("isError" in companyVat) return companyVat;
41196
41432
  if (params.reverseCharge && !params.reverseChargeCategory) return toErrorContent(REVERSE_CHARGE_CATEGORY_REQUIRED);
@@ -41210,7 +41446,7 @@ function registerBookingTools(server2) {
41210
41446
  if (basisError) return toErrorContent(basisError);
41211
41447
  }
41212
41448
  const basis = params.vatRateBasis;
41213
- const { net, vat: vat2 } = params.reverseCharge ? { net: amount, vat: roundCents(amount * rate / 100) } : splitVat(amount, rate);
41449
+ const { net, vat: vat2 } = params.reverseCharge ? { net: amount2, vat: roundCents(amount2 * rate / 100) } : splitVat(amount2, rate);
41214
41450
  const allocation = allocatePurchaseVat(net, vat2, percent);
41215
41451
  const keepFacts = rate > 0 && (params.reverseCharge || companyVat.alvRegistered && !params.edustus);
41216
41452
  const vatAllocation = percent > 0 && percent < 100 ? {
@@ -41236,13 +41472,13 @@ function registerBookingTools(server2) {
41236
41472
  { accountNumber: params.expenseAccount, debitAmount: expense, creditAmount: 0 },
41237
41473
  { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: allocation.deductibleVat, creditAmount: 0 },
41238
41474
  ...params.reverseCharge ? [{ accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }] : [],
41239
- ...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net : amount }] : []
41475
+ ...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net : amount2 }] : []
41240
41476
  ].filter((entry) => entry.debitAmount !== 0 || entry.creditAmount !== 0);
41241
41477
  if (entries.length < 2) return toErrorContent("Tositteen veron on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41242
41478
  const purchaseAccountWarning = expenseAccountWarning(params.expenseAccount);
41243
41479
  const calculation = {
41244
41480
  amount_basis: assessmentOnly ? "customs_tax_base" : params.reverseCharge ? "supplier_net" : "supplier_gross",
41245
- ...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net : amount },
41481
+ ...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net : amount2 },
41246
41482
  tax_base: net,
41247
41483
  output_vat: params.reverseCharge ? vat2 : 0,
41248
41484
  source_vat: vat2,
@@ -41253,7 +41489,7 @@ function registerBookingTools(server2) {
41253
41489
  };
41254
41490
  if (params.dryRun) return toDryRunResponse("kirjapro_book_purchase", {
41255
41491
  action: "book_purchase",
41256
- gross: amount,
41492
+ gross: amount2,
41257
41493
  vat_rate: params.vatRate,
41258
41494
  description: params.description,
41259
41495
  date: params.date,
@@ -41501,6 +41737,81 @@ function registerBookingTools(server2) {
41501
41737
  );
41502
41738
  }
41503
41739
 
41740
+ // src/vat.ts
41741
+ var EU_COUNTRY_CODES2 = /* @__PURE__ */ new Set([
41742
+ "AT",
41743
+ "BE",
41744
+ "BG",
41745
+ "HR",
41746
+ "CY",
41747
+ "CZ",
41748
+ "DE",
41749
+ "DK",
41750
+ "EE",
41751
+ "ES",
41752
+ "FI",
41753
+ "FR",
41754
+ "GR",
41755
+ "HU",
41756
+ "IE",
41757
+ "IT",
41758
+ "LT",
41759
+ "LU",
41760
+ "LV",
41761
+ "MT",
41762
+ "NL",
41763
+ "PL",
41764
+ "PT",
41765
+ "RO",
41766
+ "SE",
41767
+ "SI",
41768
+ "SK"
41769
+ ]);
41770
+ function normalizedVatNumber(value) {
41771
+ return (value ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
41772
+ }
41773
+ function validateVatTreatmentEligibility({
41774
+ code,
41775
+ sellerAlvRegistered,
41776
+ customer,
41777
+ documentDate
41778
+ }) {
41779
+ if (!sellerAlvRegistered && code !== "not_vat_registered") {
41780
+ return "A seller outside the VAT register can only use not_vat_registered.";
41781
+ }
41782
+ if (sellerAlvRegistered && code === "not_vat_registered") {
41783
+ return "A VAT-registered seller cannot use not_vat_registered.";
41784
+ }
41785
+ if (code === "standard_25_5" && documentDate < "2024-09-01") {
41786
+ return "standard_25_5 is not valid before 2024-09-01.";
41787
+ }
41788
+ if (code === "standard_13_5" && documentDate < "2026-01-01") {
41789
+ return "standard_13_5 is not valid before 2026-01-01.";
41790
+ }
41791
+ const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu";
41792
+ if (requiresBusiness && customer?.customer_kind !== "business") {
41793
+ return "The selected VAT treatment requires a business customer.";
41794
+ }
41795
+ if ((code === "reverse_construction" || code === "reverse_scrap") && !isValidFinnishBusinessId(customer?.business_id ?? "")) {
41796
+ return "Domestic reverse charge requires a Finnish buyer business ID in the form 1234567-8.";
41797
+ }
41798
+ if (code === "reverse_eu_service" || code === "intra_eu_goods") {
41799
+ const country = (customer?.country ?? "").trim().toUpperCase();
41800
+ const vatNumber = normalizedVatNumber(customer?.vat_number);
41801
+ const expectedPrefix = country === "GR" ? "EL" : country;
41802
+ if (!EU_COUNTRY_CODES2.has(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix)) {
41803
+ return "EU sale requires a non-Finnish EU country and a matching buyer VAT number.";
41804
+ }
41805
+ }
41806
+ if (code === "export_outside_eu" || code === "service_outside_eu") {
41807
+ const country = (customer?.country ?? "").trim().toUpperCase();
41808
+ if (!country || EU_COUNTRY_CODES2.has(country)) {
41809
+ return "Export outside the EU requires a buyer country outside the EU.";
41810
+ }
41811
+ }
41812
+ return null;
41813
+ }
41814
+
41504
41815
  // src/tools/offers.ts
41505
41816
  var OFFER_DETAIL_OUTPUT_SHAPE = {
41506
41817
  id: external_exports.string(),
@@ -42962,7 +43273,7 @@ var VAT_COMPLIANCE = {
42962
43273
  ],
42963
43274
  booking: "D expense/asset for net amount plus non-deductible self-assessed VAT; D ALV-saamiset only for deductible VAT; C bank/payable for supplier amount; C ALV-velka for full self-assessed VAT. Use the canonical VAT purchase command, not unlinked VAT account rows.",
42964
43275
  note: "Net VAT effect is zero only with a full deduction. Report the full assessed VAT and only the deductible share. The seller does not charge VAT on a reverse-charge invoice; this does not remove the buyer\u2019s VAT liability.",
42965
- implementation_limits: "The MCP purchase shortcut currently has no deduction-share parameter and does not support partial-deduction reverse charge. Do not use its default full deduction for that case: this example describes the accounting rule, not an added tool capability.",
43276
+ implementation_limits: "kirjapro_book_purchase (full surface) takes deductionPercent 0\u2013100 with a documented deductionReason, including for reverse charge: the full assessed VAT is payable (ALV-velka) and only the deductible share reaches ALV-saamiset and field 307. The rest is expense. Entertainment and non-registered buyers use 0.",
42966
43277
  example: { net_purchase: 100, rate: FINNISH_RATES_2026.vat.standard, deductible_share: "60%", output_vat: exampleOutputVat, deductible_vat: exampleDeductibleVat, non_deductible_vat: exampleNonDeductibleVat, expense: roundCents(100 + exampleNonDeductibleVat), payable_vat: exampleNonDeductibleVat },
42967
43278
  omavero_fields: "EU goods: 305 (VAT) / 313 (base); EU services: 306 / 314; services from outside the EU: VAT in 301-303 by rate (no base field); imports: 304 / 310; domestic construction/scrap purchases: 318 / 320, sales 319. The deductible part of each is in 307."
42968
43279
  },
@@ -43615,7 +43926,7 @@ function registerPrompts(mcpServer) {
43615
43926
  } else {
43616
43927
  fyBlock = "Ei avointa tilikautta \u2014 luo tilikausi ennen kirjausten tekemist\xE4.";
43617
43928
  }
43618
- const text = [
43929
+ const text2 = [
43619
43930
  `T\xE4m\xE4 on Kirjapro MCP -palvelin (v${VERSION}). Yhdistetty yritys:`,
43620
43931
  "",
43621
43932
  companyBlock,
@@ -43633,7 +43944,7 @@ function registerPrompts(mcpServer) {
43633
43944
  messages: [
43634
43945
  {
43635
43946
  role: "user",
43636
- content: { type: "text", text }
43947
+ content: { type: "text", text: text2 }
43637
43948
  }
43638
43949
  ]
43639
43950
  };