@kirjapro/mcp-server 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0] - Unreleased
4
+
5
+ Requires the database migrations `20261001204555`–`20261001204559` (ALV report
6
+ audit batch 1). Version 0.9.1 keeps working against that database: its sales
7
+ and reverse-charge vouchers of a VAT-registered company are refused by the
8
+ database with the same guidance, and its settlement default date stays the
9
+ old one.
10
+
11
+ - `kirjapro_book_sale`, the sale templates (`cash_sale_*`, `bank_sale_*`,
12
+ `invoice_sale_25`) of `kirjapro_create_from_template(_batch)` and
13
+ `kirjapro_book_purchase` with `reverseCharge` are refused for a
14
+ VAT-registered (ALV) company before any write: a journal voucher never
15
+ reaches the ALV report. Sales are created with `kirjapro_create_invoice` and
16
+ delivered with `kirjapro_send_invoice`; reverse-charge purchases are booked
17
+ as received invoices in the web app for now. `kirjapro_list_templates` no
18
+ longer lists the sale templates for such a company. Other companies are
19
+ unchanged.
20
+ - `kirjapro_create_transaction(s_batch)`: the database refuses lines on sales
21
+ accounts 5000-5499 and a net 4210 credit for a VAT-registered company; the
22
+ descriptions say so.
23
+ - `kirjapro_book_vat_settlement` settles the company's VAT period (month,
24
+ quarter or year from `companies.vat_period`) containing `month`. The default
25
+ date is the statutory due date (12th of the second month after the period;
26
+ annual period: end of February), and the fiscal year defaults to the open
27
+ year containing that date, so a December settlement is booked in the next
28
+ year. The database now excludes earlier settlements, closing entries and the
29
+ previous-year import, refuses a settled period, a period whose ledger does
30
+ not match the ALV report and negative balances, and books only non-zero
31
+ lines. The result and the dry run name the period. A company that is not
32
+ VAT-registered is refused.
33
+ - `kirjapro_get_alv_summary` returns `reconciliation`: the 4210/2320 ledger
34
+ against the report (status `match`, `explained` or `mismatch`, the
35
+ differences and the explaining vouchers, invoices, fact mismatches and
36
+ manual rows), and sets `warning` on a mismatch.
37
+ - The server instructions (core and full surface) say that a VAT-registered
38
+ company books sales as invoices, that `kirjapro_create_transaction` cannot
39
+ replace `kirjapro_book_sale` for it, and that its sale templates are hidden.
40
+ - Review the fingerprints of `kirjapro_book_sale`, `kirjapro_book_purchase`,
41
+ `kirjapro_book_vat_settlement`, `kirjapro_create_from_template`,
42
+ `kirjapro_create_from_template_batch`, `kirjapro_list_templates`,
43
+ `kirjapro_create_transaction`, `kirjapro_create_transactions_batch` and
44
+ `kirjapro_get_alv_summary` for their changed descriptions and schemas.
45
+
3
46
  ## [0.9.1] - 2026-09-29
4
47
 
5
48
  Requires the database migration `20260929120200` (bank reconciliation
package/dist/index.js CHANGED
@@ -4148,13 +4148,14 @@ var require_fast_uri = __commonJS({
4148
4148
  if (!malformedIPLiteral) {
4149
4149
  malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4150
4150
  }
4151
- if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4152
- if (uri.indexOf("%") !== -1) {
4153
- if (parsed.host !== void 0 && !malformedIPLiteral) {
4154
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4155
- parsed.host = reescapeHostDelimiters(host, isIP);
4156
- }
4151
+ if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
4152
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4153
+ if (!isIP) {
4154
+ host = normalizePercentEncoding(host.toLowerCase());
4157
4155
  }
4156
+ parsed.host = reescapeHostDelimiters(host, isIP);
4157
+ }
4158
+ if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4158
4159
  if (parsed.path) {
4159
4160
  parsed.path = normalizePathEncoding(parsed.path);
4160
4161
  }
@@ -35678,7 +35679,7 @@ var StdioServerTransport = class {
35678
35679
  import { createClient } from "@supabase/supabase-js";
35679
35680
 
35680
35681
  // src/version.ts
35681
- var VERSION = "0.9.1";
35682
+ var VERSION = "0.10.0";
35682
35683
 
35683
35684
  // src/auth.ts
35684
35685
  var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
@@ -36881,7 +36882,7 @@ function registerTransactionTools(server2) {
36881
36882
  server2.registerTool(
36882
36883
  "kirjapro_create_transaction",
36883
36884
  {
36884
- 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.",
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
36886
  inputSchema: {
36886
36887
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
36887
36888
  transactionDate: ISO_DATE_SCHEMA.describe("Transaction date (YYYY-MM-DD)"),
@@ -37020,7 +37021,7 @@ function registerTransactionTools(server2) {
37020
37021
  server2.registerTool(
37021
37022
  "kirjapro_create_transactions_batch",
37022
37023
  {
37023
- 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.",
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
37025
  inputSchema: {
37025
37026
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
37026
37027
  dryRun: external_exports.boolean().default(true).describe(
@@ -38598,9 +38599,19 @@ var ALV_SUMMARY_OUTPUT_SHAPE = {
38598
38599
  reverse_charge_sales_base: external_exports.number().optional(),
38599
38600
  eu_service_sales_base: external_exports.number().optional(),
38600
38601
  unclassified_legacy_reverse_charge_base: external_exports.number().optional(),
38602
+ // get_alv_summary.reconciliation: the report against the 4210/2320 ledger
38603
+ // (ALV report audit 2026-10-01, T2-1). Absent on an older database.
38604
+ reconciliation: external_exports.object({ status: external_exports.enum(["match", "explained", "mismatch"]) }).passthrough().optional(),
38601
38605
  warning: external_exports.string().optional(),
38602
38606
  message: external_exports.string().optional()
38603
38607
  };
38608
+ function alvReconciliationWarning(reconciliation) {
38609
+ if (!reconciliation || typeof reconciliation !== "object") return void 0;
38610
+ const r = reconciliation;
38611
+ if (r.status !== "mismatch") return void 0;
38612
+ 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.`;
38614
+ }
38604
38615
  var BALANCE_SHEET_ROW_SHAPE = external_exports.object({
38605
38616
  account_number: external_exports.string(),
38606
38617
  name: external_exports.string(),
@@ -39076,7 +39087,7 @@ function registerReportTools(server2) {
39076
39087
  server2.registerTool(
39077
39088
  "kirjapro_get_alv_summary",
39078
39089
  {
39079
- description: "Get VAT (ALV) summary for a date range. Returns output/input VAT breakdown by rate and OmaVero field codes (301-320). For filing deadlines, read kirjapro://compliance/deadlines.",
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.",
39080
39091
  inputSchema: {
39081
39092
  dateFrom: ISO_DATE_SCHEMA.describe("Period start date (YYYY-MM-DD)"),
39082
39093
  dateTo: ISO_DATE_SCHEMA.describe("Period end date (YYYY-MM-DD)")
@@ -39122,8 +39133,10 @@ function registerReportTools(server2) {
39122
39133
  field_320: _legacyField320,
39123
39134
  ...safeResult
39124
39135
  } = raw;
39136
+ const warning = alvReconciliationWarning(raw.reconciliation);
39125
39137
  return toResult({
39126
39138
  ...safeResult,
39139
+ ...warning && { warning },
39127
39140
  period: { from: params.dateFrom, to: params.dateTo },
39128
39141
  net_vat_payable_formatted: formatCurrency(result.net_vat_payable || 0),
39129
39142
  omavero_fields: {
@@ -39745,6 +39758,21 @@ var ACCOUNTS = {
39745
39758
  VAKUUTUKSET: "7970"
39746
39759
  };
39747
39760
 
39761
+ // src/compliance/vat-guard.ts
39762
+ 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.";
39764
+ var NOT_VAT_REGISTERED_SETTLEMENT_REFUSAL = "This company is not VAT-registered (ALV), so it has no VAT settlement to book.";
39765
+ async function readCompanyVat(supabase2, companyId2) {
39766
+ const { data, error: error61 } = await supabase2.from("companies").select("alv_registered, vat_period").eq("id", companyId2).single();
39767
+ if (error61 || !data) {
39768
+ return toErrorContent(
39769
+ error61 ? sanitizeDbError("Failed to verify the company VAT registration", error61) : "Failed to verify the company VAT registration."
39770
+ );
39771
+ }
39772
+ const row = data;
39773
+ return { alvRegistered: row.alv_registered === true, vatPeriod: row.vat_period ?? "monthly" };
39774
+ }
39775
+
39748
39776
  // src/tools/templates.ts
39749
39777
  function splitVat(gross, vatRate) {
39750
39778
  if (vatRate === 0) return { net: roundCents(gross), vat: 0 };
@@ -39844,24 +39872,28 @@ var TEMPLATES = [
39844
39872
  // Cash sales
39845
39873
  {
39846
39874
  id: "cash_sale_25",
39875
+ kind: "sale",
39847
39876
  description: "K\xE4teismyynti ALV 25,5%",
39848
39877
  vatRate: 25.5,
39849
39878
  expand: saleCashTemplate(ACCOUNTS.MYYNTI_25_5, 25.5)
39850
39879
  },
39851
39880
  {
39852
39881
  id: "cash_sale_13_5",
39882
+ kind: "sale",
39853
39883
  description: "K\xE4teismyynti ALV 13,5%",
39854
39884
  vatRate: 13.5,
39855
39885
  expand: saleCashTemplate(ACCOUNTS.MYYNTI_13_5, 13.5)
39856
39886
  },
39857
39887
  {
39858
39888
  id: "cash_sale_10",
39889
+ kind: "sale",
39859
39890
  description: "K\xE4teismyynti ALV 10%",
39860
39891
  vatRate: 10,
39861
39892
  expand: saleCashTemplate(ACCOUNTS.MYYNTI_10, 10)
39862
39893
  },
39863
39894
  {
39864
39895
  id: "cash_sale_0",
39896
+ kind: "sale",
39865
39897
  description: "K\xE4teismyynti ALV 0%",
39866
39898
  vatRate: 0,
39867
39899
  expand: saleCashTemplate(ACCOUNTS.MYYNTI_0, 0)
@@ -39869,18 +39901,21 @@ var TEMPLATES = [
39869
39901
  // Bank sales
39870
39902
  {
39871
39903
  id: "bank_sale_25",
39904
+ kind: "sale",
39872
39905
  description: "Pankkimyynti ALV 25,5%",
39873
39906
  vatRate: 25.5,
39874
39907
  expand: saleBankTemplate(ACCOUNTS.MYYNTI_25_5, 25.5)
39875
39908
  },
39876
39909
  {
39877
39910
  id: "bank_sale_13_5",
39911
+ kind: "sale",
39878
39912
  description: "Pankkimyynti ALV 13,5%",
39879
39913
  vatRate: 13.5,
39880
39914
  expand: saleBankTemplate(ACCOUNTS.MYYNTI_13_5, 13.5)
39881
39915
  },
39882
39916
  {
39883
39917
  id: "bank_sale_10",
39918
+ kind: "sale",
39884
39919
  description: "Pankkimyynti ALV 10%",
39885
39920
  vatRate: 10,
39886
39921
  expand: saleBankTemplate(ACCOUNTS.MYYNTI_10, 10)
@@ -39888,6 +39923,7 @@ var TEMPLATES = [
39888
39923
  // Invoice sale
39889
39924
  {
39890
39925
  id: "invoice_sale_25",
39926
+ kind: "sale",
39891
39927
  description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
39892
39928
  vatRate: 25.5,
39893
39929
  expand: (amount, opts) => {
@@ -39909,6 +39945,7 @@ var TEMPLATES = [
39909
39945
  // Purchase expenses
39910
39946
  {
39911
39947
  id: "purchase_expense",
39948
+ kind: "purchase",
39912
39949
  description: "Osto ALV 25,5% (pankkitililt\xE4)",
39913
39950
  vatRate: 25.5,
39914
39951
  expand: (amount, opts) => {
@@ -39929,6 +39966,7 @@ var TEMPLATES = [
39929
39966
  },
39930
39967
  {
39931
39968
  id: "service_expense",
39969
+ kind: "purchase",
39932
39970
  description: "Ulkopuolinen palvelu ALV 25,5%",
39933
39971
  vatRate: 25.5,
39934
39972
  expand: (amount, opts) => {
@@ -39950,6 +39988,7 @@ var TEMPLATES = [
39950
39988
  // Rent (no VAT)
39951
39989
  {
39952
39990
  id: "rent_payment",
39991
+ kind: "other",
39953
39992
  description: "Vuokranmaksu (pankkitililt\xE4)",
39954
39993
  vatRate: null,
39955
39994
  expand: (amount, opts) => applyOverrides(
@@ -39971,6 +40010,7 @@ var TEMPLATES = [
39971
40010
  // Bank transfer
39972
40011
  {
39973
40012
  id: "bank_transfer",
40013
+ kind: "other",
39974
40014
  description: "Tilisiirto kassasta pankkitilille",
39975
40015
  vatRate: null,
39976
40016
  expand: (amount, opts) => applyOverrides(
@@ -39991,6 +40031,12 @@ var TEMPLATES = [
39991
40031
  }
39992
40032
  ];
39993
40033
  var TEMPLATE_MAP = new Map(TEMPLATES.map((t) => [t.id, t]));
40034
+ function isSaleTemplate(templateId) {
40035
+ return TEMPLATE_MAP.get(templateId)?.kind === "sale";
40036
+ }
40037
+ function availableTemplates(alvRegistered) {
40038
+ return alvRegistered ? TEMPLATES.filter((t) => t.kind !== "sale") : TEMPLATES;
40039
+ }
39994
40040
  function expandTemplate(templateId, amount, opts) {
39995
40041
  const template = TEMPLATE_MAP.get(templateId);
39996
40042
  if (!template) {
@@ -40105,7 +40151,7 @@ function registerTemplateTools(server2) {
40105
40151
  server2.registerTool(
40106
40152
  "kirjapro_list_templates",
40107
40153
  {
40108
- description: "List available transaction templates. Templates handle double-entry balancing and VAT splitting automatically.",
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.",
40109
40155
  inputSchema: {},
40110
40156
  outputSchema: {
40111
40157
  templates: external_exports.array(external_exports.object({
@@ -40117,7 +40163,9 @@ function registerTemplateTools(server2) {
40117
40163
  annotations: READ_ONLY_ANNOTATIONS
40118
40164
  },
40119
40165
  async () => {
40120
- const list = TEMPLATES.map((t) => ({
40166
+ const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
40167
+ if ("isError" in companyVat) return companyVat;
40168
+ const list = availableTemplates(companyVat.alvRegistered).map((t) => ({
40121
40169
  id: t.id,
40122
40170
  description: t.description,
40123
40171
  vatRate: t.vatRate
@@ -40128,7 +40176,7 @@ function registerTemplateTools(server2) {
40128
40176
  server2.registerTool(
40129
40177
  "kirjapro_create_from_template",
40130
40178
  {
40131
- description: 'Create a transaction from a template. Use kirjapro_list_templates for available templates. accountOverrides swaps default accounts (e.g. {"2320": "2330"}).',
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.',
40132
40180
  inputSchema: {
40133
40181
  template: external_exports.string().describe('Template ID (e.g. "cash_sale_25", "purchase_expense")'),
40134
40182
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
@@ -40144,6 +40192,11 @@ function registerTemplateTools(server2) {
40144
40192
  async (params) => {
40145
40193
  const supabase2 = getSupabase();
40146
40194
  const companyId2 = getCompanyId();
40195
+ if (isSaleTemplate(params.template)) {
40196
+ const companyVat = await readCompanyVat(supabase2, companyId2);
40197
+ if ("isError" in companyVat) return companyVat;
40198
+ if (companyVat.alvRegistered) return toErrorContent(ALV_REGISTERED_SALE_REFUSAL);
40199
+ }
40147
40200
  return withIdempotency(
40148
40201
  supabase2,
40149
40202
  companyId2,
@@ -40184,7 +40237,7 @@ function registerTemplateTools(server2) {
40184
40237
  server2.registerTool(
40185
40238
  "kirjapro_create_from_template_batch",
40186
40239
  {
40187
- 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.",
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.",
40188
40241
  inputSchema: {
40189
40242
  fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
40190
40243
  dryRun: external_exports.boolean().default(true).describe(
@@ -40207,6 +40260,16 @@ function registerTemplateTools(server2) {
40207
40260
  async (params) => {
40208
40261
  const supabase2 = getSupabase();
40209
40262
  const companyId2 = getCompanyId();
40263
+ const saleIndexes = params.transactions.map((tx, index) => isSaleTemplate(tx.template) ? index : -1).filter((index) => index >= 0);
40264
+ if (saleIndexes.length > 0) {
40265
+ const companyVat = await readCompanyVat(supabase2, companyId2);
40266
+ if ("isError" in companyVat) return companyVat;
40267
+ if (companyVat.alvRegistered) {
40268
+ return toErrorContent(
40269
+ `${ALV_REGISTERED_SALE_REFUSAL} Sale templates at index ${saleIndexes.join(", ")}; nothing was written.`
40270
+ );
40271
+ }
40272
+ }
40210
40273
  return withIdempotency(
40211
40274
  supabase2,
40212
40275
  companyId2,
@@ -40430,6 +40493,35 @@ function freshnessMeta(rates = FINNISH_RATES_2026, today = /* @__PURE__ */ new D
40430
40493
  };
40431
40494
  }
40432
40495
 
40496
+ // src/compliance/vat-settlement-date.ts
40497
+ var pad = (value) => String(value).padStart(2, "0");
40498
+ function vatPeriodRange(month, vatPeriod) {
40499
+ const [yearText, monthText] = month.split("-");
40500
+ const year = Number(yearText);
40501
+ const monthNumber = Number(monthText);
40502
+ const period = vatPeriod ?? "monthly";
40503
+ const startMonth = period === "yearly" ? 1 : period === "quarterly" ? Math.floor((monthNumber - 1) / 3) * 3 + 1 : monthNumber;
40504
+ const endMonth = period === "yearly" ? 12 : period === "quarterly" ? startMonth + 2 : monthNumber;
40505
+ const lastDay = new Date(Date.UTC(year, endMonth, 0)).getUTCDate();
40506
+ const label = period === "yearly" ? String(year) : period === "quarterly" ? `${year}-Q${(startMonth + 2) / 3}` : month;
40507
+ return {
40508
+ start: `${year}-${pad(startMonth)}-01`,
40509
+ end: `${year}-${pad(endMonth)}-${pad(lastDay)}`,
40510
+ label
40511
+ };
40512
+ }
40513
+ function defaultSettlementDate(month, vatPeriod) {
40514
+ const { end } = vatPeriodRange(month, vatPeriod);
40515
+ const [endYear, endMonth] = end.split("-").map(Number);
40516
+ if ((vatPeriod ?? "monthly") === "yearly") {
40517
+ const februaryDays = new Date(Date.UTC(endYear + 1, 2, 0)).getUTCDate();
40518
+ return `${endYear + 1}-02-${pad(februaryDays)}`;
40519
+ }
40520
+ const dueMonthIndex = endMonth - 1 + 2;
40521
+ const dueYear = endYear + Math.floor(dueMonthIndex / 12);
40522
+ return `${dueYear}-${pad(dueMonthIndex % 12 + 1)}-12`;
40523
+ }
40524
+
40433
40525
  // src/tools/bookings.ts
40434
40526
  function expenseAccountWarning(account) {
40435
40527
  const first = account.trim().charAt(0);
@@ -40456,7 +40548,7 @@ function registerBookingTools(server2) {
40456
40548
  server2.registerTool(
40457
40549
  "kirjapro_book_purchase",
40458
40550
  {
40459
- description: "Book a purchase (osto) with automatic VAT splitting. Supports reverse charge (AVL 9\xA7) and entertainment (edustus, AVL 114\xA7). Rate rules: food/restaurants 13.5%, insurance 0% (exempt), alcohol 25.5%.",
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%.",
40460
40552
  inputSchema: {
40461
40553
  amount: external_exports.number().finite().positive().max(1e9).describe(
40462
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)."
@@ -40479,6 +40571,11 @@ function registerBookingTools(server2) {
40479
40571
  const rate = Number.parseFloat(params.vatRate);
40480
40572
  const gross = roundCents(params.amount);
40481
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
+ }
40482
40579
  if (params.edustus) {
40483
40580
  entries = [
40484
40581
  { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
@@ -40547,11 +40644,11 @@ function registerBookingTools(server2) {
40547
40644
  server2.registerTool(
40548
40645
  "kirjapro_book_vat_settlement",
40549
40646
  {
40550
- description: `Book ALV-tilitys (VAT settlement) for a month. Calculates net from ALV accounts (${ACCOUNTS.ALV_SAAMISET}/${ACCOUNTS.ALV_VELKA}) and creates settlement. Default date: 12th of month+1.`,
40647
+ description: `Book ALV-tilitys (VAT settlement) for the company's VAT period that contains the given month. Settles the period's net from ALV accounts (${ACCOUNTS.ALV_SAAMISET}/${ACCOUNTS.ALV_VELKA}) by voucher date, excluding earlier settlements, closing entries and the previous-year import. Default date: the statutory due date (12th of the second month after the period; annual period: end of February), not moved to a business day. The period follows the company's VAT period. Refuses a period already settled, a period whose ledger does not match the ALV report, and negative balances.`,
40551
40648
  inputSchema: {
40552
- month: external_exports.string().regex(/^\d{4}-\d{2}$/).describe("Settlement month (YYYY-MM)"),
40553
- fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
40554
- settlementDate: ISO_DATE_SCHEMA.optional().describe("Settlement date (default: 12th of month+1)"),
40649
+ month: external_exports.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/).describe("A month of the VAT period to settle (YYYY-MM); a quarterly or annual company settles the whole period containing it"),
40650
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID of the settlement date (default: the open fiscal year containing the settlement date)"),
40651
+ settlementDate: ISO_DATE_SCHEMA.optional().describe("Settlement date (default: the statutory due date, 12th of the second month after the period; annual period: end of February)"),
40555
40652
  paymentAccount: external_exports.string().default(ACCOUNTS.PANKKITILI).describe(`Bank account (default "${ACCOUNTS.PANKKITILI}" Pankkitili)`),
40556
40653
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
40557
40654
  dryRun: DRY_RUN_SCHEMA
@@ -40561,16 +40658,25 @@ function registerBookingTools(server2) {
40561
40658
  async (params) => {
40562
40659
  const supabase2 = getSupabase();
40563
40660
  const companyId2 = getCompanyId();
40661
+ const companyVat = await readCompanyVat(supabase2, companyId2);
40662
+ if ("isError" in companyVat) return companyVat;
40663
+ if (!companyVat.alvRegistered) return toErrorContent(NOT_VAT_REGISTERED_SETTLEMENT_REFUSAL);
40664
+ const period = vatPeriodRange(params.month, companyVat.vatPeriod);
40665
+ const dueDate = defaultSettlementDate(params.month, companyVat.vatPeriod);
40666
+ const settlementDate = params.settlementDate ?? dueDate;
40564
40667
  let fiscalYearId = params.fiscalYearId;
40565
40668
  if (!fiscalYearId) {
40566
- const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
40567
- if ("isError" in resolved) return resolved;
40568
- fiscalYearId = resolved.id;
40669
+ const { data: years, error: yearsError } = await supabase2.from("fiscal_years").select("id").eq("company_id", companyId2).eq("is_closed", false).lte("start_date", settlementDate).gte("end_date", settlementDate).order("start_date", { ascending: false }).limit(1);
40670
+ if (yearsError) {
40671
+ return toErrorContent(sanitizeDbError("Failed to query fiscal years", yearsError));
40672
+ }
40673
+ if (!years || years.length === 0) {
40674
+ return toErrorContent(
40675
+ `No open fiscal year contains the settlement date ${settlementDate} (statutory due date for ${period.label}: ${dueDate}). Create that fiscal year or pass settlementDate inside an open fiscal year.`
40676
+ );
40677
+ }
40678
+ fiscalYearId = years[0].id;
40569
40679
  }
40570
- const [year, monthStr] = params.month.split("-");
40571
- const monthNum = Number.parseInt(monthStr, 10);
40572
- const nextMonth = monthNum === 12 ? `${Number.parseInt(year, 10) + 1}-01` : `${year}-${String(monthNum + 1).padStart(2, "0")}`;
40573
- const settlementDate = params.settlementDate ?? `${nextMonth}-12`;
40574
40680
  const { data: fy } = await supabase2.from("fiscal_years").select("start_date, end_date, is_closed").eq("id", fiscalYearId).eq("company_id", companyId2).single();
40575
40681
  if (!fy) {
40576
40682
  return toErrorContent(
@@ -40584,7 +40690,7 @@ function registerBookingTools(server2) {
40584
40690
  }
40585
40691
  if (settlementDate > fy.end_date) {
40586
40692
  return toErrorContent(
40587
- `Settlement date ${settlementDate} is after fiscal year end ${fy.end_date}. Pass an explicit settlementDate within ${fy.start_date}..${fy.end_date} (typical: ${fy.end_date}) \u2014 silent clamping would shift the ALV-kausi.`
40693
+ `Settlement date ${settlementDate} is after fiscal year end ${fy.end_date}. Pass an explicit settlementDate within ${fy.start_date}..${fy.end_date} or the fiscal year that contains ${settlementDate} \u2014 silent clamping would shift the ALV-kausi.`
40588
40694
  );
40589
40695
  }
40590
40696
  if (settlementDate < fy.start_date) {
@@ -40596,9 +40702,14 @@ function registerBookingTools(server2) {
40596
40702
  return toDryRunResponse("kirjapro_book_vat_settlement", {
40597
40703
  action: "book_vat_settlement",
40598
40704
  month: params.month,
40705
+ vat_period: companyVat.vatPeriod,
40706
+ period_start: period.start,
40707
+ period_end: period.end,
40599
40708
  settlement_date: settlementDate,
40709
+ statutory_due_date: dueDate,
40710
+ fiscal_year_id: fiscalYearId,
40600
40711
  payment_account: params.paymentAccount,
40601
- hint: "Call kirjapro_get_alv_summary with the same month to preview the concrete net VAT before committing."
40712
+ hint: `Call kirjapro_get_alv_summary for ${period.start}..${period.end} to preview the net VAT and its reconciliation before committing.`
40602
40713
  });
40603
40714
  }
40604
40715
  return withIdempotency(
@@ -40621,20 +40732,27 @@ function registerBookingTools(server2) {
40621
40732
  );
40622
40733
  }
40623
40734
  const result = data;
40735
+ const periodStart = result.period_start ?? period.start;
40736
+ const periodEnd = result.period_end ?? period.end;
40624
40737
  if (result.direction === "none") {
40625
40738
  return toResult({
40626
- message: result.message ?? `No VAT entries found for ${params.month}. Nothing to settle.`,
40739
+ message: result.message ?? `No VAT entries found for ${period.label}. Nothing to settle.`,
40627
40740
  alv_saamiset: 0,
40628
- alv_velka: 0
40741
+ alv_velka: 0,
40742
+ period_start: periodStart,
40743
+ period_end: periodEnd
40629
40744
  });
40630
40745
  }
40631
40746
  return toResult({
40632
- message: `VAT settlement booked for ${params.month}`,
40747
+ message: `VAT settlement booked for ${period.label}`,
40633
40748
  transaction_id: result.transaction_id,
40634
40749
  alv_saamiset: result.alv_saamiset,
40635
40750
  alv_velka: result.alv_velka,
40636
40751
  net_vat: result.net_vat,
40637
- direction: result.direction
40752
+ direction: result.direction,
40753
+ period_start: periodStart,
40754
+ period_end: periodEnd,
40755
+ settlement_date: settlementDate
40638
40756
  });
40639
40757
  }
40640
40758
  );
@@ -40643,7 +40761,7 @@ function registerBookingTools(server2) {
40643
40761
  server2.registerTool(
40644
40762
  "kirjapro_book_sale",
40645
40763
  {
40646
- description: "Book a sale (myynti) with automatic VAT splitting. Payment types: invoice (default, D Myyntisaamiset), cash (D bank), prepaid.",
40764
+ description: "Book a sale (myynti) with automatic VAT splitting for a company that is not VAT-registered. Payment types: invoice (default, D Myyntisaamiset), cash (D bank), prepaid. A VAT-registered (ALV) company is refused: its sales reach the ALV report only as invoices (kirjapro_create_invoice + kirjapro_send_invoice).",
40647
40765
  inputSchema: {
40648
40766
  amount: external_exports.number().finite().positive().max(1e9).describe("Gross EUR amount (VAT-inclusive)"),
40649
40767
  description: external_exports.string().min(1).max(500).describe('Transaction description (e.g. "Konsultointi Acme Oy, 40h \xD7 95 \u20AC")'),
@@ -40660,6 +40778,9 @@ function registerBookingTools(server2) {
40660
40778
  annotations: WRITE_ANNOTATIONS
40661
40779
  },
40662
40780
  async (params) => {
40781
+ const companyVat = await readCompanyVat(getSupabase(), getCompanyId());
40782
+ if ("isError" in companyVat) return companyVat;
40783
+ if (companyVat.alvRegistered) return toErrorContent(ALV_REGISTERED_SALE_REFUSAL);
40663
40784
  const rate = Number.parseFloat(params.vatRate);
40664
40785
  const gross = roundCents(params.amount);
40665
40786
  let entries;
@@ -42647,7 +42768,7 @@ var INSTRUCTIONS_CORE_ADDENDUM = `
42647
42768
  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:
42648
42769
 
42649
42770
  - **Recurring invoice templates** \u2014 created and managed in the web app; cron generates the issued invoices
42650
- - **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
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
42651
42772
  - **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
42652
42773
  - **Transaction templates** \u2014 manual journal entries via \`kirjapro_create_transaction\`
42653
42774
  - **Manual offer status changes** \u2014 the customer's email-link click sets status server-side; just wait for the user to confirm acceptance
@@ -42666,10 +42787,10 @@ var INSTRUCTIONS_FULL_ADDENDUM = `
42666
42787
 
42667
42788
  ### Booking Shortcuts
42668
42789
  Faster than composing journal entries via \`kirjapro_create_transaction\`:
42669
- - \`kirjapro_book_sale\` / \`kirjapro_book_purchase\` \u2014 one-liner with auto VAT splitting
42670
- - \`kirjapro_book_vat_settlement\` \u2014 reads ALV-saamiset/ALV-velka, creates settlement entry. Call \`kirjapro_get_alv_summary\` first to preview.
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
42791
+ - \`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.
42671
42792
  - **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\`.
42672
- - \`kirjapro_list_templates\` \u2192 \`kirjapro_create_from_template\` for common patterns (13 built-in templates)
42793
+ - \`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)
42673
42794
 
42674
42795
  ### Manual Offer Status
42675
42796
  - \`kirjapro_update_offer_status\` \u2014 manual override; usually the customer's email-link click handles accept/reject server-side
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirjapro/mcp-server",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "MCP server for Finnish accounting — exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude, ChatGPT, Mistral, and other MCP clients.",
5
5
  "type": "module",
6
6
  "bin": {
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@kirjapro/mcp-server",
3
3
  "displayName": "Kirjapro - Finnish Accounting",
4
4
  "description": "Finnish double-entry bookkeeping, invoicing, and financial reporting for growing businesses. Create transactions, generate invoices with VAT calculation, produce income statements and balance sheets, and validate Finnish business identifiers.",
5
- "version": "0.9.1",
5
+ "version": "0.10.0",
6
6
  "homepage": "https://kirjapro.fi",
7
7
  "repository": "https://github.com/Jaollo/kirja-pro",
8
8
  "license": "MIT",
@@ -2,15 +2,15 @@
2
2
  "fingerprints": [
3
3
  {
4
4
  "name": "kirjapro_book_purchase",
5
- "hash": "21897b2fd6bd8809"
5
+ "hash": "ec096990916f5112"
6
6
  },
7
7
  {
8
8
  "name": "kirjapro_book_sale",
9
- "hash": "a7ad4daef0e9947b"
9
+ "hash": "6868bdf46073f442"
10
10
  },
11
11
  {
12
12
  "name": "kirjapro_book_vat_settlement",
13
- "hash": "48ab541acbce9e66"
13
+ "hash": "39f807857fb81e5d"
14
14
  },
15
15
  {
16
16
  "name": "kirjapro_convert_offer_to_invoice",
@@ -22,11 +22,11 @@
22
22
  },
23
23
  {
24
24
  "name": "kirjapro_create_from_template",
25
- "hash": "62d7241dbdba511c"
25
+ "hash": "9e774f4f25c6ce93"
26
26
  },
27
27
  {
28
28
  "name": "kirjapro_create_from_template_batch",
29
- "hash": "f10cad13dc519c7c"
29
+ "hash": "415d1d219c81467f"
30
30
  },
31
31
  {
32
32
  "name": "kirjapro_create_invoice",
@@ -42,11 +42,11 @@
42
42
  },
43
43
  {
44
44
  "name": "kirjapro_create_transaction",
45
- "hash": "88b052da9babfc50"
45
+ "hash": "bea9ccf179928813"
46
46
  },
47
47
  {
48
48
  "name": "kirjapro_create_transactions_batch",
49
- "hash": "da853c41adf6e4dd"
49
+ "hash": "160de9628d56e6be"
50
50
  },
51
51
  {
52
52
  "name": "kirjapro_delete_invoice",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  {
68
68
  "name": "kirjapro_get_alv_summary",
69
- "hash": "139628cea5a3df31"
69
+ "hash": "1c225c5d61c0839f"
70
70
  },
71
71
  {
72
72
  "name": "kirjapro_get_ar_aging",
@@ -162,7 +162,7 @@
162
162
  },
163
163
  {
164
164
  "name": "kirjapro_list_templates",
165
- "hash": "e07b6d21afbccce9"
165
+ "hash": "7601b5d1da0d7283"
166
166
  },
167
167
  {
168
168
  "name": "kirjapro_list_transactions",