@kirjapro/mcp-server 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35679,7 +35679,7 @@ var StdioServerTransport = class {
35679
35679
  import { createClient } from "@supabase/supabase-js";
35680
35680
 
35681
35681
  // src/version.ts
35682
- var VERSION = "0.10.0";
35682
+ var VERSION = "0.12.0";
35683
35683
 
35684
35684
  // src/auth.ts
35685
35685
  var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
@@ -36882,7 +36882,7 @@ function registerTransactionTools(server2) {
36882
36882
  server2.registerTool(
36883
36883
  "kirjapro_create_transaction",
36884
36884
  {
36885
- description: "Create a transaction (tosite) with journal entries. Entries must balance (debits = credits). Accepts account numbers resolved to UUIDs server-side. Prefer kirjapro_create_from_template for standard patterns. For a VAT-registered (ALV) company the database refuses lines on sales accounts 5000-5499 and a net 4210 credit: sales are invoices.",
36885
+ description: "Create a transaction (tosite) with journal entries. Entries must balance (debits = credits). Accepts account numbers resolved to UUIDs server-side. Prefer kirjapro_create_from_template for standard patterns. For a VAT-registered (ALV) company the database refuses a net credit on sales accounts 5000-5499, a sales-account line together with a 4210 or 2320 line, and a net 4210 credit: sales are invoices. Moving an amount out of a sales account without a VAT line (e.g. a fixed-asset disposal D 5010 / K 1600) is allowed. A purchase with deductible VAT belongs in kirjapro_book_purchase so the VAT reaches field 307; a free 2320 line is listed by the ALV reconciliation.",
36886
36886
  inputSchema: {
36887
36887
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
36888
36888
  transactionDate: ISO_DATE_SCHEMA.describe("Transaction date (YYYY-MM-DD)"),
@@ -37021,7 +37021,7 @@ function registerTransactionTools(server2) {
37021
37021
  server2.registerTool(
37022
37022
  "kirjapro_create_transactions_batch",
37023
37023
  {
37024
- description: "Preview or atomically create multiple transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable. For a VAT-registered (ALV) company the database refuses lines on sales accounts 5000-5499 and a net 4210 credit: sales are invoices.",
37024
+ description: "Preview or atomically create multiple transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable. For a VAT-registered (ALV) company the database refuses a net credit on sales accounts 5000-5499, a sales-account line together with a 4210 or 2320 line, and a net 4210 credit: sales are invoices. Moving an amount out of a sales account without a VAT line (e.g. a fixed-asset disposal D 5010 / K 1600) is allowed. A purchase with deductible VAT belongs in kirjapro_book_purchase so the VAT reaches field 307; a free 2320 line is listed by the ALV reconciliation.",
37025
37025
  inputSchema: {
37026
37026
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
37027
37027
  dryRun: external_exports.boolean().default(true).describe(
@@ -37395,7 +37395,7 @@ function validateVatTreatmentEligibility({
37395
37395
  if (code === "standard_13_5" && documentDate < "2026-01-01") {
37396
37396
  return "standard_13_5 is not valid before 2026-01-01.";
37397
37397
  }
37398
- const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods";
37398
+ const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu";
37399
37399
  if (requiresBusiness && customer?.customer_kind !== "business") {
37400
37400
  return "The selected VAT treatment requires a business customer.";
37401
37401
  }
@@ -37410,7 +37410,7 @@ function validateVatTreatmentEligibility({
37410
37410
  return "EU sale requires a non-Finnish EU country and a matching buyer VAT number.";
37411
37411
  }
37412
37412
  }
37413
- if (code === "export_outside_eu") {
37413
+ if (code === "export_outside_eu" || code === "service_outside_eu") {
37414
37414
  const country = (customer?.country ?? "").trim().toUpperCase();
37415
37415
  if (!country || EU_COUNTRY_CODES.has(country)) {
37416
37416
  return "Export outside the EU requires a buyer country outside the EU.";
@@ -37423,7 +37423,7 @@ function validateVatTreatmentEligibility({
37423
37423
  var CREDIT_PAGE_SIZE = 500;
37424
37424
  async function fetchFinalizedCreditTotal(supabase2, companyId2, invoiceId) {
37425
37425
  let offset = 0;
37426
- let total = 0;
37426
+ let total2 = 0;
37427
37427
  let expectedCount;
37428
37428
  for (; ; ) {
37429
37429
  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,24 +37441,24 @@ async function fetchFinalizedCreditTotal(supabase2, companyId2, invoiceId) {
37441
37441
  if (!Number.isFinite(credit.total)) {
37442
37442
  return { total: null, error: { message: "A finalized credit has an invalid amount." } };
37443
37443
  }
37444
- total += credit.total;
37444
+ total2 += credit.total;
37445
37445
  }
37446
37446
  offset += data.length;
37447
- if (offset === count) return { total: roundCents(total), error: null };
37447
+ if (offset === count) return { total: roundCents(total2), error: null };
37448
37448
  }
37449
37449
  }
37450
- function calculateInvoiceBalance(total, payments, creditedTotal) {
37450
+ function calculateInvoiceBalance(total2, payments, creditedTotal) {
37451
37451
  const paidTotal = roundCents(
37452
37452
  (payments ?? []).reduce((sum, payment) => sum + (payment.amount || 0), 0)
37453
37453
  );
37454
- return { paidTotal, remainingBalance: roundCents(total - paidTotal - creditedTotal) };
37454
+ return { paidTotal, remainingBalance: roundCents(total2 - paidTotal - creditedTotal) };
37455
37455
  }
37456
37456
 
37457
37457
  // src/tools/invoices.ts
37458
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");
37459
+ const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
37460
+ if (digits.length < 3 || digits.length > 19) {
37461
+ throw new Error("Base number must be 3-19 digits without leading zeros");
37462
37462
  }
37463
37463
  const weights = [7, 3, 1];
37464
37464
  let sum = 0;
@@ -37597,6 +37597,7 @@ function registerInvoiceTools(server2) {
37597
37597
  "reverse_scrap",
37598
37598
  "reverse_eu_service",
37599
37599
  "export_outside_eu",
37600
+ "service_outside_eu",
37600
37601
  "intra_eu_goods",
37601
37602
  "exempt_healthcare",
37602
37603
  "exempt_social_welfare",
@@ -37604,7 +37605,7 @@ function registerInvoiceTools(server2) {
37604
37605
  "exempt_financial",
37605
37606
  "exempt_insurance",
37606
37607
  "exempt_other"
37607
- ]).optional().describe("Structured VAT treatment. Required when VAT rate is 0.")
37608
+ ]).optional().describe("Structured VAT treatment. Required when VAT rate is 0. service_outside_eu is ONLY for AVL 65 general-rule B2B services: verify business use, evidence of buyer status and receiving establishment outside the EU; special place-of-supply services are excluded. Check foreign buyer liability and invoice notes. PDF delivery only; e-invoice delivery is blocked.")
37608
37609
  })
37609
37610
  ).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
37610
37611
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
@@ -37658,6 +37659,7 @@ function registerInvoiceTools(server2) {
37658
37659
  reverse_scrap: 0,
37659
37660
  reverse_eu_service: 0,
37660
37661
  export_outside_eu: 0,
37662
+ service_outside_eu: 0,
37661
37663
  intra_eu_goods: 0,
37662
37664
  exempt_healthcare: 0,
37663
37665
  exempt_social_welfare: 0,
@@ -37691,7 +37693,7 @@ function registerInvoiceTools(server2) {
37691
37693
  const gross = roundCents(line.quantity * line.unitPrice);
37692
37694
  const vatAmount = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross * vatRate / (100 + vatRate)) : roundCents(gross * (vatRate / 100));
37693
37695
  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);
37696
+ const total3 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? gross : roundCents(subtotal2 + vatAmount);
37695
37697
  return {
37696
37698
  line_number: i + 1,
37697
37699
  description: line.description,
@@ -37709,7 +37711,7 @@ function registerInvoiceTools(server2) {
37709
37711
  eu_goods_transport_confirmed: params.euGoodsTransportConfirmed,
37710
37712
  subtotal: subtotal2,
37711
37713
  vat_amount: vatAmount,
37712
- total: total2
37714
+ total: total3
37713
37715
  };
37714
37716
  });
37715
37717
  const subtotal = roundCents(
@@ -37718,7 +37720,7 @@ function registerInvoiceTools(server2) {
37718
37720
  const vatTotal = roundCents(
37719
37721
  calculatedLines.reduce((s, l) => s + l.vat_amount, 0)
37720
37722
  );
37721
- const total = roundCents(subtotal + vatTotal);
37723
+ const total2 = roundCents(subtotal + vatTotal);
37722
37724
  const { data: invoiceId, error: invError } = await supabase2.rpc(
37723
37725
  "create_invoice_with_lines",
37724
37726
  {
@@ -37731,7 +37733,7 @@ function registerInvoiceTools(server2) {
37731
37733
  p_status: "draft",
37732
37734
  p_subtotal: subtotal,
37733
37735
  p_vat_total: vatTotal,
37734
- p_total: total,
37736
+ p_total: total2,
37735
37737
  p_notes: params.notes || "",
37736
37738
  p_lines: calculatedLines
37737
37739
  }
@@ -38439,6 +38441,83 @@ function registerCustomerTools(server2) {
38439
38441
  );
38440
38442
  }
38441
38443
 
38444
+ // src/compliance/omavero-fields.ts
38445
+ var OMAVERO_FIELD_NUMBERS = [
38446
+ "301",
38447
+ "302",
38448
+ "303",
38449
+ "304",
38450
+ "305",
38451
+ "306",
38452
+ "307",
38453
+ "308",
38454
+ "309",
38455
+ "310",
38456
+ "311",
38457
+ "312",
38458
+ "313",
38459
+ "314",
38460
+ "318",
38461
+ "319",
38462
+ "320"
38463
+ ];
38464
+ var LEGACY_ALV_SUMMARY_KEYS = [
38465
+ "field_307",
38466
+ "field_307_base",
38467
+ "field_308",
38468
+ "field_308_base",
38469
+ "field_309",
38470
+ "field_309_base",
38471
+ "field_310",
38472
+ "field_310_base",
38473
+ "field_312",
38474
+ "field_312_base",
38475
+ "field_313",
38476
+ "field_313_base",
38477
+ "field_318",
38478
+ "field_319",
38479
+ "field_320",
38480
+ "total_zero_rated",
38481
+ "reverse_charge_base",
38482
+ "omavero"
38483
+ ];
38484
+ function cents(value) {
38485
+ return Math.round(value * 100) / 100;
38486
+ }
38487
+ function num(value) {
38488
+ const n = typeof value === "number" ? value : Number(value ?? 0);
38489
+ return Number.isFinite(n) ? n : 0;
38490
+ }
38491
+ function omaveroFieldsFromSummary(s) {
38492
+ const fields = {
38493
+ "301": num(s.field_301),
38494
+ "302": num(s.field_302),
38495
+ "303": num(s.field_303),
38496
+ "304": num(s.output_vat_import),
38497
+ "305": num(s.output_vat_eu_goods),
38498
+ "306": num(s.output_vat_eu_services),
38499
+ "307": num(s.total_input_vat),
38500
+ "308": num(s.net_vat_payable),
38501
+ "309": num(s.zero_vat_turnover),
38502
+ "310": num(s.field_312_base),
38503
+ "311": num(s.eu_goods_sales_base),
38504
+ "312": num(s.eu_service_sales_base),
38505
+ "313": num(s.field_308_base),
38506
+ "314": num(s.field_309_base),
38507
+ "318": num(s.output_vat_construction_scrap),
38508
+ "319": num(s.reverse_charge_sales_base),
38509
+ "320": cents(num(s.field_310_base) + num(s.field_313_base))
38510
+ };
38511
+ const map2 = s.omavero;
38512
+ if (map2 && typeof map2 === "object") {
38513
+ const values = map2;
38514
+ for (const field of OMAVERO_FIELD_NUMBERS) {
38515
+ if (values[field] !== void 0 && values[field] !== null) fields[field] = num(values[field]);
38516
+ }
38517
+ }
38518
+ return fields;
38519
+ }
38520
+
38442
38521
  // src/lib/incomeStatementSynthesis.ts
38443
38522
  function synthesizeIncomeStatementLines(lineTypes, currentByLine, previousByLine) {
38444
38523
  const lineAmounts = new Map(currentByLine);
@@ -38593,24 +38672,34 @@ var ALV_SUMMARY_OUTPUT_SHAPE = {
38593
38672
  input_vat: external_exports.unknown().optional(),
38594
38673
  net_vat_payable: external_exports.number().optional(),
38595
38674
  net_vat_payable_formatted: external_exports.string().optional(),
38596
- omavero_fields: external_exports.record(external_exports.string(), external_exports.number()).optional(),
38675
+ // Every official OmaVero field (VSRALVKV v1.10: 301-314, 318-320), ALV
38676
+ // report audit 2026-10-01, T2-11. The only field-numbered output.
38677
+ omavero_fields: external_exports.record(external_exports.string(), external_exports.number()).describe(
38678
+ "Official OmaVero VAT-return fields 301-314 and 318-320. 307 is all deductible VAT; 308 = 301+302+303+304+305+306+318 - 307."
38679
+ ).optional(),
38680
+ // Sales without a deduction right: in no OmaVero field (informational).
38681
+ exempt_sales_base: external_exports.number().optional(),
38597
38682
  zero_vat_turnover: external_exports.number().optional(),
38598
38683
  eu_goods_sales_base: external_exports.number().optional(),
38599
38684
  reverse_charge_sales_base: external_exports.number().optional(),
38600
38685
  eu_service_sales_base: external_exports.number().optional(),
38601
38686
  unclassified_legacy_reverse_charge_base: external_exports.number().optional(),
38687
+ // VAT on services bought from outside the EU, already inside field_301-303
38688
+ // and total_output_vat (ALV report audit 2026-10-01, T2-13).
38689
+ output_vat_non_eu_services: external_exports.number().optional(),
38602
38690
  // get_alv_summary.reconciliation: the report against the 4210/2320 ledger
38603
38691
  // (ALV report audit 2026-10-01, T2-1). Absent on an older database.
38604
38692
  reconciliation: external_exports.object({ status: external_exports.enum(["match", "explained", "mismatch"]) }).passthrough().optional(),
38605
38693
  warning: external_exports.string().optional(),
38606
38694
  message: external_exports.string().optional()
38607
38695
  };
38696
+ var LEGACY_REVERSE_CHARGE_WARNING = "Legacy reverse-charge invoice lines must be reclassified before the fields are complete.";
38608
38697
  function alvReconciliationWarning(reconciliation) {
38609
38698
  if (!reconciliation || typeof reconciliation !== "object") return void 0;
38610
38699
  const r = reconciliation;
38611
- if (r.status !== "mismatch") return void 0;
38700
+ if (r.status !== "mismatch" && r.status !== "explained") return void 0;
38612
38701
  const amount = (value) => (Number(value) || 0).toFixed(2);
38613
- return `Ledger accounts 4210/2320 differ from the ALV report (output VAT difference ${amount(r.output_difference)} \u20AC, input VAT difference ${amount(r.input_difference)} \u20AC). Review reconciliation before filing.`;
38702
+ return `VAT amounts or the audit trail need review (output VAT difference ${amount(r.output_difference)} \u20AC, input VAT difference ${amount(r.input_difference)} \u20AC). Equal amounts do not link a manual VAT row to a voucher. Review reconciliation before filing; do not duplicate the purchase expense.`;
38614
38703
  }
38615
38704
  var BALANCE_SHEET_ROW_SHAPE = external_exports.object({
38616
38705
  account_number: external_exports.string(),
@@ -39087,7 +39176,7 @@ function registerReportTools(server2) {
39087
39176
  server2.registerTool(
39088
39177
  "kirjapro_get_alv_summary",
39089
39178
  {
39090
- description: "Get VAT (ALV) summary for a date range. Returns output/input VAT breakdown by rate and OmaVero field codes (301-320), and a reconciliation object comparing the 4210/2320 ledger with the report (status match, explained or mismatch, with the differences and the vouchers, invoices, fact mismatches and manual rows that explain them). A mismatch sets warning. For filing deadlines, read kirjapro://compliance/deadlines.",
39179
+ description: "Get the VAT (ALV) summary for a date range. Returns `omavero_fields`, every official OmaVero VAT-return field (VSRALVKV v1.10: 301-314, 318-320). 307 is the total deductible VAT including reverse charge and import VAT; 308 = (301+302+303+304+305+306+318) - 307. 309 excludes 311/312 sales; exempt sales without a deduction right (`exempt_sales_base`) belong in no field. 311 follows AVL 138 a \xA7 (the delivery month when invoiced on or after the delivery in that month; otherwise, including an advance invoice, the next month). Fields 301-303 include the self-assessed VAT on services bought from outside the EU (output_vat_non_eu_services). Copy fields only from `omavero_fields`. Also returns a reconciliation object comparing the 4210/2320 ledger with the report (status match, explained or mismatch, with the differences and the vouchers, invoices, fact mismatches and manual rows that explain them); a mismatch or unclassified legacy reverse-charge lines set warning. For filing deadlines, read kirjapro://compliance/deadlines.",
39091
39180
  inputSchema: {
39092
39181
  dateFrom: ISO_DATE_SCHEMA.describe("Period start date (YYYY-MM-DD)"),
39093
39182
  dateTo: ISO_DATE_SCHEMA.describe("Period end date (YYYY-MM-DD)")
@@ -39127,26 +39216,21 @@ function registerReportTools(server2) {
39127
39216
  });
39128
39217
  }
39129
39218
  const raw = result;
39130
- const {
39131
- field_318: _legacyField318,
39132
- field_319: _legacyField319,
39133
- field_320: _legacyField320,
39134
- ...safeResult
39135
- } = raw;
39136
- const warning = alvReconciliationWarning(raw.reconciliation);
39219
+ const omaveroFields = omaveroFieldsFromSummary(raw);
39220
+ const legacyKeys = new Set(LEGACY_ALV_SUMMARY_KEYS);
39221
+ const safeResult = Object.fromEntries(
39222
+ Object.entries(raw).filter(([key]) => !legacyKeys.has(key))
39223
+ );
39224
+ const warnings = [
39225
+ alvReconciliationWarning(raw.reconciliation),
39226
+ Number(raw.unclassified_legacy_reverse_charge_base) ? LEGACY_REVERSE_CHARGE_WARNING : void 0
39227
+ ].filter((text) => Boolean(text));
39137
39228
  return toResult({
39138
39229
  ...safeResult,
39139
- ...warning && { warning },
39230
+ ...warnings.length > 0 && { warning: warnings.join(" ") },
39140
39231
  period: { from: params.dateFrom, to: params.dateTo },
39141
39232
  net_vat_payable_formatted: formatCurrency(result.net_vat_payable || 0),
39142
- omavero_fields: {
39143
- "309": Number(raw.zero_vat_turnover) || 0,
39144
- "311": Number(raw.eu_goods_sales_base) || 0,
39145
- "312": Number(raw.eu_service_sales_base) || 0,
39146
- "318": Number(raw.output_vat_construction_scrap) || 0,
39147
- "319": Number(raw.reverse_charge_sales_base) || 0,
39148
- "320": (Number(raw.field_310_base) || 0) + (Number(raw.field_313_base) || 0)
39149
- }
39233
+ omavero_fields: omaveroFields
39150
39234
  });
39151
39235
  }
39152
39236
  );
@@ -39612,9 +39696,9 @@ function validateFinnishIBAN(input2) {
39612
39696
  return { valid: true };
39613
39697
  }
39614
39698
  function generateReferenceNumber2(baseNumber) {
39615
- const digits = baseNumber.replace(/\D/g, "");
39616
- if (digits.length < 1 || digits.length > 19) {
39617
- return { reference: "", error: "Base number must be 1-19 digits." };
39699
+ const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
39700
+ if (digits.length < 3 || digits.length > 19) {
39701
+ return { reference: "", error: "Base number must be 3-19 digits without leading zeros." };
39618
39702
  }
39619
39703
  const weights = [7, 3, 1];
39620
39704
  let sum = 0;
@@ -39666,7 +39750,7 @@ function registerValidationTools(server2) {
39666
39750
  {
39667
39751
  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.",
39668
39752
  inputSchema: {
39669
- baseNumber: external_exports.string().describe('Base number (1-19 digits, e.g. invoice number "00042")')
39753
+ baseNumber: external_exports.string().describe('Base number (3-19 digits excluding leading zeros, e.g. "20260042")')
39670
39754
  },
39671
39755
  outputSchema: REFERENCE_OUTPUT_SHAPE,
39672
39756
  annotations: READ_ONLY_ANNOTATIONS
@@ -39715,6 +39799,392 @@ function registerFiscalYearTools(server2) {
39715
39799
  );
39716
39800
  }
39717
39801
 
39802
+ // src/compliance/purchase-vat-input.ts
39803
+ var PURCHASE_VAT_BASIS_SCHEMA = external_exports.object({
39804
+ version: external_exports.literal(1),
39805
+ kind: external_exports.enum([
39806
+ "supply",
39807
+ "supply_month",
39808
+ "advance_payment",
39809
+ "continuous_period_end",
39810
+ "continuous_year_end",
39811
+ "eu_acquisition_month",
39812
+ "customs_acceptance",
39813
+ "original_document_rate"
39814
+ ]),
39815
+ date: ISO_DATE_SCHEMA.optional(),
39816
+ month: external_exports.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).optional(),
39817
+ evidence_reference: external_exports.string().trim().min(1).max(2e3),
39818
+ correction_reason: external_exports.string().trim().min(1).max(500).optional()
39819
+ }).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.");
39820
+
39821
+ // src/compliance/regulatory-data.ts
39822
+ var MCP_REGISTRY = {
39823
+ schemaVersion: 1,
39824
+ jurisdiction: "FI",
39825
+ sources: {
39826
+ vat: {
39827
+ title: "Verohallinto: arvonlis\xE4veroprosentit",
39828
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/arvonlisaveroprosentit/"
39829
+ },
39830
+ "vat-2013-transition": {
39831
+ title: "Verohallinto A119/200/2012: arvonlis\xE4verokantojen muutos 1.1.2013 (virallisohjeen Edilex-kopio)",
39832
+ url: "https://www.edilex.fi/verohallinnon_ohjeet/2012_1220.html",
39833
+ watch: false
39834
+ },
39835
+ "vat-vehicles": {
39836
+ title: "Vero: ajoneuvojen ja k\xE4ytt\xF6kulujen ALV",
39837
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ajoneuvon-ja-sen-kayttokulujen-arvonlisavero/"
39838
+ },
39839
+ "vat-own-use": {
39840
+ title: "Vero: oman k\xE4yt\xF6n ja alivastikkeellisten luovutusten ALV",
39841
+ url: "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/47899/oman-k%C3%A4yt%C3%B6n-ja-alivastikkeellisten-luovutusten-arvonlis%C3%A4verotus/"
39842
+ },
39843
+ "vat-return": {
39844
+ title: "Vero: ALV-ilmoituksen t\xE4ytt\xF6ohje",
39845
+ url: "https://www.vero.fi/alv/tayttoohje"
39846
+ },
39847
+ "vat-small-business": {
39848
+ title: "Vero: arvonlis\xE4veroton v\xE4h\xE4inen toiminta",
39849
+ url: "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48658/arvonlisaveroton-vahainen-toiminta/"
39850
+ },
39851
+ "vat-recap": {
39852
+ title: "Vero: yhteenvetoilmoitus",
39853
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ulkomaankauppa/yhteisokauppa/yhteenvetoilmoitus-ilmoitusohjeet/"
39854
+ },
39855
+ "vat-invoices": {
39856
+ title: "Vero: laskutusvaatimukset",
39857
+ url: "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48090/laskutusvaatimukset-arvonlisaverotuksessa3/"
39858
+ },
39859
+ "einvoice-law": {
39860
+ title: "Valtiokonttori: verkkolaskulaki ja valtion vastaanotto",
39861
+ url: "https://www.valtiokonttori.fi/verkkolasku/"
39862
+ },
39863
+ payroll: {
39864
+ title: "Verohallinto: sosiaalivakuutusmaksut",
39865
+ url: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/yritys_tyonantajana/sosiaalivakuutusmaksut/"
39866
+ },
39867
+ unemployment: {
39868
+ title: "Ty\xF6llisyysrahasto: vahvistetut maksut 2026",
39869
+ url: "https://www.tyollisyysrahasto.fi/uutiset/vuoden-2026-tyottomyysvakuutusmaksut-on-vahvistettu/"
39870
+ },
39871
+ unemploymentScope: {
39872
+ title: "Ty\xF6llisyysrahasto: maksuvelvollisuus",
39873
+ url: "https://www.tyollisyysrahasto.fi/tyottomyysvakuusmaksut/tyottomyysvakuutusmaksuvelvollisuus/"
39874
+ },
39875
+ tyel: {
39876
+ title: "Ilmarinen: TyEL-maksut",
39877
+ url: "https://www.ilmarinen.fi/tyonantaja/tyel-maksut/"
39878
+ },
39879
+ "incomes-register": {
39880
+ title: "Tulorekisteri: palkkojen ilmoittaminen",
39881
+ url: "https://www.vero.fi/tulorekisteri/yritykset-ja-organisaatiot/yksityiskohtaiset-tulorekisterin-ohjeet/90124/palkat-tietojen-ilmoittaminen-tulorekisteriin6/"
39882
+ },
39883
+ "company-size": {
39884
+ title: "Suomi.fi: tilinp\xE4\xE4t\xF6s",
39885
+ url: "https://www.suomi.fi/yritykselle/talouden-hallinta-ja-verotus/yrityksen-taloushallinto/opas/tilinpaatos-ja-tilintarkastus/tilinpaatos"
39886
+ },
39887
+ "bookkeeping-act": {
39888
+ title: "Finlex: ajantasainen kirjanpitolaki",
39889
+ url: "https://www.finlex.fi/fi/laki/ajantasa/1997/19971336",
39890
+ format: "finlex-akn",
39891
+ workUri: "/akn/fi/act/statute-consolidated/1997/1336",
39892
+ requiredText: "Luettelo kirjanpidoista ja aineistoista"
39893
+ },
39894
+ "bookkeeping-snapshot": {
39895
+ title: "Kirjanpitolaki, ajantasaisen lain virallinen tallenne",
39896
+ url: "https://eur-lex.europa.eu/legal-content/FI/TXT/PDF/?uri=NIM%3A202604826",
39897
+ watch: false
39898
+ },
39899
+ "small-company-transition": {
39900
+ title: "KILA 1982/2018: yksitt\xE4isen pienyrityksen kokorajasiirtym\xE4, kohta 2.2",
39901
+ url: "https://kirjanpitolautakunta.fi/-/pienyrityksen-ja-pienkonsernin-raja-arvojen-arvioimises-1"
39902
+ }
39903
+ },
39904
+ rules: {
39905
+ vat: {
39906
+ kind: "statutory",
39907
+ units: {
39908
+ general: "percent",
39909
+ reduced: "percent",
39910
+ newspapers: "percent",
39911
+ zero: "percent"
39912
+ },
39913
+ conditions: "Kanta valitaan tapahtuman ja hy\xF6dykkeen verokohtelun perusteella. 0 % ei ole sama kuin verosta vapautettu myynti. Ei muuta tallennettujen tapahtumien historiallisia kantoja.",
39914
+ periods: [
39915
+ {
39916
+ validFrom: "2013-01-01",
39917
+ validUntil: "2024-08-31",
39918
+ reviewedOn: "2026-10-03",
39919
+ reviewDue: "2026-11-20",
39920
+ sourceIds: ["vat", "vat-2013-transition"],
39921
+ values: {
39922
+ general: 24,
39923
+ reduced: 14,
39924
+ newspapers: 10,
39925
+ zero: 0
39926
+ }
39927
+ },
39928
+ {
39929
+ validFrom: "2024-09-01",
39930
+ validUntil: "2025-12-31",
39931
+ reviewedOn: "2026-09-05",
39932
+ reviewDue: "2026-11-20",
39933
+ sourceIds: ["vat"],
39934
+ values: {
39935
+ general: 25.5,
39936
+ reduced: 14,
39937
+ newspapers: 10,
39938
+ zero: 0
39939
+ }
39940
+ },
39941
+ {
39942
+ validFrom: "2026-01-01",
39943
+ validUntil: null,
39944
+ reviewedOn: "2026-09-06",
39945
+ reviewDue: "2026-11-20",
39946
+ sourceIds: [
39947
+ "vat",
39948
+ "vat-vehicles",
39949
+ "vat-own-use",
39950
+ "vat-return",
39951
+ "vat-small-business",
39952
+ "vat-recap",
39953
+ "vat-invoices",
39954
+ "einvoice-law"
39955
+ ],
39956
+ values: {
39957
+ general: 25.5,
39958
+ reduced: 13.5,
39959
+ newspapers: 10,
39960
+ zero: 0
39961
+ }
39962
+ }
39963
+ ]
39964
+ },
39965
+ payroll: {
39966
+ kind: "statutory",
39967
+ units: {
39968
+ tyelEmployee: "percent",
39969
+ health: "percent",
39970
+ unemploymentEmployee: "percent",
39971
+ unemploymentLower: "percent",
39972
+ unemploymentUpper: "percent",
39973
+ unemploymentThreshold: "EUR",
39974
+ unemploymentExemption: "EUR",
39975
+ tyelMonthlyMinimum: "EUR"
39976
+ },
39977
+ 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.",
39978
+ periods: [
39979
+ {
39980
+ validFrom: "2026-01-01",
39981
+ validUntil: "2026-12-31",
39982
+ reviewedOn: "2026-09-06",
39983
+ reviewDue: "2026-11-20",
39984
+ sourceIds: ["payroll", "unemployment", "unemploymentScope", "tyel", "incomes-register"],
39985
+ values: {
39986
+ tyelEmployee: 7.3,
39987
+ health: 1.91,
39988
+ unemploymentEmployee: 0.89,
39989
+ unemploymentLower: 0.31,
39990
+ unemploymentUpper: 1.23,
39991
+ unemploymentThreshold: 2509500,
39992
+ unemploymentExemption: 1500,
39993
+ tyelMonthlyMinimum: 71.72
39994
+ }
39995
+ }
39996
+ ]
39997
+ },
39998
+ payrollEstimates: {
39999
+ kind: "estimate",
40000
+ units: {
40001
+ tyelBaseEmployer: "percent",
40002
+ tyelAverageEmployer: "percent",
40003
+ tyelTemporaryEmployer: "percent",
40004
+ accidentLow: "percent",
40005
+ accidentAverage: "percent",
40006
+ accidentHigh: "percent",
40007
+ accidentVeryHigh: "percent",
40008
+ groupLife: "percent",
40009
+ holidayBonus: "months/year",
40010
+ workingHours: "hours/month"
40011
+ },
40012
+ 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.",
40013
+ periods: [
40014
+ {
40015
+ validFrom: "2026-01-01",
40016
+ validUntil: "2026-12-31",
40017
+ reviewedOn: "2026-09-05",
40018
+ reviewDue: "2026-11-20",
40019
+ sourceIds: ["tyel", "payroll"],
40020
+ values: {
40021
+ tyelBaseEmployer: 17.55,
40022
+ tyelAverageEmployer: 17.1,
40023
+ tyelTemporaryEmployer: 18.55,
40024
+ accidentLow: 0.25,
40025
+ accidentAverage: 0.51,
40026
+ accidentHigh: 1.5,
40027
+ accidentVeryHigh: 2.8,
40028
+ groupLife: 0.06,
40029
+ holidayBonus: 0.5,
40030
+ workingHours: 158
40031
+ }
40032
+ }
40033
+ ]
40034
+ },
40035
+ companySizes: {
40036
+ kind: "statutory",
40037
+ 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.",
40038
+ units: {
40039
+ soleTraderStatements: "euroa / henkil\xF6\xE4; katso avaimet",
40040
+ smallCompany: "euroa / henkil\xF6\xE4; katso avaimet",
40041
+ singleEntry: "euroa / henkil\xF6\xE4; katso avaimet"
40042
+ },
40043
+ periods: [
40044
+ {
40045
+ validFrom: "2026-01-01",
40046
+ validUntil: null,
40047
+ reviewedOn: "2026-09-06",
40048
+ reviewDue: "2026-12-01",
40049
+ sourceIds: [
40050
+ "company-size",
40051
+ "bookkeeping-act",
40052
+ "bookkeeping-snapshot",
40053
+ "small-company-transition"
40054
+ ],
40055
+ values: {
40056
+ soleTraderStatements: {
40057
+ assets: 45e4,
40058
+ revenue: 9e5,
40059
+ employees: 10
40060
+ },
40061
+ smallCompany: {
40062
+ assets: 75e5,
40063
+ revenue: 15e6,
40064
+ employees: 50
40065
+ },
40066
+ singleEntry: {
40067
+ assets: 1e5,
40068
+ revenue: 2e5,
40069
+ employees: 3
40070
+ }
40071
+ }
40072
+ }
40073
+ ]
40074
+ }
40075
+ }
40076
+ };
40077
+
40078
+ // src/compliance/date-local.ts
40079
+ var DATE_LOCAL_SHAPE = /^\d{4}-\d{2}-\d{2}$/;
40080
+ function parseDateLocal(dateStr) {
40081
+ if (!DATE_LOCAL_SHAPE.test(dateStr)) return new Date(Number.NaN);
40082
+ const [y, m, d] = dateStr.split("-").map(Number);
40083
+ if (y === void 0 || m === void 0 || d === void 0) return new Date(Number.NaN);
40084
+ return new Date(y, m - 1, d);
40085
+ }
40086
+ function formatDateLocal(date5) {
40087
+ const y = date5.getFullYear();
40088
+ const m = String(date5.getMonth() + 1).padStart(2, "0");
40089
+ const d = String(date5.getDate()).padStart(2, "0");
40090
+ return `${y}-${m}-${d}`;
40091
+ }
40092
+
40093
+ // src/compliance/purchase-vat-basis.ts
40094
+ var DAY_KINDS = /* @__PURE__ */ new Set([
40095
+ "supply",
40096
+ "advance_payment",
40097
+ "continuous_period_end",
40098
+ "continuous_year_end",
40099
+ "customs_acceptance"
40100
+ ]);
40101
+ var MONTH_KINDS = /* @__PURE__ */ new Set(["supply_month", "eu_acquisition_month"]);
40102
+ var CATEGORIES = /* @__PURE__ */ new Set([
40103
+ "domestic",
40104
+ "eu_goods",
40105
+ "eu_services",
40106
+ "non_eu_services",
40107
+ "import",
40108
+ "construction",
40109
+ "scrap_metal"
40110
+ ]);
40111
+ function isDay(value) {
40112
+ if (typeof value !== "string") return false;
40113
+ const date5 = parseDateLocal(value);
40114
+ return !Number.isNaN(date5.getTime()) && formatDateLocal(date5) === value;
40115
+ }
40116
+ function isMonth(value) {
40117
+ return typeof value === "string" && /^\d{4}-(0[1-9]|1[0-2])$/.test(value) && isDay(`${value}-01`);
40118
+ }
40119
+ function hasText(value, max = 500) {
40120
+ return typeof value === "string" && value.trim().length > 0 && value.length <= max;
40121
+ }
40122
+ function purchaseVatBasisError(value, category) {
40123
+ if (!CATEGORIES.has(category)) return "Valitse tunnettu oston ALV-k\xE4sittely.";
40124
+ if (!value || typeof value !== "object" || Array.isArray(value))
40125
+ return "Vahvista oston ALV-kannan peruste ennen kirjausta.";
40126
+ const basis = value;
40127
+ if (basis.version !== 1 || typeof basis.kind !== "string")
40128
+ return "ALV-kannan peruste on puutteellinen tai sen versio ei ole tuettu.";
40129
+ const dayKind = DAY_KINDS.has(basis.kind);
40130
+ const monthKind = MONTH_KINDS.has(basis.kind);
40131
+ const original = basis.kind === "original_document_rate";
40132
+ if (!dayKind && !monthKind && !original) return "Valitse tunnettu ALV-kannan peruste.";
40133
+ const keys = /* @__PURE__ */ new Set([
40134
+ "version",
40135
+ "kind",
40136
+ "evidence_reference",
40137
+ ...dayKind ? ["date"] : monthKind ? ["month"] : ["correction_reason"]
40138
+ ]);
40139
+ if (Object.keys(basis).some((key) => !keys.has(key)))
40140
+ return "ALV-kannan perusteessa on ristiriitaisia tai tuntemattomia tietoja.";
40141
+ if (!hasText(basis.evidence_reference, 2e3))
40142
+ return "Yksil\xF6i ALV-kannan perusteen tosite tai muu selvitys (enint\xE4\xE4n 2000 merkki\xE4).";
40143
+ if (dayKind && !isDay(basis.date)) return "Anna ALV-kannan perusteen todellinen p\xE4iv\xE4m\xE4\xE4r\xE4.";
40144
+ if (monthKind && !isMonth(basis.month))
40145
+ return "Anna ALV-kannan perusteen kuukausi muodossa VVVV-KK.";
40146
+ if (original && !hasText(basis.correction_reason))
40147
+ return "Kerro oikaisun syy ja yksil\xF6i alkuper\xE4inen tosite tai hyvitetty ajanjakso.";
40148
+ if (!original) {
40149
+ if (category === "eu_goods" !== (basis.kind === "eu_acquisition_month"))
40150
+ return "EU-tavaraoston kanta m\xE4\xE4r\xE4ytyy yhteis\xF6hankinnan kohdistuskuukaudelta. Tarkista vastaanotto ja laskun saaminen.";
40151
+ if (category === "import" !== (basis.kind === "customs_acceptance"))
40152
+ return "Maahantuonnin kanta vaatii tulli-ilmoituksen hyv\xE4ksymisen perusteen. Tarkista tullausasiakirja.";
40153
+ }
40154
+ if (basis.kind === "continuous_year_end" && (!["eu_services", "non_eu_services"].includes(category) || !basis.date.endsWith("-12-31")))
40155
+ return "Vuoden lopun peruste koskee yleiss\xE4\xE4nn\xF6n jatkuvaa ulkomaanpalvelua ilman tilityksi\xE4 tai maksuja; p\xE4iv\xE4ksi tulee 31.12.";
40156
+ return null;
40157
+ }
40158
+ function purchaseVatRatesForBasis(basis) {
40159
+ let from;
40160
+ let until;
40161
+ if ("date" in basis) from = until = basis.date;
40162
+ if ("month" in basis) {
40163
+ from = `${basis.month}-01`;
40164
+ const first = parseDateLocal(from);
40165
+ until = formatDateLocal(new Date(first.getFullYear(), first.getMonth() + 1, 0));
40166
+ }
40167
+ const periods = MCP_REGISTRY.rules.vat.periods.filter(
40168
+ (period) => !from || period.validFrom <= from && (!period.validUntil || until !== void 0 && period.validUntil >= until)
40169
+ );
40170
+ return [
40171
+ ...new Set(
40172
+ periods.flatMap((period) => [
40173
+ period.values.general,
40174
+ period.values.reduced,
40175
+ period.values.newspapers
40176
+ ])
40177
+ )
40178
+ ].sort((a, b) => b - a);
40179
+ }
40180
+ function purchaseVatRateError(value, category, rate) {
40181
+ const shape = purchaseVatBasisError(value, category);
40182
+ if (shape) return shape;
40183
+ if (!Number.isFinite(rate) || !purchaseVatRatesForBasis(value).includes(rate))
40184
+ return "ALV-kanta ei vastaa annettua perustetta tai sen ajankohtaa ei ole tuetussa verokantahistoriassa. Tarkista tosite ja kanta.";
40185
+ return null;
40186
+ }
40187
+
39718
40188
  // src/accounts.ts
39719
40189
  var ACCOUNTS = {
39720
40190
  // ── Assets (2xxx) ──
@@ -39760,7 +40230,21 @@ var ACCOUNTS = {
39760
40230
 
39761
40231
  // src/compliance/vat-guard.ts
39762
40232
  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.";
39763
- var ALV_REGISTERED_REVERSE_CHARGE_REFUSAL = "Reverse-charge purchases must be booked as a received invoice in the Kirjapro web app or as a manual Ostojen ALV row so fields 305/306/313/314 are filled. MCP support follows in a later release.";
40233
+ 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.";
40234
+ var REVERSE_CHARGE_CATEGORIES = [
40235
+ "eu_goods",
40236
+ "eu_services",
40237
+ "non_eu_services",
40238
+ "import",
40239
+ "construction",
40240
+ "scrap_metal"
40241
+ ];
40242
+ 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).";
40243
+ var REVERSE_CHARGE_RATE_REQUIRED = "A reverse-charge purchase needs the positive Finnish rate established by vatRateBasis, including applicable historical rates.";
40244
+ 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".';
40245
+ function purchaseTemplateBatchRefusal(indexes) {
40246
+ 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.`;
40247
+ }
39764
40248
  var NOT_VAT_REGISTERED_SETTLEMENT_REFUSAL = "This company is not VAT-registered (ALV), so it has no VAT settlement to book.";
39765
40249
  async function readCompanyVat(supabase2, companyId2) {
39766
40250
  const { data, error: error61 } = await supabase2.from("companies").select("alv_registered, vat_period").eq("id", companyId2).single();
@@ -39777,8 +40261,8 @@ async function readCompanyVat(supabase2, companyId2) {
39777
40261
  function splitVat(gross, vatRate) {
39778
40262
  if (vatRate === 0) return { net: roundCents(gross), vat: 0 };
39779
40263
  const net = roundCents(gross / (1 + vatRate / 100));
39780
- const vat = roundCents(gross - net);
39781
- return { net, vat };
40264
+ const vat2 = roundCents(gross - net);
40265
+ return { net, vat: vat2 };
39782
40266
  }
39783
40267
  function applyOverrides(entries, overrides) {
39784
40268
  if (!overrides) return entries;
@@ -39828,7 +40312,7 @@ async function validateAccountOverrides(overrides) {
39828
40312
  }
39829
40313
  function saleCashTemplate(revenueAccount, vatRate) {
39830
40314
  return (amount, opts) => {
39831
- const { net, vat } = splitVat(amount, vatRate);
40315
+ const { net, vat: vat2 } = splitVat(amount, vatRate);
39832
40316
  const entries = [
39833
40317
  {
39834
40318
  accountNumber: ACCOUNTS.KASSA,
@@ -39837,11 +40321,11 @@ function saleCashTemplate(revenueAccount, vatRate) {
39837
40321
  },
39838
40322
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
39839
40323
  ];
39840
- if (vat > 0) {
40324
+ if (vat2 > 0) {
39841
40325
  entries.push({
39842
40326
  accountNumber: ACCOUNTS.ALV_VELKA,
39843
40327
  debitAmount: 0,
39844
- creditAmount: vat
40328
+ creditAmount: vat2
39845
40329
  });
39846
40330
  }
39847
40331
  return applyOverrides(entries, opts?.accountOverrides);
@@ -39849,7 +40333,7 @@ function saleCashTemplate(revenueAccount, vatRate) {
39849
40333
  }
39850
40334
  function saleBankTemplate(revenueAccount, vatRate) {
39851
40335
  return (amount, opts) => {
39852
- const { net, vat } = splitVat(amount, vatRate);
40336
+ const { net, vat: vat2 } = splitVat(amount, vatRate);
39853
40337
  const entries = [
39854
40338
  {
39855
40339
  accountNumber: ACCOUNTS.PANKKITILI,
@@ -39858,11 +40342,11 @@ function saleBankTemplate(revenueAccount, vatRate) {
39858
40342
  },
39859
40343
  { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
39860
40344
  ];
39861
- if (vat > 0) {
40345
+ if (vat2 > 0) {
39862
40346
  entries.push({
39863
40347
  accountNumber: ACCOUNTS.ALV_VELKA,
39864
40348
  debitAmount: 0,
39865
- creditAmount: vat
40349
+ creditAmount: vat2
39866
40350
  });
39867
40351
  }
39868
40352
  return applyOverrides(entries, opts?.accountOverrides);
@@ -39927,7 +40411,7 @@ var TEMPLATES = [
39927
40411
  description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
39928
40412
  vatRate: 25.5,
39929
40413
  expand: (amount, opts) => {
39930
- const { net, vat } = splitVat(amount, 25.5);
40414
+ const { net, vat: vat2 } = splitVat(amount, 25.5);
39931
40415
  return applyOverrides(
39932
40416
  [
39933
40417
  {
@@ -39936,7 +40420,7 @@ var TEMPLATES = [
39936
40420
  creditAmount: 0
39937
40421
  },
39938
40422
  { accountNumber: ACCOUNTS.MYYNTI_25_5, debitAmount: 0, creditAmount: net },
39939
- { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat }
40423
+ { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }
39940
40424
  ],
39941
40425
  opts?.accountOverrides
39942
40426
  );
@@ -39949,11 +40433,11 @@ var TEMPLATES = [
39949
40433
  description: "Osto ALV 25,5% (pankkitililt\xE4)",
39950
40434
  vatRate: 25.5,
39951
40435
  expand: (amount, opts) => {
39952
- const { net, vat } = splitVat(amount, 25.5);
40436
+ const { net, vat: vat2 } = splitVat(amount, 25.5);
39953
40437
  return applyOverrides(
39954
40438
  [
39955
40439
  { accountNumber: ACCOUNTS.OSTOT, debitAmount: net, creditAmount: 0 },
39956
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40440
+ { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat2, creditAmount: 0 },
39957
40441
  {
39958
40442
  accountNumber: ACCOUNTS.PANKKITILI,
39959
40443
  debitAmount: 0,
@@ -39970,11 +40454,11 @@ var TEMPLATES = [
39970
40454
  description: "Ulkopuolinen palvelu ALV 25,5%",
39971
40455
  vatRate: 25.5,
39972
40456
  expand: (amount, opts) => {
39973
- const { net, vat } = splitVat(amount, 25.5);
40457
+ const { net, vat: vat2 } = splitVat(amount, 25.5);
39974
40458
  return applyOverrides(
39975
40459
  [
39976
40460
  { accountNumber: ACCOUNTS.OSTOT_PALVELUT, debitAmount: net, creditAmount: 0 },
39977
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40461
+ { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat2, creditAmount: 0 },
39978
40462
  {
39979
40463
  accountNumber: ACCOUNTS.PANKKITILI,
39980
40464
  debitAmount: 0,
@@ -40034,8 +40518,17 @@ var TEMPLATE_MAP = new Map(TEMPLATES.map((t) => [t.id, t]));
40034
40518
  function isSaleTemplate(templateId) {
40035
40519
  return TEMPLATE_MAP.get(templateId)?.kind === "sale";
40036
40520
  }
40521
+ function inputVatOf(entries) {
40522
+ return roundCents(
40523
+ entries.filter((e) => e.accountNumber === ACCOUNTS.ALV_SAAMISET).reduce((sum, e) => sum + e.debitAmount - e.creditAmount, 0)
40524
+ );
40525
+ }
40526
+ function isVatPurchaseTemplate(templateId) {
40527
+ const template = TEMPLATE_MAP.get(templateId);
40528
+ return template?.kind === "purchase" && inputVatOf(template.expand(100)) !== 0;
40529
+ }
40037
40530
  function availableTemplates(alvRegistered) {
40038
- return alvRegistered ? TEMPLATES.filter((t) => t.kind !== "sale") : TEMPLATES;
40531
+ return alvRegistered ? TEMPLATES.filter((t) => t.kind !== "sale") : TEMPLATES.filter((t) => !isVatPurchaseTemplate(t.id));
40039
40532
  }
40040
40533
  function expandTemplate(templateId, amount, opts) {
40041
40534
  const template = TEMPLATE_MAP.get(templateId);
@@ -40099,7 +40592,7 @@ function toJournalEntries(entries, accountMap) {
40099
40592
  credit_amount: e.creditAmount
40100
40593
  }));
40101
40594
  }
40102
- async function resolveAndCreate(params) {
40595
+ async function preparePosting(params) {
40103
40596
  const supabase2 = getSupabase();
40104
40597
  const companyId2 = getCompanyId();
40105
40598
  let fiscalYearId = params.fiscalYearId;
@@ -40128,16 +40621,49 @@ async function resolveAndCreate(params) {
40128
40621
  `Template produced unbalanced entries: debits ${totalDebit.toFixed(2)} != credits ${totalCredit.toFixed(2)}.`
40129
40622
  );
40130
40623
  }
40131
- const journalEntries = toJournalEntries(params.entries, accountResult);
40132
- const { data: transactionId, error: txError } = await supabase2.rpc(
40624
+ return { fiscalYearId, journalEntries: toJournalEntries(params.entries, accountResult) };
40625
+ }
40626
+ async function resolveAndCreatePurchase(params) {
40627
+ for (const row of params.vatRows) {
40628
+ const basisError = purchaseVatRateError(row.vat_rate_basis, row.category, row.vat_rate);
40629
+ if (basisError) return toErrorContent(basisError);
40630
+ }
40631
+ const capability = await getSupabase().rpc("purchase_vat_contract_version");
40632
+ if (capability.error || capability.data !== 1) return toErrorContent(
40633
+ 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."
40634
+ );
40635
+ const prepared = await preparePosting(params);
40636
+ if ("isError" in prepared) return prepared;
40637
+ const { data, error: error61 } = await getSupabase().rpc("book_purchase_with_vat", {
40638
+ p_company_id: getCompanyId(),
40639
+ p_fiscal_year_id: prepared.fiscalYearId,
40640
+ p_transaction_date: params.transactionDate,
40641
+ p_description: params.description,
40642
+ p_document_number: params.documentNumber || "",
40643
+ p_entries: prepared.journalEntries,
40644
+ p_vat_rows: params.vatRows,
40645
+ ...params.supplierName ? { p_supplier_name: params.supplierName } : {}
40646
+ });
40647
+ const result = data;
40648
+ if (error61 || !result?.transaction_id) {
40649
+ return toErrorContent(
40650
+ error61 ? sanitizeDbError("Failed to book the purchase", error61) : "Failed to book the purchase: no ID returned."
40651
+ );
40652
+ }
40653
+ return { id: result.transaction_id, vatEntryIds: result.vat_entry_ids ?? [] };
40654
+ }
40655
+ async function resolveAndCreate(params) {
40656
+ const prepared = await preparePosting(params);
40657
+ if ("isError" in prepared) return prepared;
40658
+ const { data: transactionId, error: txError } = await getSupabase().rpc(
40133
40659
  "create_transaction_with_entries",
40134
40660
  {
40135
- p_company_id: companyId2,
40136
- p_fiscal_year_id: fiscalYearId,
40661
+ p_company_id: getCompanyId(),
40662
+ p_fiscal_year_id: prepared.fiscalYearId,
40137
40663
  p_transaction_date: params.transactionDate,
40138
40664
  p_description: params.description,
40139
40665
  p_document_number: params.documentNumber || "",
40140
- p_entries: journalEntries
40666
+ p_entries: prepared.journalEntries
40141
40667
  }
40142
40668
  );
40143
40669
  if (txError || !transactionId) {
@@ -40151,7 +40677,7 @@ function registerTemplateTools(server2) {
40151
40677
  server2.registerTool(
40152
40678
  "kirjapro_list_templates",
40153
40679
  {
40154
- description: "List available transaction templates. Templates handle double-entry balancing and VAT splitting automatically. Sale templates are not listed for a VAT-registered (ALV) company: its sales are invoices.",
40680
+ description: "List available transaction templates. Templates handle double-entry balancing and VAT splitting automatically. Sale templates are not listed for a VAT-registered (ALV) company: its sales are invoices. Purchase templates with VAT are not listed for a company that is not VAT-registered.",
40155
40681
  inputSchema: {},
40156
40682
  outputSchema: {
40157
40683
  templates: external_exports.array(external_exports.object({
@@ -40176,7 +40702,7 @@ function registerTemplateTools(server2) {
40176
40702
  server2.registerTool(
40177
40703
  "kirjapro_create_from_template",
40178
40704
  {
40179
- description: 'Create a transaction from a template. Use kirjapro_list_templates for available templates. accountOverrides swaps default accounts (e.g. {"2320": "2330"}). Sale templates are refused for a VAT-registered (ALV) company: create an invoice instead.',
40705
+ description: 'Create a transaction from a template. Use kirjapro_list_templates for available templates. accountOverrides swaps default accounts (e.g. {"2320": "2330"}). Sale templates are refused for a VAT-registered (ALV) company: create an invoice instead. A purchase template with VAT (2320) books its VAT fact so the VAT reaches field 307; for a company that is not VAT-registered it is refused (book the purchase gross).',
40180
40706
  inputSchema: {
40181
40707
  template: external_exports.string().describe('Template ID (e.g. "cash_sale_25", "purchase_expense")'),
40182
40708
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
@@ -40184,6 +40710,7 @@ function registerTemplateTools(server2) {
40184
40710
  amount: external_exports.number().finite().positive().max(1e9).describe("Gross amount in EUR"),
40185
40711
  description: external_exports.string().min(1).max(500).describe("Transaction description"),
40186
40712
  documentNumber: external_exports.string().optional().describe("Document/receipt number"),
40713
+ vatRateBasis: PURCHASE_VAT_BASIS_SCHEMA.optional(),
40187
40714
  accountOverrides: external_exports.record(external_exports.string(), external_exports.string()).optional().describe('Account number swaps, e.g. {"2320": "2330"}'),
40188
40715
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
40189
40716
  },
@@ -40192,10 +40719,15 @@ function registerTemplateTools(server2) {
40192
40719
  async (params) => {
40193
40720
  const supabase2 = getSupabase();
40194
40721
  const companyId2 = getCompanyId();
40195
- if (isSaleTemplate(params.template)) {
40722
+ const template = TEMPLATE_MAP.get(params.template);
40723
+ let alvRegistered = null;
40724
+ if (template && (template.kind === "sale" || template.kind === "purchase")) {
40196
40725
  const companyVat = await readCompanyVat(supabase2, companyId2);
40197
40726
  if ("isError" in companyVat) return companyVat;
40198
- if (companyVat.alvRegistered) return toErrorContent(ALV_REGISTERED_SALE_REFUSAL);
40727
+ alvRegistered = companyVat.alvRegistered;
40728
+ if (template.kind === "sale" && alvRegistered) {
40729
+ return toErrorContent(ALV_REGISTERED_SALE_REFUSAL);
40730
+ }
40199
40731
  }
40200
40732
  return withIdempotency(
40201
40733
  supabase2,
@@ -40214,7 +40746,29 @@ function registerTemplateTools(server2) {
40214
40746
  if ("error" in result) {
40215
40747
  return toErrorContent(result.error);
40216
40748
  }
40217
- const createResult = await resolveAndCreate({
40749
+ const inputVat = template?.kind === "purchase" ? inputVatOf(result.entries) : 0;
40750
+ const recordsVat = template?.kind === "purchase" && inputVatOf(template.expand(100, { accountOverrides: params.accountOverrides })) !== 0;
40751
+ if (recordsVat && alvRegistered === false) {
40752
+ return toErrorContent(PURCHASE_TEMPLATE_NOT_VAT_REGISTERED_REFUSAL);
40753
+ }
40754
+ const createResult = recordsVat ? await resolveAndCreatePurchase({
40755
+ fiscalYearId: params.fiscalYearId,
40756
+ transactionDate: params.transactionDate,
40757
+ description: params.description,
40758
+ documentNumber: params.documentNumber,
40759
+ entries: result.entries,
40760
+ vatRows: [
40761
+ {
40762
+ category: "domestic",
40763
+ vat_rate_basis: params.vatRateBasis,
40764
+ vat_rate: template?.vatRate ?? 0,
40765
+ tax_base: roundCents(
40766
+ result.entries.filter((e) => e.accountNumber !== ACCOUNTS.ALV_SAAMISET).reduce((sum, e) => sum + e.debitAmount, 0)
40767
+ ),
40768
+ vat_amount: inputVat
40769
+ }
40770
+ ]
40771
+ }) : await resolveAndCreate({
40218
40772
  fiscalYearId: params.fiscalYearId,
40219
40773
  transactionDate: params.transactionDate,
40220
40774
  description: params.description,
@@ -40228,7 +40782,8 @@ function registerTemplateTools(server2) {
40228
40782
  message: "Transaction created from template",
40229
40783
  template: params.template,
40230
40784
  transaction_id: createResult.id,
40231
- entries: result.entries
40785
+ entries: result.entries,
40786
+ ..."vatEntryIds" in createResult && { vat_entry_ids: createResult.vatEntryIds }
40232
40787
  });
40233
40788
  }
40234
40789
  );
@@ -40237,7 +40792,7 @@ function registerTemplateTools(server2) {
40237
40792
  server2.registerTool(
40238
40793
  "kirjapro_create_from_template_batch",
40239
40794
  {
40240
- description: "Preview or atomically create multiple template transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable. A batch with a sale template is refused for a VAT-registered (ALV) company before any write.",
40795
+ description: "Preview or atomically create multiple template transactions in one call (up to 50). dryRun defaults to true. Commit is always all-or-nothing; partial/best-effort writes are intentionally unavailable. A batch with a sale template is refused for a VAT-registered (ALV) company before any write. Purchase templates with VAT (2320) are refused in a batch: book them one at a time with kirjapro_create_from_template so the VAT reaches the ALV report.",
40241
40796
  inputSchema: {
40242
40797
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
40243
40798
  dryRun: external_exports.boolean().default(true).describe(
@@ -40260,6 +40815,14 @@ function registerTemplateTools(server2) {
40260
40815
  async (params) => {
40261
40816
  const supabase2 = getSupabase();
40262
40817
  const companyId2 = getCompanyId();
40818
+ const vatPurchaseIndexes = params.transactions.map((tx, index) => {
40819
+ const template = TEMPLATE_MAP.get(tx.template);
40820
+ if (template?.kind !== "purchase") return -1;
40821
+ return inputVatOf(template.expand(100, { accountOverrides: tx.accountOverrides })) !== 0 ? index : -1;
40822
+ }).filter((index) => index >= 0);
40823
+ if (vatPurchaseIndexes.length > 0) {
40824
+ return toErrorContent(purchaseTemplateBatchRefusal(vatPurchaseIndexes));
40825
+ }
40263
40826
  const saleIndexes = params.transactions.map((tx, index) => isSaleTemplate(tx.template) ? index : -1).filter((index) => index >= 0);
40264
40827
  if (saleIndexes.length > 0) {
40265
40828
  const companyVat = await readCompanyVat(supabase2, companyId2);
@@ -40449,49 +41012,98 @@ function registerTemplateTools(server2) {
40449
41012
  );
40450
41013
  }
40451
41014
 
41015
+ // src/tools/bookings.ts
41016
+ import { randomUUID as randomUUID2 } from "node:crypto";
41017
+
41018
+ // src/compliance/purchase-vat-allocation.ts
41019
+ function allocatePurchaseVat(net, vat2, deductionPercent) {
41020
+ if (![net, vat2, deductionPercent].every(Number.isFinite) || net < 0 || vat2 < 0 || deductionPercent < 0 || deductionPercent > 100) {
41021
+ throw new Error("V\xE4hennysosuuden on oltava 0\u2013100 % ja ostosummien v\xE4hint\xE4\xE4n nolla.");
41022
+ }
41023
+ const netCents = Math.round(net * 100);
41024
+ const vatCents = Math.round(vat2 * 100);
41025
+ const deductibleVatCents = Math.round(vatCents * deductionPercent / 100 + 1e-9);
41026
+ const deductibleNetCents = Math.round(netCents * deductionPercent / 100 + 1e-9);
41027
+ return {
41028
+ deductibleVat: deductibleVatCents / 100,
41029
+ nonDeductibleVat: (vatCents - deductibleVatCents) / 100,
41030
+ deductibleNet: deductibleNetCents / 100,
41031
+ nonDeductibleNet: (netCents - deductibleNetCents) / 100,
41032
+ expense: (netCents + vatCents - deductibleVatCents) / 100,
41033
+ gross: (netCents + vatCents) / 100
41034
+ };
41035
+ }
41036
+
40452
41037
  // src/compliance/rates.ts
41038
+ function periodForRule(rule, date5) {
41039
+ const periods = MCP_REGISTRY.rules[rule].periods;
41040
+ const period = periods.find(
41041
+ (candidate) => candidate.validFrom <= date5 && (!candidate.validUntil || candidate.validUntil >= date5)
41042
+ );
41043
+ if (!period) throw new Error(`No verified ${rule} rule for ${date5}`);
41044
+ return period;
41045
+ }
41046
+ var vat = periodForRule("vat", "2026-01-01").values;
41047
+ var payrollPeriod = periodForRule("payroll", "2026-01-01");
41048
+ var payroll = payrollPeriod.values;
41049
+ var estimates = periodForRule("payrollEstimates", "2026-01-01").values;
41050
+ var total = (employer) => Math.round((employer + payroll.tyelEmployee) * 100) / 100;
40453
41051
  var FINNISH_RATES_2026 = {
40454
41052
  year: 2026,
40455
- validFrom: "2026-01-01",
40456
- validUntil: "2026-12-31",
40457
- sourcesVerifiedAt: "2026-03-15",
40458
- source: "AVL 1501/1993, ETK 2026, Elo 2026, vero.fi 2026",
40459
- vat: {
40460
- standard: 25.5,
40461
- reduced: 13.5,
40462
- secondReduced: 10,
40463
- zero: 0
40464
- },
41053
+ validFrom: payrollPeriod.validFrom,
41054
+ validUntil: payrollPeriod.validUntil,
41055
+ vat: { standard: vat.general, reduced: vat.reduced, secondReduced: vat.newspapers, zero: vat.zero },
40465
41056
  social: {
40466
- tyelEmployee: 7.3,
40467
- tyelEmployer: 17.55,
40468
- tyelEmployerAverage: 17.1,
40469
- tyelKeskimaarainen: 24.4,
40470
- tyelPerusmaksu: 24.85,
40471
- sotu: 1.91,
40472
- ttvEmployer: 0.31,
40473
- ttvEmployerLarge: 1.23,
40474
- ttvEmployerLargeThreshold: 2509500,
40475
- ttvEmployee: 0.89,
40476
- tapaturmaAverage: 0.51,
40477
- ryhmahenki: 0.06
40478
- }
40479
- };
40480
- function freshnessMeta(rates = FINNISH_RATES_2026, today = /* @__PURE__ */ new Date()) {
40481
- const validUntil = /* @__PURE__ */ new Date(`${rates.validUntil}T23:59:59Z`);
40482
- const msPerDay = 864e5;
40483
- const daysUntilExpiry = Math.ceil(
40484
- (validUntil.getTime() - today.getTime()) / msPerDay
40485
- );
41057
+ tyelEmployee: payroll.tyelEmployee,
41058
+ tyelEmployer: estimates.tyelBaseEmployer,
41059
+ tyelEmployerAverage: estimates.tyelAverageEmployer,
41060
+ tyelKeskimaarainen: total(estimates.tyelAverageEmployer),
41061
+ tyelPerusmaksu: total(estimates.tyelBaseEmployer),
41062
+ tyelTemporary: total(estimates.tyelTemporaryEmployer),
41063
+ sotu: payroll.health,
41064
+ ttvEmployer: payroll.unemploymentLower,
41065
+ ttvEmployerLarge: payroll.unemploymentUpper,
41066
+ ttvEmployerLargeThreshold: payroll.unemploymentThreshold,
41067
+ ttvEmployee: payroll.unemploymentEmployee,
41068
+ tapaturmaAverage: estimates.accidentAverage,
41069
+ ryhmahenki: estimates.groupLife
41070
+ }
41071
+ };
41072
+ function regulatoryFreshness(rule, today = /* @__PURE__ */ new Date()) {
41073
+ const period = periodForRule(rule, "2026-01-01");
41074
+ const day = helsinkiDateString(today);
41075
+ const expired = period.validUntil !== null && day > period.validUntil;
40486
41076
  return {
40487
- published_at: rates.sourcesVerifiedAt,
40488
- valid_from: rates.validFrom,
40489
- valid_through: rates.validUntil,
40490
- sources_verified_at: rates.sourcesVerifiedAt,
40491
- stale: daysUntilExpiry < 0,
40492
- days_until_expiry: daysUntilExpiry
41077
+ rule,
41078
+ kind: MCP_REGISTRY.rules[rule].kind,
41079
+ conditions: MCP_REGISTRY.rules[rule].conditions,
41080
+ coverage_meaning: "Bundled registry coverage interval, not necessarily enactment or first legal effective date; check event/fiscal-year applicability separately",
41081
+ valid_from: period.validFrom,
41082
+ valid_through: period.validUntil,
41083
+ sources_verified_at: period.reviewedOn,
41084
+ review_due: period.reviewDue,
41085
+ review_overdue: day > period.reviewDue,
41086
+ stale: expired || day > period.reviewDue || day < period.validFrom,
41087
+ expired,
41088
+ not_yet_effective: day < period.validFrom,
41089
+ days_until_expiry: period.validUntil === null ? null : Math.round(
41090
+ (Date.parse(`${period.validUntil}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) / 864e5
41091
+ ),
41092
+ sources: period.sourceIds.map((id) => ({
41093
+ id,
41094
+ ...MCP_REGISTRY.sources[id]
41095
+ }))
40493
41096
  };
40494
41097
  }
41098
+ var VAT_RATE_HISTORY = MCP_REGISTRY.rules.vat.periods.map((period) => ({
41099
+ valid_from: period.validFrom,
41100
+ valid_through: period.validUntil,
41101
+ standard: period.values.general,
41102
+ reduced: period.values.reduced,
41103
+ second_reduced: period.values.newspapers,
41104
+ 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."
41105
+ }));
41106
+ var COMPANY_SIZE_RULE = periodForRule("companySizes", "2026-01-01");
40495
41107
 
40496
41108
  // src/compliance/vat-settlement-date.ts
40497
41109
  var pad = (value) => String(value).padStart(2, "0");
@@ -40526,14 +41138,14 @@ function defaultSettlementDate(month, vatPeriod) {
40526
41138
  function expenseAccountWarning(account) {
40527
41139
  const first = account.trim().charAt(0);
40528
41140
  if (first && !"6789".includes(first)) {
40529
- return `expenseAccount "${account}" is outside the YSE 2017 expense range (6xxx materials/services, 7xxx\u20139xxx expenses). Verify it is really an expense account \u2014 booking a purchase to a non-expense account misclassifies the income statement.`;
41141
+ 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.`;
40530
41142
  }
40531
41143
  return null;
40532
41144
  }
40533
41145
  function revenueAccountWarning(account) {
40534
41146
  const first = account.trim().charAt(0);
40535
41147
  if (first && first !== "5") {
40536
- return `revenueAccount "${account}" is outside the YSE 2017 revenue range (5xxx Liikevaihto). Verify it is really a revenue account \u2014 booking a sale to a non-revenue account misclassifies the income statement.`;
41148
+ 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.`;
40537
41149
  }
40538
41150
  return null;
40539
41151
  }
@@ -40548,17 +41160,24 @@ function registerBookingTools(server2) {
40548
41160
  server2.registerTool(
40549
41161
  "kirjapro_book_purchase",
40550
41162
  {
40551
- description: "Book a purchase (osto) with automatic VAT splitting. Supports reverse charge (AVL 9\xA7) for companies that are not VAT-registered and entertainment (edustus, AVL 114\xA7). For a VAT-registered (ALV) company reverseCharge is refused: book it as a received invoice in the web app. Rate rules: food/restaurants 13.5%, insurance 0% (exempt), alcohol 25.5%.",
41163
+ 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%.",
40552
41164
  inputSchema: {
40553
41165
  amount: external_exports.number().finite().positive().max(1e9).describe(
40554
- "EUR amount. Standard purchase: GROSS (VAT-inclusive) \u2014 VAT is split out of this. Reverse charge (reverseCharge=true): NET foreign-invoice amount \u2014 the foreign supplier does not charge Finnish VAT, so the parameter must be net. VAT is self-assessed on top per AVL 9\xA7 producing equal debit to ALV-saamiset and credit to ALV-velka. Entertainment (edustus=true): GROSS (full amount to expense, no VAT deduction per AVL 114\xA7)."
41166
+ "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."
40555
41167
  ),
40556
41168
  description: external_exports.string().min(1).max(500).describe("Transaction description"),
40557
- date: ISO_DATE_SCHEMA.describe("Transaction date (YYYY-MM-DD)"),
40558
- vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).describe("Finnish VAT rate"),
41169
+ 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."),
41170
+ vatRate: external_exports.enum(["25.5", "13.5", "10", "0", "24", "14"]).describe("Finnish VAT rate on the source, including historical 24/14"),
41171
+ vatRateBasis: PURCHASE_VAT_BASIS_SCHEMA.optional(),
41172
+ 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."),
41173
+ 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."),
40559
41174
  expenseAccount: external_exports.string().describe('Expense account number (e.g. "8080")'),
40560
- reverseCharge: external_exports.boolean().default(false).describe(`EU reverse charge: D expense + D ${ACCOUNTS.ALV_SAAMISET} ALV-saamiset + C ${ACCOUNTS.ALV_VELKA} ALV-velka + C bank`),
40561
- edustus: external_exports.boolean().default(false).describe("Entertainment expense (AVL 114\xA7): 0% VAT deduction, full gross to expense"),
41175
+ 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`),
41176
+ reverseChargeCategory: external_exports.enum(REVERSE_CHARGE_CATEGORIES).optional().describe(
41177
+ "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)"
41178
+ ),
41179
+ supplierName: external_exports.string().max(200).optional().describe("Supplier name, stored on the VAT fact"),
41180
+ 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"),
40562
41181
  paymentAccount: external_exports.string().default(ACCOUNTS.PANKKITILI).describe(`Payment account (default "${ACCOUNTS.PANKKITILI}" Pankkitili)`),
40563
41182
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
40564
41183
  documentNumber: external_exports.string().optional().describe("Document/receipt number"),
@@ -40568,76 +41187,109 @@ function registerBookingTools(server2) {
40568
41187
  annotations: WRITE_ANNOTATIONS
40569
41188
  },
40570
41189
  async (params) => {
40571
- const rate = Number.parseFloat(params.vatRate);
40572
- const gross = roundCents(params.amount);
40573
- let entries;
40574
- if (params.reverseCharge && !params.edustus) {
40575
- const vat = await readCompanyVat(getSupabase(), getCompanyId());
40576
- if ("isError" in vat) return vat;
40577
- if (vat.alvRegistered) return toErrorContent(ALV_REGISTERED_REVERSE_CHARGE_REFUSAL);
40578
- }
40579
- if (params.edustus) {
40580
- entries = [
40581
- { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
40582
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40583
- ];
40584
- } else if (params.reverseCharge) {
40585
- const vat = roundCents(gross * rate / 100);
40586
- entries = [
40587
- { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
40588
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40589
- // ALV-saamiset (input)
40590
- { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat },
40591
- // ALV-velka (output)
40592
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40593
- ];
40594
- } else if (rate === 0) {
40595
- entries = [
40596
- { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
40597
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40598
- ];
40599
- } else {
40600
- const { net, vat } = splitVat(gross, rate);
40601
- entries = [
40602
- { accountNumber: params.expenseAccount, debitAmount: net, creditAmount: 0 },
40603
- { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: vat, creditAmount: 0 },
40604
- { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
40605
- ];
40606
- }
40607
- const purchaseAccountWarning = expenseAccountWarning(params.expenseAccount);
40608
- if (params.dryRun) {
40609
- return toDryRunResponse("kirjapro_book_purchase", {
41190
+ const execute = async () => {
41191
+ const rate = Number(params.vatRate);
41192
+ const amount = roundCents(params.amount);
41193
+ if (amount <= 0) return toErrorContent("Oston summan on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41194
+ const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
41195
+ if ("isError" in companyVat) return companyVat;
41196
+ if (params.reverseCharge && !params.reverseChargeCategory) return toErrorContent(REVERSE_CHARGE_CATEGORY_REQUIRED);
41197
+ if (params.reverseCharge && rate === 0) return toErrorContent(REVERSE_CHARGE_RATE_REQUIRED);
41198
+ const category = params.reverseCharge ? params.reverseChargeCategory : "domestic";
41199
+ const assessmentOnly = category === "import";
41200
+ const percent = params.edustus || !companyVat.alvRegistered ? 0 : params.deductionPercent ?? 100;
41201
+ if ((params.edustus || !companyVat.alvRegistered) && (params.deductionPercent ?? 0) > 0)
41202
+ return toErrorContent("Edustuksesta tai rekister\xF6im\xE4tt\xF6m\xE4n yrityksen ostosta ei tehd\xE4 ALV-v\xE4hennyst\xE4. Korjaa v\xE4hennysosuus nollaksi.");
41203
+ if (!companyVat.alvRegistered && params.reverseCharge && !["eu_services", "non_eu_services"].includes(category))
41204
+ return toErrorContent(NOT_VAT_REGISTERED_PURCHASE_REFUSAL);
41205
+ const reason = params.edustus ? "Edustuskulu, AVL 114 \xA7 1 mom. 3 kohta" : !companyVat.alvRegistered ? "Yritys ei ole arvonlis\xE4verovelvollinen" : params.deductionReason;
41206
+ if (rate > 0 && percent < 100 && !reason)
41207
+ return toErrorContent("Kirjoita v\xE4hennysosuuden jakoperuste. Yksityiskulut erotetaan erikseen.");
41208
+ if (rate > 0) {
41209
+ const basisError = purchaseVatRateError(params.vatRateBasis, category, rate);
41210
+ if (basisError) return toErrorContent(basisError);
41211
+ }
41212
+ const basis = params.vatRateBasis;
41213
+ const { net, vat: vat2 } = params.reverseCharge ? { net: amount, vat: roundCents(amount * rate / 100) } : splitVat(amount, rate);
41214
+ const allocation = allocatePurchaseVat(net, vat2, percent);
41215
+ const keepFacts = rate > 0 && (params.reverseCharge || companyVat.alvRegistered && !params.edustus);
41216
+ const vatAllocation = percent > 0 && percent < 100 ? {
41217
+ version: 1,
41218
+ group_id: randomUUID2(),
41219
+ net,
41220
+ vat: vat2,
41221
+ deduction_percent: percent,
41222
+ reason
41223
+ } : void 0;
41224
+ const fact = {
41225
+ category,
41226
+ vat_rate: rate,
41227
+ vat_rate_basis: basis,
41228
+ ...vatAllocation ? { vat_allocation: vatAllocation } : {}
41229
+ };
41230
+ const vatRows = keepFacts ? [
41231
+ ...percent > 0 ? [{ ...fact, tax_base: allocation.deductibleNet, vat_amount: allocation.deductibleVat, is_deductible: true }] : [],
41232
+ ...percent < 100 ? [{ ...fact, tax_base: allocation.nonDeductibleNet, vat_amount: allocation.nonDeductibleVat, is_deductible: false, non_deductible_reason: reason }] : []
41233
+ ].filter((row) => row.tax_base !== 0 || row.vat_amount !== 0) : [];
41234
+ const expense = assessmentOnly ? allocation.nonDeductibleVat : allocation.expense;
41235
+ const entries = [
41236
+ { accountNumber: params.expenseAccount, debitAmount: expense, creditAmount: 0 },
41237
+ { accountNumber: ACCOUNTS.ALV_SAAMISET, debitAmount: allocation.deductibleVat, creditAmount: 0 },
41238
+ ...params.reverseCharge ? [{ accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }] : [],
41239
+ ...!assessmentOnly ? [{ accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: params.reverseCharge ? net : amount }] : []
41240
+ ].filter((entry) => entry.debitAmount !== 0 || entry.creditAmount !== 0);
41241
+ if (entries.length < 2) return toErrorContent("Tositteen veron on oltava v\xE4hint\xE4\xE4n yksi sentti.");
41242
+ const purchaseAccountWarning = expenseAccountWarning(params.expenseAccount);
41243
+ const calculation = {
41244
+ amount_basis: assessmentOnly ? "customs_tax_base" : params.reverseCharge ? "supplier_net" : "supplier_gross",
41245
+ ...assessmentOnly ? {} : { supplier_amount: params.reverseCharge ? net : amount },
41246
+ tax_base: net,
41247
+ output_vat: params.reverseCharge ? vat2 : 0,
41248
+ source_vat: vat2,
41249
+ deductible_vat: allocation.deductibleVat,
41250
+ expense_total: expense,
41251
+ deduction_percent: percent,
41252
+ ...reason ? { deduction_reason: reason } : {}
41253
+ };
41254
+ if (params.dryRun) return toDryRunResponse("kirjapro_book_purchase", {
40610
41255
  action: "book_purchase",
40611
- gross,
41256
+ gross: amount,
40612
41257
  vat_rate: params.vatRate,
40613
41258
  description: params.description,
40614
41259
  date: params.date,
40615
41260
  entries,
40616
- ...purchaseAccountWarning && { account_warning: purchaseAccountWarning }
41261
+ calculation,
41262
+ ...params.reverseCharge ? { reverse_charge: calculation } : {},
41263
+ ...vatRows.length ? { vat_rows: vatRows } : {},
41264
+ ...purchaseAccountWarning ? { account_warning: purchaseAccountWarning } : {}
40617
41265
  });
40618
- }
41266
+ const posting = {
41267
+ fiscalYearId: params.fiscalYearId,
41268
+ transactionDate: params.date,
41269
+ description: params.description,
41270
+ documentNumber: params.documentNumber,
41271
+ entries
41272
+ };
41273
+ const result = vatRows.length ? await resolveAndCreatePurchase({ ...posting, vatRows, supplierName: params.supplierName }) : await resolveAndCreate(posting);
41274
+ if ("isError" in result) return result;
41275
+ return toResult({
41276
+ message: assessmentOnly ? "Customs VAT assessment booked" : "Purchase booked",
41277
+ transaction_id: result.id,
41278
+ entries,
41279
+ calculation,
41280
+ ...params.reverseCharge ? { reverse_charge: calculation } : {},
41281
+ ..."vatEntryIds" in result ? { vat_entry_ids: result.vatEntryIds } : {},
41282
+ ...purchaseAccountWarning ? { warning: purchaseAccountWarning } : {}
41283
+ });
41284
+ };
41285
+ if (params.dryRun) return execute();
40619
41286
  return withIdempotency(
40620
41287
  getSupabase(),
40621
41288
  getCompanyId(),
40622
41289
  "kirjapro_book_purchase",
40623
41290
  params.idempotencyKey,
40624
41291
  params,
40625
- async () => {
40626
- const result = await resolveAndCreate({
40627
- fiscalYearId: params.fiscalYearId,
40628
- transactionDate: params.date,
40629
- description: params.description,
40630
- documentNumber: params.documentNumber,
40631
- entries
40632
- });
40633
- if ("isError" in result) return result;
40634
- return toResult({
40635
- message: "Purchase booked",
40636
- transaction_id: result.id,
40637
- entries,
40638
- ...purchaseAccountWarning && { warning: purchaseAccountWarning }
40639
- });
40640
- }
41292
+ execute
40641
41293
  );
40642
41294
  }
40643
41295
  );
@@ -40791,21 +41443,21 @@ function registerBookingTools(server2) {
40791
41443
  { accountNumber: params.revenueAccount, debitAmount: 0, creditAmount: gross }
40792
41444
  ];
40793
41445
  } else {
40794
- const { net: net2, vat: vat2 } = splitVat(gross, rate);
41446
+ const { net: net2, vat: vat3 } = splitVat(gross, rate);
40795
41447
  entries = [
40796
41448
  { accountNumber: debitAccount, debitAmount: gross, creditAmount: 0 },
40797
41449
  { accountNumber: params.revenueAccount, debitAmount: 0, creditAmount: net2 },
40798
- { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat2 }
41450
+ { accountNumber: ACCOUNTS.ALV_VELKA, debitAmount: 0, creditAmount: vat3 }
40799
41451
  ];
40800
41452
  }
40801
- const { net, vat } = rate > 0 ? splitVat(gross, rate) : { net: gross, vat: 0 };
41453
+ const { net, vat: vat2 } = rate > 0 ? splitVat(gross, rate) : { net: gross, vat: 0 };
40802
41454
  const saleAccountWarning = revenueAccountWarning(params.revenueAccount);
40803
41455
  if (params.dryRun) {
40804
41456
  return toDryRunResponse("kirjapro_book_sale", {
40805
41457
  action: "book_sale",
40806
41458
  gross,
40807
41459
  net,
40808
- vat,
41460
+ vat: vat2,
40809
41461
  vat_rate: `${params.vatRate} %`,
40810
41462
  payment_type: params.paymentType,
40811
41463
  debit_account: debitAccount,
@@ -40835,7 +41487,7 @@ function registerBookingTools(server2) {
40835
41487
  transaction_id: result.id,
40836
41488
  gross,
40837
41489
  net,
40838
- vat,
41490
+ vat: vat2,
40839
41491
  vat_rate: `${params.vatRate} %`,
40840
41492
  payment_type: params.paymentType,
40841
41493
  debit_account: debitAccount,
@@ -40970,6 +41622,7 @@ function registerOfferTools(server2) {
40970
41622
  "reverse_scrap",
40971
41623
  "reverse_eu_service",
40972
41624
  "export_outside_eu",
41625
+ "service_outside_eu",
40973
41626
  "intra_eu_goods",
40974
41627
  "exempt_healthcare",
40975
41628
  "exempt_social_welfare",
@@ -40977,7 +41630,7 @@ function registerOfferTools(server2) {
40977
41630
  "exempt_financial",
40978
41631
  "exempt_insurance",
40979
41632
  "exempt_other"
40980
- ]).optional().describe("Structured VAT treatment. Required when VAT rate is 0.")
41633
+ ]).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.")
40981
41634
  })).min(1).max(MAX_DOCUMENT_LINES).describe(`Line items (max ${MAX_DOCUMENT_LINES})`),
40982
41635
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
40983
41636
  },
@@ -41046,7 +41699,7 @@ function registerOfferTools(server2) {
41046
41699
  const enteredTotal = roundCents(line.quantity * line.unitPrice);
41047
41700
  const vatAmount = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(enteredTotal * vatRate / (100 + vatRate)) : roundCents(enteredTotal * (vatRate / 100));
41048
41701
  const subtotal2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(enteredTotal - vatAmount) : enteredTotal;
41049
- const total2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? enteredTotal : roundCents(subtotal2 + vatAmount);
41702
+ const total3 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? enteredTotal : roundCents(subtotal2 + vatAmount);
41050
41703
  return {
41051
41704
  line_number: i + 1,
41052
41705
  description: line.description,
@@ -41064,12 +41717,12 @@ function registerOfferTools(server2) {
41064
41717
  eu_goods_transport_confirmed: params.euGoodsTransportConfirmed,
41065
41718
  subtotal: subtotal2,
41066
41719
  vat_amount: vatAmount,
41067
- total: total2
41720
+ total: total3
41068
41721
  };
41069
41722
  });
41070
41723
  const subtotal = roundCents(calculatedLines.reduce((s, l) => s + l.subtotal, 0));
41071
41724
  const vatTotal = roundCents(calculatedLines.reduce((s, l) => s + l.vat_amount, 0));
41072
- const total = roundCents(subtotal + vatTotal);
41725
+ const total2 = roundCents(subtotal + vatTotal);
41073
41726
  const { data: offerId, error: rpcError } = await supabase2.rpc("create_offer_with_lines", {
41074
41727
  p_company_id: companyId2,
41075
41728
  p_customer_id: params.customerId || null,
@@ -41083,7 +41736,7 @@ function registerOfferTools(server2) {
41083
41736
  p_notes: params.notes || null,
41084
41737
  p_subtotal: subtotal,
41085
41738
  p_vat_total: vatTotal,
41086
- p_total: total,
41739
+ p_total: total2,
41087
41740
  p_lines: calculatedLines
41088
41741
  });
41089
41742
  if (rpcError) {
@@ -41097,7 +41750,7 @@ function registerOfferTools(server2) {
41097
41750
  title: params.title,
41098
41751
  subtotal,
41099
41752
  vat_total: vatTotal,
41100
- total,
41753
+ total: total2,
41101
41754
  lines: calculatedLines
41102
41755
  });
41103
41756
  }
@@ -42159,7 +42812,7 @@ var VAT_RATE_METADATA = [
42159
42812
  { 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" },
42160
42813
  { rate: FINNISH_RATES_2026.vat.secondReduced, label: "Lehdet ja aikakauslehdet", description: "Newspapers and magazines (print and digital) only", basis: "AVL 85a\xA7" },
42161
42814
  { 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" },
42162
- { rate: null, label: "Verovapaat myynnit", description: "Healthcare (AVL 34-36\xA7), education (AVL 39-40\xA7), financial services (AVL 41-42\xA7), insurance (AVL 44\xA7). NO input VAT deduction", basis: "AVL 34-44\xA7" }
42815
+ { 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" }
42163
42816
  ];
42164
42817
  function registerSchemaResources(server2) {
42165
42818
  server2.registerResource(
@@ -42181,7 +42834,7 @@ function registerSchemaResources(server2) {
42181
42834
  "vat-rates",
42182
42835
  "kirjapro://vat-rates",
42183
42836
  {
42184
- description: "Finnish VAT rates (2026). Rates: 25.5% (general), 13.5% (food/restaurants/books/medicine/transport/accommodation/sports), 10% (newspapers/magazines only), 0% (exports/vessels \u2014 input VAT deductible), exempt (healthcare/education/finance/insurance \u2014 NO input VAT deduction).",
42837
+ 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).",
42185
42838
  mimeType: "application/json"
42186
42839
  },
42187
42840
  async (uri) => ({
@@ -42190,7 +42843,7 @@ function registerSchemaResources(server2) {
42190
42843
  mimeType: "application/json",
42191
42844
  text: JSON.stringify({
42192
42845
  rates: VAT_RATE_METADATA,
42193
- _meta: freshnessMeta(FINNISH_RATES_2026)
42846
+ _meta: regulatoryFreshness("vat")
42194
42847
  })
42195
42848
  }]
42196
42849
  })
@@ -42271,6 +42924,9 @@ function registerCompanyResources(server2) {
42271
42924
  }
42272
42925
 
42273
42926
  // src/resources/compliance.ts
42927
+ var exampleOutputVat = FINNISH_RATES_2026.vat.standard;
42928
+ var exampleDeductibleVat = roundCents(exampleOutputVat * 0.6);
42929
+ var exampleNonDeductibleVat = roundCents(exampleOutputVat - exampleDeductibleVat);
42274
42930
  var VAT_COMPLIANCE = {
42275
42931
  rates: {
42276
42932
  current_year: FINNISH_RATES_2026.year,
@@ -42280,40 +42936,40 @@ var VAT_COMPLIANCE = {
42280
42936
  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" },
42281
42937
  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)" }
42282
42938
  },
42283
- rate_history: [
42284
- { period: "1.1.2026 \u2192", standard: FINNISH_RATES_2026.vat.standard, reduced: FINNISH_RATES_2026.vat.reduced, second_reduced: FINNISH_RATES_2026.vat.secondReduced, note: "Reduced rate unified from 14% \u2192 13.5%" },
42285
- { period: "1.9.2024 \u2192 31.12.2025", standard: 25.5, reduced: 14, second_reduced: 10, note: "Standard raised from 24% \u2192 25.5%" },
42286
- { period: "1.1.2013 \u2192 31.8.2024", standard: 24, reduced: 14, second_reduced: 10 }
42287
- ],
42939
+ rate_history: VAT_RATE_HISTORY,
42940
+ 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.",
42941
+ 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.",
42288
42942
  deduction_rules: [
42289
- { type: "Edustuskulut (entertainment)", vat_deduction: "0%", income_tax: "50%", basis: "AVL 114\xA7", booking: "Full gross to expense, NO ALV-saamiset entry" },
42290
- { type: "Neuvottelukulut (meetings)", vat_deduction: "100%", income_tax: "100%", basis: "AVL 102\xA7", booking: "Normal VAT split" },
42291
- { type: "Henkil\xF6st\xF6kulut (staff events)", vat_deduction: "100%", income_tax: "100%", basis: "AVL 102\xA7", booking: "Normal VAT split" },
42292
- { type: "Insurance premiums", vat_deduction: "0% (exempt)", income_tax: "100%", basis: "AVL 44\xA7", booking: "Full gross to expense, no VAT (vakuutusmaksuvero is separate)" },
42293
- { type: "Healthcare (staff)", vat_deduction: "0% (exempt)", income_tax: "100%", basis: "AVL 34-36\xA7" },
42294
- { type: "Financial services", vat_deduction: "0% (exempt)", income_tax: "100%", basis: "AVL 41-42\xA7" },
42295
- { type: "Henkil\xF6auto (passenger car M1)", vat_deduction: "100% if exclusive business use, 0% if ANY private use (incl. commuting)", income_tax: "Proportional", basis: "AVL 114\xA7 1 mom 4 kohta", booking: "Ajop\xE4iv\xE4kirja (mileage logbook) required as proof" },
42296
- { type: "Pakettiauto/kuorma-auto (van N1+)", vat_deduction: "Proportional to business use", income_tax: "Proportional", basis: "AVL 102\xA7, 117\xA7" },
42297
- { type: "Markkinointitilaisuudet (marketing events)", vat_deduction: "100%", income_tax: "100%", basis: "AVL 102\xA7, KHO 2013:63", booking: "Product launches, trade shows \u2014 NOT client entertainment" }
42943
+ { 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" },
42944
+ { 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" },
42945
+ { 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" },
42946
+ { 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" },
42947
+ { 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" },
42948
+ { 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" },
42949
+ { 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" },
42950
+ { 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" },
42951
+ { 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" }
42298
42952
  ],
42299
42953
  reverse_charge: {
42300
- rule: "Buyer self-assesses both output and input VAT. Applies to: (1) foreign B2B providers not charging Finnish VAT, (2) domestic construction services, (3) metal scrap, (4) emissions allowances, (5) investment gold",
42954
+ 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.",
42301
42955
  basis: "AVL 8a-8d\xA7, 9\xA7, 65\xA7; EU VAT Directive Art. 196, Art. 199",
42302
42956
  scenarios: [
42303
- { type: "Foreign B2B", basis: "AVL 9\xA7", note: "Most common: EU and non-EU service providers" },
42304
- { type: "Construction services", basis: "AVL 8c\xA7", note: 'Since 1.4.2011. Buyer must itself sell/rent construction services "not just occasionally". Regular company commissioning renovation: NO reverse charge' },
42957
+ { 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" },
42958
+ { 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' },
42305
42959
  { type: "Metal scrap and waste", basis: "AVL 8d\xA7", note: "Since 1.1.2015. Between VAT-registered entities" },
42306
42960
  { type: "Emissions allowances", basis: "AVL 8b\xA7", note: "Greenhouse gas emission allowances" },
42307
42961
  { type: "Investment gold", basis: "AVL 8a\xA7", note: "Taxable investment gold, gold materials, gold semi-finished products" }
42308
42962
  ],
42309
- booking: "4 entries: D expense + D ALV-saamiset + C bank + C ALV-velka (k\xE4\xE4nnetty)",
42310
- note: "Net VAT effect is zero but BOTH must be reported on ALV-ilmoitus. Invoice must NOT show VAT rate or VAT amount",
42311
- omavero_fields: "Foreign: 305 (EU goods), 306 (EU services). Domestic construction/scrap: 318 (VAT amount), 319/320 (tax bases)"
42963
+ booking: "D expense/asset for net amount plus non-deductible self-assessed VAT; D ALV-saamiset only for deductible VAT; C bank/payable for supplier amount; C ALV-velka for full self-assessed VAT. Use the canonical VAT purchase command, not unlinked VAT account rows.",
42964
+ note: "Net VAT effect is zero only with a full deduction. Report the full assessed VAT and only the deductible share. The seller does not charge VAT on a reverse-charge invoice; this does not remove the buyer\u2019s VAT liability.",
42965
+ implementation_limits: "The MCP purchase shortcut currently has no deduction-share parameter and does not support partial-deduction reverse charge. Do not use its default full deduction for that case: this example describes the accounting rule, not an added tool capability.",
42966
+ example: { net_purchase: 100, rate: FINNISH_RATES_2026.vat.standard, deductible_share: "60%", output_vat: exampleOutputVat, deductible_vat: exampleDeductibleVat, non_deductible_vat: exampleNonDeductibleVat, expense: roundCents(100 + exampleNonDeductibleVat), payable_vat: exampleNonDeductibleVat },
42967
+ 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."
42312
42968
  },
42313
42969
  common_mistakes: [
42314
- "Food/coffee at 25.5% \u2014 should be 13.5% (AVL 85\xA7)",
42315
- "Train ticket at 10% \u2014 should be 13.5% (transport = reduced rate)",
42316
- "Using 14% after 1.1.2026 \u2014 rate changed to 13.5%",
42970
+ "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",
42971
+ "For a domestic passenger-transport supply in 2026, use the current reduced rate; older supplies, advances and credits retain their event-date rate",
42972
+ "Replacing a historical 14% rate merely because the document or return is dated in 2026 \u2014 retain the original supply/advance/credit treatment",
42317
42973
  "Restaurant alcohol at 13.5% \u2014 alcohol is 25.5% even at restaurants",
42318
42974
  "Newspaper/magazine at 13.5% \u2014 periodicals are 10%",
42319
42975
  "Splitting VAT from insurance premium \u2014 insurance is VAT-exempt, not zero-rated",
@@ -42327,40 +42983,44 @@ var INVOICE_REQUIREMENTS = {
42327
42983
  mandatory_fields: [
42328
42984
  { kohta: 1, field: "Laskun antamisp\xE4iv\xE4", english: "Invoice date", status: "implemented", relevance: "always" },
42329
42985
  { kohta: 2, field: "Juokseva tunniste", english: "Sequential invoice number", status: "implemented", relevance: "always", note: "Gaps must be detectable per Vero requirement" },
42330
- { kohta: 3, field: "Myyj\xE4n ALV-tunniste", english: "Seller VAT ID (FI + Y-tunnus)", status: "implemented", relevance: "always", note: "Y-tunnus always required; FI-format strictly mandatory for EU cross-border" },
42986
+ { 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" },
42331
42987
  { kohta: 4, field: "Ostajan ALV-tunniste", english: "Buyer VAT ID", status: "implemented", relevance: "conditional", note: "Required for reverse charge and EU B2B" },
42332
42988
  { kohta: 5, field: "Myyj\xE4n ja ostajan nimi ja osoite", english: "Names and addresses", status: "implemented", relevance: "always" },
42333
42989
  { kohta: 6, field: "Tavaroiden/palvelujen kuvaus ja m\xE4\xE4r\xE4", english: "Description and quantity", status: "implemented", relevance: "always" },
42334
42990
  { 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" },
42335
42991
  { kohta: 8, field: "Veron peruste, yksikk\xF6hinta, alennukset", english: "Tax base, unit price, discounts", status: "implemented", relevance: "always" },
42336
- { kohta: 9, field: "Verokanta", english: "VAT rate", status: "implemented", relevance: "always" },
42337
- { kohta: 10, field: "Suoritettavan veron m\xE4\xE4r\xE4", english: "VAT amount", status: "implemented", relevance: "always" },
42338
- { kohta: 11, field: "Merkint\xE4 verottomuudesta", english: "Tax exemption notation", status: "gap", relevance: "conditional", note: 'Simple "Veroton myynti" suffices. Citing AVL section is recommended but not mandatory (Vero.fi 5.1.11)' },
42339
- { kohta: 12, field: "Merkint\xE4 k\xE4\xE4nnetyst\xE4 verovelvollisuudesta", english: "Reverse charge notation", status: "gap", relevance: "conditional", note: '"K\xE4\xE4nnetty verovelvollisuus" phrase is mandatory. Legal basis citation is recommended, not required' },
42992
+ { 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)" },
42993
+ { 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" },
42994
+ { 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)' },
42995
+ { 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' },
42340
42996
  { kohta: 13, field: "Merkint\xE4 itselaskutuksesta", english: "Self-billing notation", status: "n/a", relevance: "situational", note: "When buyer issues invoice on seller behalf" },
42341
42997
  { kohta: 14, field: "Uuden kuljetusv\xE4lineen tiedot", english: "New means of transport details", status: "n/a", relevance: "situational" },
42342
42998
  { kohta: 15, field: "Voittomarginaalij\xE4rjestelm\xE4", english: "Margin scheme (used goods)", status: "n/a", relevance: "situational" },
42343
42999
  { kohta: 16, field: "Voittomarginaalij\xE4rjestelm\xE4 \u2014 matkatoimistot", english: "Margin scheme (travel agents)", status: "n/a", relevance: "situational" },
42344
43000
  { kohta: 17, field: "Verollisen sijoituskullan myynti", english: "Taxable investment gold", status: "n/a", relevance: "situational" },
42345
- { kohta: 18, field: "Viittaus alkuper\xE4iseen laskuun", english: "Credit note reference (muutoslasku)", status: "not_implemented", relevance: "conditional", note: "Only mandatory requirement: unambiguous reference to original invoice" }
43001
+ { 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)" }
42346
43002
  ],
42347
43003
  simplified_invoice: {
42348
43004
  threshold: 400,
42349
- note: "Invoices \u2264 400 \u20AC may use simplified format (AVL 209 f \xA7). Also: retail to consumers, restaurant/catering, passenger transport \u2014 regardless of amount"
43005
+ 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).",
43006
+ 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"]
42350
43007
  },
42351
43008
  muutoslasku: {
42352
- status: "not_implemented",
42353
- mandatory: "Unambiguous reference to original invoice (number or period)",
42354
- recommended: "Hyvityslasku label, reason for credit, same numbering sequence \u2014 common practice, not legally mandated"
43009
+ status: "implemented",
43010
+ mandatory: "Unambiguous reference to the original invoice and the information being changed (AVL 209 e \xA7 18 kohta)",
43011
+ recommended: "Label and reason help interpretation. A separate systematic credit-note numbering series is allowed; the original rate is preserved.",
43012
+ 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."
42355
43013
  }
42356
43014
  };
42357
43015
  var FILING_DEADLINES = {
42358
43016
  vat_filing: {
42359
43017
  thresholds: [
42360
- { revenue: "< 30,000 \u20AC", periods: ["monthly", "quarterly", "annually"], note: "Annual: deadline last day of February of following year (28.2 or 29.2 in leap year)" },
42361
- { revenue: "30,000\u2013100,000 \u20AC", periods: ["monthly", "quarterly"], note: "Quarterly: 12th of 2nd month after quarter" },
43018
+ { 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)" },
43019
+ { revenue: "> 30,000 and \u2264 100,000 \u20AC", periods: ["monthly", "quarterly"], note: "Quarterly: 12th of 2nd month after quarter" },
42362
43020
  { revenue: "> 100,000 \u20AC", periods: ["monthly"], note: "Monthly: 12th of 2nd month after reporting month" }
42363
43021
  ],
43022
+ 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.",
43023
+ 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.",
42364
43024
  monthly_example: "January 2026 VAT \u2192 deadline 12.3.2026",
42365
43025
  quarterly_deadlines: {
42366
43026
  Q1: "12.5",
@@ -42370,31 +43030,35 @@ var FILING_DEADLINES = {
42370
43030
  }
42371
43031
  },
42372
43032
  tyel: {
42373
- monthly_report: "5th of following month \u2192 pension company",
42374
- payment: "20th of following month \u2192 pension company"
43033
+ 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",
43034
+ 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"
42375
43035
  },
42376
43036
  tulorekisteri: {
42377
43037
  palkkatietoilmoitus: "5 calendar days from payment date \u2192 Tulorekisteri",
42378
43038
  tyonantajan_erillisilmoitus: "5th of following month \u2192 Tulorekisteri (sotu totals)"
42379
43039
  },
42380
- sairausvakuutusmaksu_payment: "12th of following month \u2192 Vero (OmaVero, oma-aloitteinen)",
43040
+ 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",
42381
43041
  tilinpaatos: "4 months from fiscal year end (KPL 3:6)",
42382
43042
  prh_filing: "8 months from fiscal year end (OYL 8:10)",
42383
43043
  pienyrittaja: {
42384
43044
  threshold: 2e4,
42385
- note: "Revenue under 20,000 \u20AC/year: no VAT registration obligation (from 1.1.2025, was 15,000 \u20AC)",
42386
- alarajahuojennus: "REMOVED from 1.1.2025 \u2014 hard threshold, no progressive relief"
43045
+ 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",
43046
+ alarajahuojennus: "Removed for periods from 1.1.2025; historical relief for earlier periods is not rewritten"
42387
43047
  },
42388
43048
  maksuperusteinen_alv: {
42389
43049
  threshold: 5e5,
42390
- note: "Revenue under 500,000 \u20AC: may opt for cash-basis VAT (report when payment received/made, not when invoiced)",
42391
- application: "Opt-in via OmaVero"
43050
+ 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.",
43051
+ application: "No application: the business applies the cash basis in its bookkeeping. Since 2025 the VAT return has no cash-basis field.",
43052
+ source: "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/pienyrityksen-maksuperusteinen-alv/",
43053
+ sources_verified_at: "2026-10-02"
42392
43054
  },
42393
43055
  late_penalties: {
42394
- myohastymismaksu: "3 \u20AC/day for days 1-45 (max 135 \u20AC), then +2% of tax amount (max 15,000 \u20AC per tax type)",
43056
+ myohastymismaksu: "3 \u20AC/day for at most 45 days (135 \u20AC); more than 45 days late: 135 \u20AC + 2 % of the declared payable tax (max 15,000 \u20AC per tax type and period); also for returns with no tax payable",
42395
43057
  viivastyskorko_2026: "9.5% for most taxes, 4.5% for inheritance tax",
42396
- correction_grace: "Corrections within 45 days of original deadline: no penalty if change \u2264 500 \u20AC",
42397
- nil_return: "Even when no VAT activity occurred, a nil return must be filed for each verokausi"
43058
+ correction_grace: "A correction return filed within 45 days of the original due date carries no late-filing fee (no amount condition). Later corrections: 2 % of the additional payable tax.",
43059
+ nil_return: "Even when no VAT activity occurred, a nil return must be filed for each verokausi",
43060
+ source: "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48377/seuraamusmaksut-oma-aloitteisessa-verotuksessa5/",
43061
+ sources_verified_at: "2026-10-02"
42398
43062
  },
42399
43063
  payment_deadline: "Same as filing deadline (12th of M+2 for monthly/quarterly, last day of Feb for annual)",
42400
43064
  omavero_field_codes: {
@@ -42412,33 +43076,33 @@ var FILING_DEADLINES = {
42412
43076
  318: "Vero rakentamispalvelun ja metalliromun ostoista (construction/scrap reverse charge VAT)"
42413
43077
  },
42414
43078
  tax_bases: {
42415
- 309: "0-verokannan alainen liikevaihto (zero-rate turnover: exports, EU sales with deduction right)",
43079
+ 309: "0-verokannan alainen liikevaihto (zero-rate turnover, e.g. exports of goods outside the EU, AVL 55-72 h \xA7). Does NOT include EU goods sales (311) or EU service sales (312). Exempt sales without a deduction right (healthcare, education, finance, insurance) are reported in no field.",
42416
43080
  310: "Tavaroiden maahantuonnit EU:n ulkopuolelta (goods imports from outside EU)",
42417
- 311: "Tavaroiden myynnit muihin EU-maihin (goods sales to EU)",
43081
+ 311: "Tavaroiden myynnit muihin EU-maihin (goods sales to EU; reported for the delivery month when invoiced on or after the delivery in that month, otherwise, including an advance invoice, the next month, AVL 138 a \xA7)",
42418
43082
  312: "Palveluiden myynnit muihin EU-maihin (service sales to EU)",
42419
43083
  313: "Tavaraostot muista EU-maista (goods purchases from EU)",
42420
43084
  314: "Palveluostot muista EU-maista (service purchases from EU)",
42421
43085
  319: "Rakentamispalvelun ja metalliromun myynnit (construction/scrap sales)",
42422
43086
  320: "Rakentamispalvelun ja metalliromun ostot (construction/scrap purchases)"
42423
43087
  },
42424
- removed_2025: "315, 316, 317 (alarajahuojennus fields) removed from 1.1.2025"
43088
+ removed_2025: "315, 316, 317 (alarajahuojennus fields) removed from 1.1.2025",
43089
+ sources_verified_at: "2026-10-02"
42425
43090
  }
42426
43091
  };
42427
43092
  var SOCIAL_INSURANCE_2026 = {
42428
43093
  year: FINNISH_RATES_2026.year,
42429
43094
  sources: [
42430
- "https://www.etk.fi/ajankohtaista/tyoelakemaksut-vuonna-2026/",
42431
- "https://www.elo.fi/fi-fi/tyonantaja/tyel-vakuuttaminen/tyel-maksu/sosiaalivakuutusmaksut-2026",
42432
- "https://tyel-vakuutus.fi/tyonantajan-sivukulut/"
43095
+ ...regulatoryFreshness("payroll").sources.map((source) => source.url),
43096
+ ...regulatoryFreshness("payrollEstimates").sources.map((source) => source.url)
42433
43097
  ],
42434
43098
  tyel: {
42435
43099
  keskimaarainen: FINNISH_RATES_2026.social.tyelKeskimaarainen,
42436
43100
  perusmaksu: FINNISH_RATES_2026.social.tyelPerusmaksu,
42437
- tilapaistyonantaja: 25.85,
43101
+ tilapaistyonantaja: FINNISH_RATES_2026.social.tyelTemporary,
42438
43102
  employee: FINNISH_RATES_2026.social.tyelEmployee,
42439
43103
  employer_average: FINNISH_RATES_2026.social.tyelEmployerAverage,
42440
43104
  employer_perusmaksu: FINNISH_RATES_2026.social.tyelEmployer,
42441
- note: "Perusmaksu 24.85% is what pension companies bill before company-specific adjustments. Keskim\xE4\xE4r\xE4inen 24.40% is the actual average. Age differentiation eliminated in 2026 (ETK)"
43105
+ 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"
42442
43106
  },
42443
43107
  sotu: {
42444
43108
  rate: FINNISH_RATES_2026.social.sotu,
@@ -42450,20 +43114,19 @@ var SOCIAL_INSURANCE_2026 = {
42450
43114
  employer_large: FINNISH_RATES_2026.social.ttvEmployerLarge,
42451
43115
  employer_large_threshold: FINNISH_RATES_2026.social.ttvEmployerLargeThreshold,
42452
43116
  employee: FINNISH_RATES_2026.social.ttvEmployee,
42453
- note: "Ty\xF6tt\xF6myysvakuutus. Employer rate 0.31% for payroll \u2264 2,509,500 \u20AC; 1.23% above. Employee up from 0.59% in 2025"
43117
+ 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."
42454
43118
  },
42455
43119
  tapaturma: {
42456
43120
  average: FINNISH_RATES_2026.social.tapaturmaAverage,
42457
- range: "0.05\u20135.00%",
42458
43121
  payer: "employer",
42459
43122
  note: "Company-specific rate based on industry and claims history"
42460
43123
  },
42461
43124
  ryhmahenkivakuutus: {
42462
43125
  rate: FINNISH_RATES_2026.social.ryhmahenki,
42463
43126
  payer: "employer",
42464
- note: "Standardized via collective agreement"
43127
+ note: "Planning estimate only; obligation and actual premium depend on the applicable collective agreement and insurance contract"
42465
43128
  },
42466
- total_employer_side_costs: "~20% of gross salary (incl. TyEL employer share)",
43129
+ total_employer_side_costs: "Calculate from the applicable employee scope and actual contract premiums; there is no single exact statutory total for every employer",
42467
43130
  // Account numbers are built from the canonical chart (src/accounts.ts) so they
42468
43131
  // can never drift from the YSE 2017 numbering the agent actually books against.
42469
43132
  // DO NOT hardcode literals here — a wrong-but-existing number (e.g. 2320, which
@@ -42483,124 +43146,172 @@ var SOCIAL_INSURANCE_2026 = {
42483
43146
  var BOOKKEEPING_RULES = {
42484
43147
  legal_basis: "Kirjanpitolaki 1336/1997",
42485
43148
  core_principles: [
42486
- { rule: "Double-entry (kahdenkertainen kirjanpito)", basis: "KPL 2:1", note: "Every entry: total debits = total credits" },
43149
+ { 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" },
42487
43150
  { rule: "Voucher principle (tositeperiaate)", basis: "KPL 2:5", note: "Every entry must be based on a numbered voucher" },
42488
43151
  { 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" },
42489
43152
  { rule: "Sequential numbering", basis: "KPL 2:5", note: "Gap detection must be possible from the numbering sequence" },
42490
- { rule: "Closed period immutability", basis: "KPL 2:7", note: "Corrections to closed periods must use new entries in current period" }
43153
+ { 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" }
42491
43154
  ],
42492
43155
  retention: [
42493
- { type: "Accounting records (kirjanpitokirjat)", years: 10, basis: "KPL 2:10 1 mom" },
42494
- { type: "Financial statements (tilinp\xE4\xE4t\xF6s)", years: 10, basis: "KPL 3:9" },
42495
- { type: "Vouchers, invoices (tositteet)", years: 6, basis: "KPL 2:10 2 mom" },
42496
- { type: "E-invoice XML originals", years: 6, basis: "KPL 2:7" }
43156
+ { type: "Accounting ledgers, chart and list of records", years: 10, starts: "From the fiscal-year closing date", basis: "KPL 2:10 1 mom" },
43157
+ { type: "Financial statements and management report", years: 10, starts: "From the fiscal-year closing date", basis: "KPL 2:10 1 mom" },
43158
+ { 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" },
43159
+ { 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" }
42497
43160
  ],
43161
+ 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.",
42498
43162
  fiscal_year: {
42499
- normal_duration: "12 months",
42500
- max_duration: "18 months (first/changed period only)",
43163
+ 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.",
43164
+ max_duration: "18 months when starting, ending or changing the fiscal-year end (KPL 1:4)",
42501
43165
  tilinpaatos_deadline: "4 months from fiscal year end",
42502
43166
  prh_deadline: "8 months from fiscal year end (Oy, Oyj)"
42503
43167
  },
42504
43168
  company_sizes: [
42505
- { class: "Mikroyritys", revenue: "< 900K", balance: "< 450K", employees: "< 10", tilinpaatos: "Simplified (PMA)" },
42506
- { class: "Pienyritys", revenue: "< 15M", balance: "< 7.5M", employees: "< 50", tilinpaatos: "Standard (PMA)" },
42507
- { class: "Keskisuuri", revenue: "< 50M", balance: "< 25M", employees: "< 250", tilinpaatos: "Full (KPA) + toimintakertomus for Oy/Osk" },
42508
- { class: "Suuryritys", revenue: "\u2265 50M", balance: "\u2265 25M", employees: "\u2265 250", tilinpaatos: "Full + toimintakertomus + rahoituslaskelma" }
42509
- ]
43169
+ { 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" },
43170
+ { 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" }
43171
+ ],
43172
+ 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.",
43173
+ 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.",
43174
+ 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."
43175
+ };
43176
+ var CONTENT_REVIEWS = {
43177
+ vat: [{
43178
+ checked_at: "2026-10-03",
43179
+ review_due: "2026-11-20",
43180
+ scope: "Historical rates, deduction conditions, vehicle/staff/marketing distinctions and separate reverse-charge liability/deduction; numerical rate provenance is in numeric_rules.",
43181
+ sources: [
43182
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/arvonlisaveroprosentit/Arvonlisaverokantojen-muutostilanteet/",
43183
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/335806/arvonlis%C3%A4verovelvollisen-opas/",
43184
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48679/palvelujen-ulkomaankaupan--arvonlis%C3%A4verotus2/",
43185
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48462/markkinointi--ja-edustustilaisuudet-arvonlis%C3%A4verotuksessa/",
43186
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ajoneuvon-ja-sen-kayttokulujen-arvonlisavero/",
43187
+ "https://kho.fi/fi/index/paatokset/ennakkopaatokset/1434454271753.html",
43188
+ "https://www.finlex.fi/fi/laki/ajantasa/1993/19931501",
43189
+ "https://www.finlex.fi/fi/laki/ajantasa/1968/19680360"
43190
+ ]
43191
+ }],
43192
+ invoicing: [{
43193
+ checked_at: "2026-10-03",
43194
+ review_due: "2026-11-20",
43195
+ 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.",
43196
+ sources: [INVOICE_REQUIREMENTS.source]
43197
+ }],
43198
+ deadlines: [{
43199
+ checked_at: "2026-10-03",
43200
+ review_due: "2026-11-20",
43201
+ scope: "Inclusive 30,000/100,000 limits, application/effective dates, small-business two-calendar-year rule, earnings reports and contractual TyEL payment dates.",
43202
+ sources: [
43203
+ "https://www.finlex.fi/fi/laki/ajantasa/2016/20160768",
43204
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/arvonlisaverotus/ilmoitus-ja-maksuohjeet/verokauden-muutos/",
43205
+ "https://www.vero.fi/tietoa-verohallinnosta/yhteystiedot-ja-asiointi/lomakkeet/kuvaus/hakemus_omaaloitteisten_verojen_verokau/",
43206
+ "https://www.vero.fi/syventavat-vero-ohjeet/ohje-hakusivu/48658/arvonlisaveroton-vahainen-toiminta/",
43207
+ "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/yritys_tyonantajana/nain-ilmoitat-palkat-ja-tyonantajasuoritukset/",
43208
+ "https://www.ilmarinen.fi/ajankohtaista/uutiset-ja-tiedotteet/2026/tyel-vakuutusmaksukorko-nousee-270-prosenttiin-1.7.2026-alkaen/"
43209
+ ]
43210
+ }, {
43211
+ checked_at: "2026-10-02",
43212
+ review_due: "2026-11-20",
43213
+ valid_through: "2026-12-31",
43214
+ 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.",
43215
+ sources: [FILING_DEADLINES.maksuperusteinen_alv.source, FILING_DEADLINES.late_penalties.source, FILING_DEADLINES.omavero_field_codes.source_url]
43216
+ }],
43217
+ "social-insurance": [{
43218
+ checked_at: "2026-10-03",
43219
+ review_due: "2026-11-20",
43220
+ scope: "Distinction between statutory employee contributions and employer contract/average estimates. Numerical source checks retain their registry dates.",
43221
+ sources: ["https://www.ilmarinen.fi/tyonantaja/tyel-maksut/", "https://www.vero.fi/yritykset-ja-yhteisot/verot-ja-maksut/yritys_tyonantajana/sosiaalivakuutusmaksut/"]
43222
+ }],
43223
+ bookkeeping: [{
43224
+ checked_at: "2026-10-03",
43225
+ review_due: "2026-12-01",
43226
+ 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.",
43227
+ sources: [
43228
+ "https://www.finlex.fi/fi/laki/ajantasa/1997/19971336",
43229
+ "https://www.finlex.fi/fi/laki/ajantasa/1993/19931501",
43230
+ "https://kirjanpitolautakunta.fi/-/pienyrityksen-ja-pienkonsernin-raja-arvojen-arvioimises-1",
43231
+ "https://kirjanpitolautakunta.fi/-/konsernikokorajojen-arvioimisesta",
43232
+ "https://www.finlex.fi/fi/lainsaadanto/saadoskokoelma/2024/605"
43233
+ ]
43234
+ }]
42510
43235
  };
42511
43236
  var TOPICS = {
42512
43237
  vat: {
42513
- description: "Finnish VAT compliance: rates (25.5/13.5/10/0%/exempt), rate history, deduction rules (edustus/neuvottelu/insurance), reverse charge, and common mistakes. CRITICAL: 0% (zero-rated, input VAT deductible) \u2260 exempt (no input VAT deduction). Legal basis: AVL 1501/1993.",
42514
- dated: true,
43238
+ description: "Finnish VAT: current and historical rates, conditional deductions and reverse-charge liability separately from deduction. A 2026 document can lawfully carry historical VAT.",
43239
+ rules: ["vat"],
42515
43240
  body: VAT_COMPLIANCE
42516
43241
  },
42517
43242
  invoicing: {
42518
- description: "Invoice requirements per AVL 209 e \xA7: 18 mandatory markings (kohdat 1-18), muutoslasku/hyvityslasku rules, simplified invoice (\u2264400\u20AC + retail/restaurant). Shows implementation status. Source: vero.fi",
42519
- dated: false,
43243
+ 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.",
43244
+ rules: [],
42520
43245
  body: INVOICE_REQUIREMENTS
42521
43246
  },
42522
43247
  deadlines: {
42523
- description: "Finnish tax filing deadlines: VAT verokausi thresholds (<30K annual, <100K quarterly, >100K monthly), TyEL reporting, tilinp\xE4\xE4t\xF6s deadlines, pienyritt\xE4j\xE4 20K\u20AC threshold, maksuperusteinen ALV.",
42524
- dated: true,
43248
+ 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.",
43249
+ rules: [],
42525
43250
  body: FILING_DEADLINES
42526
43251
  },
42527
43252
  "social-insurance": {
42528
- description: "2026 Finnish social insurance rates: TyEL (perusmaksu 24.85%, keskim\xE4\xE4r\xE4inen 24.40%, employee 7.30%), sotu (1.91%), TTV (0.31%/0.89%), tapaturma (~0.51%), ryhm\xE4henki (0.06%). Sources: ETK, Elo, tyel-vakuutus.fi",
42529
- dated: true,
43253
+ description: "2026 employee social-insurance rates and separately labelled employer premium estimates; use the actual insurance contract for exact employer liabilities.",
43254
+ rules: ["payroll", "payrollEstimates"],
42530
43255
  body: SOCIAL_INSURANCE_2026
42531
43256
  },
42532
43257
  bookkeeping: {
42533
- description: "KPL (Kirjanpitolaki) requirements: double-entry, audit trail, voucher principle, retention periods (10/6 years), fiscal year rules (max 18 months), company size classification (mikro/pien/keski/suuri).",
42534
- dated: false,
43258
+ 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.",
43259
+ rules: ["companySizes"],
42535
43260
  body: BOOKKEEPING_RULES
42536
43261
  }
42537
43262
  };
42538
- function wrapWithMeta(body, dated) {
42539
- const meta3 = freshnessMeta();
42540
- return JSON.stringify({
42541
- _meta: {
42542
- published_at: meta3.published_at,
42543
- valid_from: meta3.valid_from,
42544
- valid_through: dated ? meta3.valid_through : null,
42545
- sources_verified_at: meta3.sources_verified_at,
42546
- stale: dated ? meta3.stale : false,
42547
- days_until_expiry: dated ? meta3.days_until_expiry : null,
42548
- dated_rates: dated
42549
- },
42550
- ...body
42551
- });
43263
+ function complianceTopicMeta(slug, today = /* @__PURE__ */ new Date()) {
43264
+ const topic = TOPICS[slug];
43265
+ const day = helsinkiDateString(today);
43266
+ const rules = topic.rules.map((rule) => regulatoryFreshness(rule, today));
43267
+ const reviews = CONTENT_REVIEWS[slug];
43268
+ const dates = [...rules.map((rule) => rule.sources_verified_at), ...reviews.map((review) => review.checked_at)];
43269
+ const expiries = [...rules.map((rule) => rule.valid_through), ...reviews.map((review) => review.valid_through)].filter((date5) => !!date5).sort();
43270
+ const expiry = expiries[0] ?? null;
43271
+ const reviewDue = [...rules.map((rule) => rule.review_due), ...reviews.map((review) => review.review_due)].sort()[0];
43272
+ return {
43273
+ published_at: null,
43274
+ dated_rates: slug === "vat" || slug === "social-insurance" || slug === "deadlines",
43275
+ sources_verified_at: dates.sort()[0],
43276
+ 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.",
43277
+ valid_from: null,
43278
+ valid_through: expiry,
43279
+ review_due: reviewDue,
43280
+ review_overdue: day > reviewDue,
43281
+ stale: rules.some((rule) => rule.stale) || day > reviewDue || expiry !== null && day > expiry,
43282
+ days_until_expiry: expiry === null ? null : Math.round((Date.parse(`${expiry}T00:00:00Z`) - Date.parse(`${day}T00:00:00Z`)) / 864e5),
43283
+ content_reviews: reviews,
43284
+ numeric_rules: rules,
43285
+ accountant_confirmed: false,
43286
+ decision_status: "Agent-reviewed primary sources; interpretations are provisional and separately identified. Not an accountant approval or a guarantee for a particular transaction.",
43287
+ implementation_verified_at: slug === "invoicing" ? "2026-10-03" : null
43288
+ };
43289
+ }
43290
+ function complianceResource(slug, today = /* @__PURE__ */ new Date()) {
43291
+ return { _meta: complianceTopicMeta(slug, today), ...TOPICS[slug].body };
42552
43292
  }
42553
43293
  function registerComplianceResources(server2) {
42554
- for (const [slug, topic] of Object.entries(TOPICS)) {
43294
+ for (const slug of Object.keys(TOPICS)) {
42555
43295
  server2.registerResource(
42556
43296
  `compliance-${slug}`,
42557
43297
  `kirjapro://compliance/${slug}`,
42558
- {
42559
- description: topic.description,
42560
- mimeType: "application/json"
42561
- },
42562
- async (uri) => ({
42563
- contents: [{
42564
- uri: uri.href,
42565
- mimeType: "application/json",
42566
- text: wrapWithMeta(topic.body, topic.dated)
42567
- }]
42568
- })
43298
+ { description: TOPICS[slug].description, mimeType: "application/json" },
43299
+ async (uri) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(complianceResource(slug)) }] })
42569
43300
  );
42570
43301
  }
42571
43302
  server2.registerResource(
42572
43303
  "compliance-freshness",
42573
43304
  "kirjapro://compliance/_freshness",
42574
- {
42575
- description: "Freshness index for every dated compliance chunk. Read this at session start. Returns valid_through/stale/days_until_expiry for each topic so the agent can detect expired rates before relying on them.",
42576
- mimeType: "application/json"
42577
- },
42578
- async (uri) => {
42579
- const meta3 = freshnessMeta();
42580
- const entries = Object.entries(TOPICS).map(([slug, topic]) => ({
42581
- topic: slug,
42582
- uri: `kirjapro://compliance/${slug}`,
42583
- dated_rates: topic.dated,
42584
- valid_through: topic.dated ? meta3.valid_through : null,
42585
- stale: topic.dated ? meta3.stale : false,
42586
- days_until_expiry: topic.dated ? meta3.days_until_expiry : null
42587
- }));
42588
- return {
42589
- contents: [{
42590
- uri: uri.href,
42591
- mimeType: "application/json",
42592
- text: JSON.stringify({
42593
- _meta: {
42594
- sources_verified_at: meta3.sources_verified_at,
42595
- published_at: meta3.published_at,
42596
- rates_module: "src/compliance/rates.ts",
42597
- source: FINNISH_RATES_2026.source
42598
- },
42599
- topics: entries
42600
- })
42601
- }]
42602
- };
42603
- }
43305
+ { 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" },
43306
+ async (uri) => ({ contents: [{
43307
+ uri: uri.href,
43308
+ mimeType: "application/json",
43309
+ text: JSON.stringify({
43310
+ _meta: { published_at: null, sources_verified_at: null, provenance: "Review dates and validity are scoped per topic below" },
43311
+ registry: "regulatory/registry.json (generated package-local subset)",
43312
+ topics: Object.keys(TOPICS).map((slug) => ({ topic: slug, uri: `kirjapro://compliance/${slug}`, ...complianceTopicMeta(slug) }))
43313
+ })
43314
+ }] })
42604
43315
  );
42605
43316
  }
42606
43317
 
@@ -42702,7 +43413,7 @@ Do not invent a "closest match" tool for these tasks. A confidently wrong action
42702
43413
  A bank transaction that is already booked (a receipt or purchase booked as paid from the bank account, a manual invoice payment, a paid purchase invoice, a Kirjapro payment batch) is linked, never posted again: \`kirjapro_list_bank_link_candidates\` \u2192 \`kirjapro_link_bank_transaction\`. Never post a manual voucher for an already-booked or skipped bank transaction; a wrong reconciliation is undone in the Kirjapro app (Pankki \u2192 T\xE4sm\xE4ytys \u2192 T\xE4sm\xE4ytetyt \u2192 Pura t\xE4sm\xE4ytys) or, on the full surface, with \`kirjapro_unreconcile_bank_transaction\`, and then matched again.
42703
43414
 
42704
43415
  ### Reports
42705
- \`kirjapro_get_dashboard\` (overview) | \`kirjapro_get_income_statement\` (tuloslaskelma) | \`kirjapro_get_balance_sheet\` (tase \u2014 \`equity.synthetic_tilikauden_tulos\` is the unclosed P/L residual already included in equity; never add it again. The cumulative report groups current earnings under 3600 and retained earnings under 3500. Report rows can differ from raw ledger account balances. Do not present book equity as an approved distribution limit) | \`kirjapro_get_ar_aging\` (bounded myyntisaamisten aggregate; weighted age is not revenue-based DSO) | \`kirjapro_get_alv_summary\` (ALV-yhteenveto with OmaVero field codes 301-320).
43416
+ \`kirjapro_get_dashboard\` (overview) | \`kirjapro_get_income_statement\` (tuloslaskelma) | \`kirjapro_get_balance_sheet\` (tase \u2014 \`equity.synthetic_tilikauden_tulos\` is the unclosed P/L residual already included in equity; never add it again. The cumulative report groups current earnings under 3600 and retained earnings under 3500. Report rows can differ from raw ledger account balances. Do not present book equity as an approved distribution limit) | \`kirjapro_get_ar_aging\` (bounded myyntisaamisten aggregate; weighted age is not revenue-based DSO) | \`kirjapro_get_alv_summary\` (ALV-yhteenveto; the official OmaVero fields are in \`omavero_fields\`).
42706
43417
 
42707
43418
  ## Safety Rules
42708
43419
 
@@ -42768,7 +43479,7 @@ var INSTRUCTIONS_CORE_ADDENDUM = `
42768
43479
  The following workflows are NOT exposed in the current (core) surface. When a user asks for them, redirect to the web UI rather than reaching for the closest available tool:
42769
43480
 
42770
43481
  - **Recurring invoice templates** \u2014 created and managed in the web app; cron generates the issued invoices
42771
- - **Booking shortcuts** (book_purchase / book_sale / book_vat_settlement) \u2014 for these, you CAN compose the journal entries manually via \`kirjapro_create_transaction\` with explicit debit/credit lines if the user prefers; otherwise redirect. Exception: a VAT-registered (ALV) company books sales only as invoices (\`kirjapro_create_invoice\` + \`kirjapro_send_invoice\`) and reverse-charge purchases in the web app; the database refuses its vouchers with sales accounts 5000-5499 or a net 4210 credit
43482
+ - **Booking shortcuts** (book_purchase / book_sale / book_vat_settlement) \u2014 for these, you CAN compose the journal entries manually via \`kirjapro_create_transaction\` with explicit debit/credit lines if the user prefers; otherwise redirect. Exception: a VAT-registered (ALV) company books sales only as invoices (\`kirjapro_create_invoice\` + \`kirjapro_send_invoice\`) and reverse-charge purchases and purchases with deductible VAT in the web app (a free 2320 line on a voucher has no VAT fact and does not reach field 307); the database refuses its vouchers with a net credit on sales accounts 5000-5499, a sales-account line together with a 4210 or 2320 line, or a net 4210 credit; moving an amount out of a sales account without a VAT line (e.g. a fixed-asset disposal D 5010 / K 1600) is allowed
42772
43483
  - **Payroll (palkanlaskenta)** \u2014 Kirjapro does NOT calculate or book payroll. Direct the user to Palkkaus.fi or Palkka.fi for the calculation, then bring the resulting payslip back and book it via \`kirjapro_create_transaction\` with explicit debit/credit lines
42773
43484
  - **Transaction templates** \u2014 manual journal entries via \`kirjapro_create_transaction\`
42774
43485
  - **Manual offer status changes** \u2014 the customer's email-link click sets status server-side; just wait for the user to confirm acceptance
@@ -42787,7 +43498,7 @@ var INSTRUCTIONS_FULL_ADDENDUM = `
42787
43498
 
42788
43499
  ### Booking Shortcuts
42789
43500
  Faster than composing journal entries via \`kirjapro_create_transaction\`:
42790
- - \`kirjapro_book_sale\` / \`kirjapro_book_purchase\` \u2014 one-liner with auto VAT splitting. A VAT-registered (ALV) company books sales as invoices (\`kirjapro_create_invoice\` + \`kirjapro_send_invoice\`): \`kirjapro_book_sale\` and \`kirjapro_book_purchase\` with \`reverseCharge\` are refused for it
43501
+ - \`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
42791
43502
  - \`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.
42792
43503
  - **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\`.
42793
43504
  - \`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)
@@ -42943,15 +43654,19 @@ async function main() {
42943
43654
  });
42944
43655
  registerResources(server);
42945
43656
  registerPrompts(server);
42946
- const freshness = freshnessMeta(FINNISH_RATES_2026);
42947
- if (freshness.stale) {
42948
- console.error(
42949
- `[kirjapro-mcp] WARNING: Finnish rates dataset is STALE (valid through ${freshness.valid_through}). Running on expired VAT and social-insurance rates. Upgrade @kirjapro/mcp-server immediately.`
42950
- );
42951
- } else if (freshness.days_until_expiry < 30) {
42952
- console.error(
42953
- `[kirjapro-mcp] NOTICE: Finnish rates dataset expires in ${freshness.days_until_expiry} days (${freshness.valid_through}). Upgrade @kirjapro/mcp-server before year-end to avoid stale rates.`
42954
- );
43657
+ for (const rule of ["vat", "payroll", "payrollEstimates"]) {
43658
+ const freshness = regulatoryFreshness(rule);
43659
+ if (freshness.expired || freshness.not_yet_effective) {
43660
+ console.error(
43661
+ `[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.`
43662
+ );
43663
+ } else if (freshness.review_overdue) {
43664
+ console.error(
43665
+ `[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.`
43666
+ );
43667
+ } else if (freshness.days_until_expiry !== null && freshness.days_until_expiry < 30) {
43668
+ console.error(`[kirjapro-mcp] NOTICE: Bundled ${rule} rules cover ${freshness.days_until_expiry} more days (${freshness.valid_through}).`);
43669
+ }
42955
43670
  }
42956
43671
  const transport = new StdioServerTransport();
42957
43672
  transport.onclose = () => void shutdown();