@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/CHANGELOG.md +59 -1
- package/dist/index.js +1305 -596
- package/package.json +1 -1
- package/server.json +1 -1
- package/tool-fingerprints.json +6 -6
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.
|
|
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/
|
|
37311
|
-
|
|
37312
|
-
|
|
37313
|
-
|
|
37314
|
-
|
|
37315
|
-
|
|
37316
|
-
|
|
37317
|
-
|
|
37318
|
-
|
|
37319
|
-
|
|
37320
|
-
|
|
37321
|
-
|
|
37322
|
-
|
|
37323
|
-
|
|
37324
|
-
|
|
37325
|
-
|
|
37326
|
-
|
|
37327
|
-
|
|
37328
|
-
|
|
37329
|
-
|
|
37330
|
-
|
|
37331
|
-
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
37335
|
-
|
|
37336
|
-
|
|
37337
|
-
|
|
37338
|
-
|
|
37339
|
-
|
|
37340
|
-
|
|
37341
|
-
|
|
37342
|
-
|
|
37343
|
-
|
|
37344
|
-
|
|
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
|
-
|
|
37348
|
-
|
|
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
|
|
37378
|
-
|
|
37379
|
-
|
|
37380
|
-
|
|
37381
|
-
|
|
37382
|
-
|
|
37383
|
-
|
|
37384
|
-
|
|
37385
|
-
|
|
37386
|
-
if (
|
|
37387
|
-
|
|
37388
|
-
|
|
37389
|
-
|
|
37390
|
-
|
|
37391
|
-
|
|
37392
|
-
if (
|
|
37393
|
-
return "
|
|
37394
|
-
|
|
37395
|
-
|
|
37396
|
-
|
|
37397
|
-
|
|
37398
|
-
|
|
37399
|
-
|
|
37400
|
-
|
|
37401
|
-
|
|
37402
|
-
|
|
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
|
|
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.
|
|
37410
|
-
return "
|
|
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
|
-
|
|
37415
|
-
|
|
37416
|
-
|
|
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
|
|
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
|
-
|
|
37623
|
+
total2 += credit.total;
|
|
37445
37624
|
}
|
|
37446
37625
|
offset += data.length;
|
|
37447
|
-
if (offset === count) return { total: roundCents(
|
|
37626
|
+
if (offset === count) return { total: roundCents(total2), error: null };
|
|
37448
37627
|
}
|
|
37449
37628
|
}
|
|
37450
|
-
function calculateInvoiceBalance(
|
|
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(
|
|
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
|
|
37616
|
-
const companyId2 = getCompanyId();
|
|
37833
|
+
const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
|
|
37617
37834
|
return withIdempotency(
|
|
37618
|
-
|
|
37619
|
-
|
|
37835
|
+
ctx.supabase,
|
|
37836
|
+
ctx.companyId,
|
|
37620
37837
|
"kirjapro_create_invoice",
|
|
37621
37838
|
params.idempotencyKey,
|
|
37622
37839
|
params,
|
|
37623
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
39697
|
-
const digits = baseNumber.replace(/\D/g, "");
|
|
39698
|
-
if (digits.length <
|
|
39699
|
-
return { reference: "", error: "Base number must be
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
|
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 (
|
|
40440
|
+
if (vat2 > 0) {
|
|
39937
40441
|
entries.push({
|
|
39938
40442
|
accountNumber: ACCOUNTS.ALV_VELKA,
|
|
39939
40443
|
debitAmount: 0,
|
|
39940
|
-
creditAmount:
|
|
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 (
|
|
40461
|
+
if (vat2 > 0) {
|
|
39958
40462
|
entries.push({
|
|
39959
40463
|
accountNumber: ACCOUNTS.ALV_VELKA,
|
|
39960
40464
|
debitAmount: 0,
|
|
39961
|
-
creditAmount:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
40623
|
-
validUntil:
|
|
40624
|
-
|
|
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:
|
|
40634
|
-
tyelEmployer:
|
|
40635
|
-
tyelEmployerAverage:
|
|
40636
|
-
tyelKeskimaarainen:
|
|
40637
|
-
tyelPerusmaksu:
|
|
40638
|
-
|
|
40639
|
-
|
|
40640
|
-
|
|
40641
|
-
|
|
40642
|
-
|
|
40643
|
-
|
|
40644
|
-
|
|
40645
|
-
|
|
40646
|
-
}
|
|
40647
|
-
|
|
40648
|
-
|
|
40649
|
-
const
|
|
40650
|
-
const
|
|
40651
|
-
|
|
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
|
-
|
|
40655
|
-
|
|
40656
|
-
|
|
40657
|
-
|
|
40658
|
-
|
|
40659
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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):
|
|
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):
|
|
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
|
|
40743
|
-
|
|
40744
|
-
|
|
40745
|
-
|
|
40746
|
-
|
|
40747
|
-
|
|
40748
|
-
|
|
40749
|
-
return toErrorContent(
|
|
40750
|
-
|
|
40751
|
-
|
|
40752
|
-
|
|
40753
|
-
if (
|
|
40754
|
-
|
|
40755
|
-
|
|
40756
|
-
|
|
40757
|
-
|
|
40758
|
-
|
|
40759
|
-
|
|
40760
|
-
|
|
40761
|
-
|
|
40762
|
-
|
|
40763
|
-
|
|
40764
|
-
|
|
40765
|
-
|
|
40766
|
-
|
|
40767
|
-
|
|
40768
|
-
|
|
40769
|
-
|
|
40770
|
-
|
|
40771
|
-
|
|
40772
|
-
|
|
40773
|
-
|
|
40774
|
-
|
|
40775
|
-
|
|
40776
|
-
|
|
40777
|
-
|
|
40778
|
-
|
|
40779
|
-
|
|
40780
|
-
{
|
|
40781
|
-
|
|
40782
|
-
|
|
40783
|
-
|
|
40784
|
-
|
|
40785
|
-
|
|
40786
|
-
|
|
40787
|
-
|
|
40788
|
-
{ accountNumber: params.
|
|
40789
|
-
|
|
40790
|
-
|
|
40791
|
-
|
|
40792
|
-
|
|
40793
|
-
|
|
40794
|
-
|
|
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
|
-
|
|
40802
|
-
...
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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).
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
42477
|
-
|
|
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: "
|
|
42483
|
-
{ type: "Henkil\xF6st\xF6kulut (staff events)", vat_deduction: "
|
|
42484
|
-
{ type: "Insurance premiums", vat_deduction: "
|
|
42485
|
-
{ type: "Healthcare (staff)", vat_deduction: "
|
|
42486
|
-
{ type: "Financial services", vat_deduction: "
|
|
42487
|
-
{ type: "Henkil\xF6auto (passenger car M1)", vat_deduction: "
|
|
42488
|
-
{ type: "Pakettiauto/kuorma-auto (van N1+)", vat_deduction: "Proportional to business use", income_tax: "
|
|
42489
|
-
{ type: "Markkinointitilaisuudet (marketing events)", vat_deduction: "
|
|
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
|
|
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: "
|
|
42496
|
-
{ type: "Construction services", basis: "AVL 8c\xA7", note: 'Since 1.4.2011. Buyer must itself sell/rent construction services "not just occasionally".
|
|
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: "
|
|
42502
|
-
note: "Net VAT effect is zero
|
|
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
|
-
"
|
|
42507
|
-
"
|
|
42508
|
-
"
|
|
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: "
|
|
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: "
|
|
42529
|
-
{ kohta: 10, field: "Suoritettavan veron m\xE4\xE4r\xE4", english: "VAT amount", status: "implemented", relevance: "
|
|
42530
|
-
{ kohta: 11, field: "Merkint\xE4 verottomuudesta", english: "Tax exemption notation", status: "
|
|
42531
|
-
{ kohta: 12, field: "Merkint\xE4 k\xE4\xE4nnetyst\xE4 verovelvollisuudesta", english: "Reverse charge notation", status: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
42545
|
-
mandatory: "Unambiguous reference to original invoice (
|
|
42546
|
-
recommended: "
|
|
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: "
|
|
42553
|
-
{ revenue: "30,000\
|
|
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: "
|
|
42566
|
-
payment: "
|
|
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
|
|
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: "
|
|
42578
|
-
alarajahuojennus: "
|
|
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.
|
|
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
|
-
"
|
|
42628
|
-
"
|
|
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:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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:
|
|
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: "
|
|
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
|
|
42691
|
-
{ type: "Financial statements
|
|
42692
|
-
{ type: "Vouchers, invoices
|
|
42693
|
-
{ type: "E-invoice
|
|
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 (
|
|
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",
|
|
42703
|
-
{ class: "Pienyritys",
|
|
42704
|
-
|
|
42705
|
-
|
|
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
|
|
42711
|
-
|
|
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: "
|
|
42716
|
-
|
|
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
|
|
42721
|
-
|
|
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
|
|
42726
|
-
|
|
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: "
|
|
42731
|
-
|
|
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
|
|
42736
|
-
const
|
|
42737
|
-
|
|
42738
|
-
|
|
42739
|
-
|
|
42740
|
-
|
|
42741
|
-
|
|
42742
|
-
|
|
42743
|
-
|
|
42744
|
-
|
|
42745
|
-
|
|
42746
|
-
|
|
42747
|
-
|
|
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
|
|
43485
|
+
for (const slug of Object.keys(TOPICS)) {
|
|
42752
43486
|
server2.registerResource(
|
|
42753
43487
|
`compliance-${slug}`,
|
|
42754
43488
|
`kirjapro://compliance/${slug}`,
|
|
42755
|
-
{
|
|
42756
|
-
|
|
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
|
-
|
|
42773
|
-
|
|
42774
|
-
|
|
42775
|
-
|
|
42776
|
-
|
|
42777
|
-
|
|
42778
|
-
topic: slug,
|
|
42779
|
-
|
|
42780
|
-
|
|
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\`
|
|
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
|
|
43144
|
-
|
|
43145
|
-
|
|
43146
|
-
|
|
43147
|
-
|
|
43148
|
-
|
|
43149
|
-
|
|
43150
|
-
|
|
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();
|