@kirjapro/mcp-server 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/index.js +481 -290
- package/package.json +1 -1
- package/server.json +1 -1
- package/tool-fingerprints.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0] - 2026-10-04
|
|
4
|
+
|
|
5
|
+
No database prerequisite: every function, rule and VAT treatment it uses was
|
|
6
|
+
already live with 0.12.0, and 0.12.0 remains compatible with the same database.
|
|
7
|
+
|
|
8
|
+
- `kirjapro_create_invoice` runs on the invoice-draft core that Kirjapro's AI
|
|
9
|
+
Chat also uses (`src/shared/invoice-draft.ts`, generated from the repository's
|
|
10
|
+
`src/lib/invoice-draft.ts`). Line amounts round like the database (numeric
|
|
11
|
+
`ROUND`, half away from zero), so 3 × 1.255 € returns 3.77 €, as stored;
|
|
12
|
+
0.12.0 returned 3.76 €.
|
|
13
|
+
- The database allocates the invoice and reference number in the creation
|
|
14
|
+
transaction. The server no longer reads `get_next_invoice_number` first, and
|
|
15
|
+
the success message names the stored number.
|
|
16
|
+
- Errors are a stable code with a Finnish explanation, `CODE: viesti` (for
|
|
17
|
+
example `VAT_REQUIRES_EU_VAT_NUMBER: …` or `FISCAL_YEAR_CLOSED: …`), like the
|
|
18
|
+
existing `IDEMPOTENCY_*:` errors. Other tools keep their English messages.
|
|
19
|
+
- A lost database response returns `IDEMPOTENCY_OUTCOME_UNKNOWN` and leaves the
|
|
20
|
+
idempotency claim pending, so a retry with the same key never creates a
|
|
21
|
+
second invoice. 0.12.0 cached it as an ordinary failure.
|
|
22
|
+
- When the invoice is created but reading it back fails, the result is a
|
|
23
|
+
success with a `warning` and the invoice id, cached like any success.
|
|
24
|
+
- `paymentTermDays` must be a whole number of days (0–365). It still defaults
|
|
25
|
+
to 14, so the customer's own payment term applies only when it is passed.
|
|
26
|
+
- Returned `lines` no longer echo `reverse_charge_confirmation_text`; the
|
|
27
|
+
database sets the invoice's confirmation text.
|
|
28
|
+
- `kirjapro_send_invoice` declares `openWorldHint: true`: email and e-invoice
|
|
29
|
+
delivery are external effects. Only its fingerprint changes; the
|
|
30
|
+
`kirjapro_create_invoice` input schema and fingerprint are unchanged.
|
|
31
|
+
- `kirjapro://compliance/vat`: the reverse-charge `implementation_limits` now
|
|
32
|
+
says that `kirjapro_book_purchase` takes `deductionPercent` 0–100 with a
|
|
33
|
+
documented reason, also for reverse charge. 0.12.0 still said that the
|
|
34
|
+
parameter did not exist.
|
|
35
|
+
|
|
3
36
|
## [0.12.0] - 2026-10-04
|
|
4
37
|
|
|
5
38
|
Requires deployment of all seven migrations in the compliance batch before release:
|
package/dist/index.js
CHANGED
|
@@ -35679,7 +35679,7 @@ var StdioServerTransport = class {
|
|
|
35679
35679
|
import { createClient } from "@supabase/supabase-js";
|
|
35680
35680
|
|
|
35681
35681
|
// src/version.ts
|
|
35682
|
-
var VERSION = "0.
|
|
35682
|
+
var VERSION = "0.13.0";
|
|
35683
35683
|
|
|
35684
35684
|
// src/auth.ts
|
|
35685
35685
|
var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
|
|
@@ -36392,18 +36392,6 @@ function tagCustomer(row) {
|
|
|
36392
36392
|
if ("notes" in row) wrapped.notes = tagUntrusted(row.notes);
|
|
36393
36393
|
return wrapped;
|
|
36394
36394
|
}
|
|
36395
|
-
function addDaysToIsoDate(date5, days) {
|
|
36396
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date5);
|
|
36397
|
-
if (!match) {
|
|
36398
|
-
throw new Error(`Invalid ISO date: ${date5}`);
|
|
36399
|
-
}
|
|
36400
|
-
const year = Number.parseInt(match[1], 10);
|
|
36401
|
-
const month = Number.parseInt(match[2], 10);
|
|
36402
|
-
const day = Number.parseInt(match[3], 10);
|
|
36403
|
-
const utcDate = new Date(Date.UTC(year, month - 1, day));
|
|
36404
|
-
utcDate.setUTCDate(utcDate.getUTCDate() + days);
|
|
36405
|
-
return utcDate.toISOString().slice(0, 10);
|
|
36406
|
-
}
|
|
36407
36395
|
function formatFinnishDate(iso) {
|
|
36408
36396
|
const [y, m, d] = iso.split("-");
|
|
36409
36397
|
return `${Number.parseInt(d, 10)}.${Number.parseInt(m, 10)}.${y}`;
|
|
@@ -37307,45 +37295,123 @@ var VALID_TRANSITIONS = {
|
|
|
37307
37295
|
cancelled: []
|
|
37308
37296
|
};
|
|
37309
37297
|
|
|
37310
|
-
// src/
|
|
37311
|
-
|
|
37312
|
-
|
|
37313
|
-
|
|
37314
|
-
|
|
37315
|
-
|
|
37316
|
-
|
|
37317
|
-
|
|
37318
|
-
|
|
37319
|
-
|
|
37320
|
-
|
|
37321
|
-
|
|
37322
|
-
|
|
37323
|
-
|
|
37324
|
-
|
|
37325
|
-
|
|
37326
|
-
|
|
37327
|
-
|
|
37328
|
-
|
|
37329
|
-
|
|
37330
|
-
|
|
37331
|
-
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
37335
|
-
|
|
37336
|
-
|
|
37337
|
-
|
|
37338
|
-
|
|
37339
|
-
|
|
37340
|
-
|
|
37341
|
-
|
|
37342
|
-
|
|
37343
|
-
|
|
37344
|
-
|
|
37298
|
+
// src/shared/invoice-draft.ts
|
|
37299
|
+
var INVOICE_VAT_TREATMENT_CODES = [
|
|
37300
|
+
"standard_25_5",
|
|
37301
|
+
"standard_13_5",
|
|
37302
|
+
"standard_10",
|
|
37303
|
+
"not_vat_registered",
|
|
37304
|
+
"reverse_construction",
|
|
37305
|
+
"reverse_scrap",
|
|
37306
|
+
"reverse_eu_service",
|
|
37307
|
+
"export_outside_eu",
|
|
37308
|
+
"service_outside_eu",
|
|
37309
|
+
"intra_eu_goods",
|
|
37310
|
+
"exempt_healthcare",
|
|
37311
|
+
"exempt_social_welfare",
|
|
37312
|
+
"exempt_education",
|
|
37313
|
+
"exempt_financial",
|
|
37314
|
+
"exempt_insurance",
|
|
37315
|
+
"exempt_other"
|
|
37316
|
+
];
|
|
37317
|
+
var INVOICE_VAT_RATES = ["25.5", "13.5", "10", "0"];
|
|
37318
|
+
var INVOICE_DRAFT_MAX_LINES = 100;
|
|
37319
|
+
var INVOICE_DRAFT_CUSTOMER_COLUMNS = "id, name, payment_term_days, customer_kind, business_id, vat_number, country";
|
|
37320
|
+
var ERROR_MESSAGES = {
|
|
37321
|
+
INVALID_INPUT: (detail) => `Laskun tiedoissa on virhe: ${detail}.`,
|
|
37322
|
+
CUSTOMER_NOT_FOUND: (detail) => `Asiakasta ei l\xF6ydy t\xE4st\xE4 yrityksest\xE4${detail ? ` (${detail})` : ""}.`,
|
|
37323
|
+
CUSTOMER_AMBIGUOUS: (detail) => `Useita asiakkaita osuu hakuun: ${detail}. Tarkenna nime\xE4.`,
|
|
37324
|
+
LOOKUP_FAILED: () => "Laskun tietojen haku ep\xE4onnistui. Mit\xE4\xE4n ei tallennettu; voit yritt\xE4\xE4 uudelleen.",
|
|
37325
|
+
FISCAL_YEAR_REQUIRED: () => "Ei tilikautta valittuna.",
|
|
37326
|
+
FISCAL_YEAR_NOT_FOUND: () => "Tilikautta ei l\xF6ydy. Tarkista valinta.",
|
|
37327
|
+
FISCAL_YEAR_CLOSED: () => "Tilikausi on suljettu. Laskua ei voi luoda.",
|
|
37328
|
+
VAT_TREATMENT_REQUIRED: () => "0 % ALV vaatii t\xE4sm\xE4llisen ALV-k\xE4sittelyn syyn.",
|
|
37329
|
+
VAT_SELLER_NOT_REGISTERED: () => "ALV-rekisteriin kuulumaton myyj\xE4 voi k\xE4ytt\xE4\xE4 vain Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
|
|
37330
|
+
VAT_SELLER_REGISTERED: () => "ALV-rekisteriss\xE4 oleva myyj\xE4 ei voi k\xE4ytt\xE4\xE4 Ei ALV-rekisteriss\xE4 -k\xE4sittely\xE4.",
|
|
37331
|
+
VAT_RATE_NOT_IN_FORCE: (detail) => `ALV-kanta ${detail}.`,
|
|
37332
|
+
VAT_REQUIRES_BUSINESS_CUSTOMER: () => "Valittu ALV-k\xE4sittely vaatii yritysasiakkaan.",
|
|
37333
|
+
VAT_REQUIRES_FINNISH_BUSINESS_ID: () => "Kotimainen k\xE4\xE4nnetty verovelvollisuus vaatii ostajan kelvollisen Y-tunnuksen.",
|
|
37334
|
+
VAT_REQUIRES_EU_VAT_NUMBER: () => "EU-myynti vaatii muun EU-maan maakoodin ja sit\xE4 vastaavan ostajan ALV-tunnisteen.",
|
|
37335
|
+
VAT_REQUIRES_NON_EU_COUNTRY: () => "EU:n ulkopuolinen myynti vaatii ostajalle EU:n ulkopuolisen maan.",
|
|
37336
|
+
INVOICE_NUMBER_CONFLICT: () => "Laskunumero on jo k\xE4yt\xF6ss\xE4 (samanaikainen luonti). Yrit\xE4 uudelleen.",
|
|
37337
|
+
INVALID_LINE: () => "Laskurivi ei kelpaa. Tarkista m\xE4\xE4r\xE4, hinta ja ALV-k\xE4sittely.",
|
|
37338
|
+
PERMISSION_DENIED: () => "Ei oikeutta luoda laskua t\xE4lle yritykselle.",
|
|
37339
|
+
MFA_REQUIRED: () => "Vahvista kaksivaiheinen tunnistautuminen Kirjaprossa ja yrit\xE4 sitten uudelleen.",
|
|
37340
|
+
WRITE_REJECTED: () => "Laskun luonti hyl\xE4ttiin. Mit\xE4\xE4n ei tallennettu.",
|
|
37341
|
+
OUTCOME_UNKNOWN: () => "Laskun luonnin lopputulosta ei voitu varmistaa. Tarkista laskulista ennen uutta yrityst\xE4."
|
|
37342
|
+
};
|
|
37343
|
+
function retryFor(code) {
|
|
37344
|
+
if (code === "OUTCOME_UNKNOWN") return "reconcile";
|
|
37345
|
+
return code === "LOOKUP_FAILED" || code === "INVOICE_NUMBER_CONFLICT" ? "safe" : "never";
|
|
37346
|
+
}
|
|
37347
|
+
function invoiceDraftError(code, detail = "") {
|
|
37348
|
+
return { code, message: ERROR_MESSAGES[code](detail), retry: retryFor(code) };
|
|
37349
|
+
}
|
|
37350
|
+
function roundNumericMoney(value) {
|
|
37351
|
+
if (!Number.isFinite(value)) return value;
|
|
37352
|
+
if (Math.abs(value) >= 9e13) return Math.round(value * 100) / 100;
|
|
37353
|
+
const digits = Math.abs(value).toPrecision(15);
|
|
37354
|
+
if (digits.includes("e")) return 0;
|
|
37355
|
+
const [whole = "0", fraction = ""] = digits.split(".");
|
|
37356
|
+
const cents2 = Number(whole) * 100 + Number(fraction.slice(0, 2).padEnd(2, "0")) + (Number(fraction.charAt(2) || "0") >= 5 ? 1 : 0);
|
|
37357
|
+
if (cents2 === 0) return 0;
|
|
37358
|
+
return value < 0 ? -cents2 / 100 : cents2 / 100;
|
|
37359
|
+
}
|
|
37360
|
+
function calculateLineTotals(quantity, unitPrice, vatRate, priceEntryMode = "vat_exclusive", discountPercent = 0) {
|
|
37361
|
+
const discountFactor = 1 - discountPercent / 100;
|
|
37362
|
+
if (priceEntryMode === "vat_inclusive" && vatRate > 0) {
|
|
37363
|
+
const total3 = roundNumericMoney(quantity * unitPrice * discountFactor);
|
|
37364
|
+
const vatAmount2 = roundNumericMoney(total3 * vatRate / (100 + vatRate));
|
|
37365
|
+
const subtotal2 = roundNumericMoney(total3 - vatAmount2);
|
|
37366
|
+
return { subtotal: subtotal2, vatAmount: vatAmount2, total: total3 };
|
|
37367
|
+
}
|
|
37368
|
+
const subtotal = roundNumericMoney(quantity * unitPrice * discountFactor);
|
|
37369
|
+
const vatAmount = roundNumericMoney(subtotal * (vatRate / 100));
|
|
37370
|
+
const total2 = roundNumericMoney(subtotal + vatAmount);
|
|
37371
|
+
return { subtotal, vatAmount, total: total2 };
|
|
37372
|
+
}
|
|
37373
|
+
function calculateInvoiceTotals(lines) {
|
|
37374
|
+
const subtotalCents = lines.reduce((sum, line) => sum + Math.round(line.subtotal * 100), 0);
|
|
37375
|
+
const vatTotalCents = lines.reduce((sum, line) => sum + Math.round(line.vat_amount * 100), 0);
|
|
37376
|
+
const totalCents = lines.reduce((sum, line) => sum + Math.round(line.total * 100), 0);
|
|
37377
|
+
return {
|
|
37378
|
+
subtotal: subtotalCents / 100,
|
|
37379
|
+
vatTotal: vatTotalCents / 100,
|
|
37380
|
+
total: totalCents / 100
|
|
37381
|
+
};
|
|
37345
37382
|
}
|
|
37346
|
-
|
|
37347
|
-
|
|
37348
|
-
|
|
37383
|
+
var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
37384
|
+
function dateParts(date5) {
|
|
37385
|
+
const match = ISO_DATE.exec(date5);
|
|
37386
|
+
if (!match) return null;
|
|
37387
|
+
const year = Number(match[1]);
|
|
37388
|
+
const month = Number(match[2]);
|
|
37389
|
+
const day = Number(match[3]);
|
|
37390
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
37391
|
+
return utc.getUTCFullYear() === year && utc.getUTCMonth() === month - 1 && utc.getUTCDate() === day ? [year, month, day] : null;
|
|
37392
|
+
}
|
|
37393
|
+
function addDaysIso(date5, days) {
|
|
37394
|
+
const parts = dateParts(date5);
|
|
37395
|
+
if (!parts) throw new RangeError(`Invalid date: ${date5}`);
|
|
37396
|
+
const [year, month, day] = parts;
|
|
37397
|
+
return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
|
|
37398
|
+
}
|
|
37399
|
+
var TREATMENT_RATES = {
|
|
37400
|
+
standard_25_5: 25.5,
|
|
37401
|
+
standard_13_5: 13.5,
|
|
37402
|
+
standard_10: 10
|
|
37403
|
+
};
|
|
37404
|
+
var STANDARD_TREATMENTS = {
|
|
37405
|
+
"25.5": "standard_25_5",
|
|
37406
|
+
"13.5": "standard_13_5",
|
|
37407
|
+
"10": "standard_10"
|
|
37408
|
+
};
|
|
37409
|
+
function resolveInvoiceVatTreatment(vatRate, code) {
|
|
37410
|
+
const resolved = code ?? (vatRate === void 0 ? void 0 : STANDARD_TREATMENTS[vatRate]);
|
|
37411
|
+
if (!resolved) return invoiceDraftError("VAT_TREATMENT_REQUIRED");
|
|
37412
|
+
return { code: resolved, rate: TREATMENT_RATES[resolved] ?? 0 };
|
|
37413
|
+
}
|
|
37414
|
+
var EU_COUNTRY_CODES = [
|
|
37349
37415
|
"AT",
|
|
37350
37416
|
"BE",
|
|
37351
37417
|
"BG",
|
|
@@ -37373,51 +37439,164 @@ var EU_COUNTRY_CODES = /* @__PURE__ */ new Set([
|
|
|
37373
37439
|
"SE",
|
|
37374
37440
|
"SI",
|
|
37375
37441
|
"SK"
|
|
37376
|
-
]
|
|
37377
|
-
function
|
|
37378
|
-
|
|
37379
|
-
|
|
37380
|
-
|
|
37381
|
-
|
|
37382
|
-
|
|
37383
|
-
|
|
37384
|
-
|
|
37385
|
-
|
|
37386
|
-
if (
|
|
37387
|
-
|
|
37388
|
-
|
|
37389
|
-
|
|
37390
|
-
|
|
37391
|
-
|
|
37392
|
-
if (
|
|
37393
|
-
return "
|
|
37394
|
-
|
|
37395
|
-
|
|
37396
|
-
|
|
37397
|
-
|
|
37398
|
-
|
|
37399
|
-
|
|
37400
|
-
|
|
37401
|
-
|
|
37402
|
-
|
|
37403
|
-
return "Domestic reverse charge requires a Finnish buyer business ID in the form 1234567-8.";
|
|
37404
|
-
}
|
|
37442
|
+
];
|
|
37443
|
+
function isFinnishBusinessId(value) {
|
|
37444
|
+
const match = /^(\d{7})-(\d)$/.exec(value.replace(/\s/g, ""));
|
|
37445
|
+
if (!match) return false;
|
|
37446
|
+
const digits = match[1] ?? "";
|
|
37447
|
+
const weights = [7, 9, 10, 5, 8, 4, 2];
|
|
37448
|
+
let sum = 0;
|
|
37449
|
+
for (let index = 0; index < weights.length; index += 1)
|
|
37450
|
+
sum += Number(digits.charAt(index)) * (weights[index] ?? 0);
|
|
37451
|
+
const remainder = sum % 11;
|
|
37452
|
+
if (remainder === 1) return false;
|
|
37453
|
+
return Number(match[2]) === (remainder === 0 ? 0 : 11 - remainder);
|
|
37454
|
+
}
|
|
37455
|
+
function invoiceVatTreatmentError(code, sellerAlvRegistered, customer, documentDate) {
|
|
37456
|
+
if (!sellerAlvRegistered && code !== "not_vat_registered")
|
|
37457
|
+
return invoiceDraftError("VAT_SELLER_NOT_REGISTERED");
|
|
37458
|
+
if (sellerAlvRegistered && code === "not_vat_registered")
|
|
37459
|
+
return invoiceDraftError("VAT_SELLER_REGISTERED");
|
|
37460
|
+
if (code === "standard_25_5" && documentDate < "2024-09-01")
|
|
37461
|
+
return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "25,5 % ei ole voimassa ennen 1.9.2024");
|
|
37462
|
+
if (code === "standard_13_5" && documentDate < "2026-01-01")
|
|
37463
|
+
return invoiceDraftError("VAT_RATE_NOT_IN_FORCE", "13,5 % ei ole voimassa ennen 1.1.2026");
|
|
37464
|
+
if ((code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu") && customer.customer_kind !== "business")
|
|
37465
|
+
return invoiceDraftError("VAT_REQUIRES_BUSINESS_CUSTOMER");
|
|
37466
|
+
if ((code === "reverse_construction" || code === "reverse_scrap") && !isFinnishBusinessId(customer.business_id ?? ""))
|
|
37467
|
+
return invoiceDraftError("VAT_REQUIRES_FINNISH_BUSINESS_ID");
|
|
37468
|
+
const country = (customer.country ?? "").trim().toUpperCase();
|
|
37405
37469
|
if (code === "reverse_eu_service" || code === "intra_eu_goods") {
|
|
37406
|
-
const
|
|
37407
|
-
const vatNumber = normalizedVatNumber(customer?.vat_number);
|
|
37470
|
+
const vatNumber = (customer.vat_number ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
37408
37471
|
const expectedPrefix = country === "GR" ? "EL" : country;
|
|
37409
|
-
if (!EU_COUNTRY_CODES.
|
|
37410
|
-
return "
|
|
37411
|
-
}
|
|
37472
|
+
if (!EU_COUNTRY_CODES.includes(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix))
|
|
37473
|
+
return invoiceDraftError("VAT_REQUIRES_EU_VAT_NUMBER");
|
|
37412
37474
|
}
|
|
37413
|
-
if (code === "export_outside_eu" || code === "service_outside_eu")
|
|
37414
|
-
|
|
37415
|
-
|
|
37416
|
-
|
|
37417
|
-
|
|
37475
|
+
if ((code === "export_outside_eu" || code === "service_outside_eu") && (!country || EU_COUNTRY_CODES.includes(country)))
|
|
37476
|
+
return invoiceDraftError("VAT_REQUIRES_NON_EU_COUNTRY");
|
|
37477
|
+
return null;
|
|
37478
|
+
}
|
|
37479
|
+
function inputError(input2) {
|
|
37480
|
+
if (!dateParts(input2.invoiceDate)) return "laskun p\xE4iv\xE4m\xE4\xE4r\xE4 ei ole kelvollinen";
|
|
37481
|
+
if (input2.lines.length < 1 || input2.lines.length > INVOICE_DRAFT_MAX_LINES)
|
|
37482
|
+
return `laskulla pit\xE4\xE4 olla 1\u2013${INVOICE_DRAFT_MAX_LINES} rivi\xE4`;
|
|
37483
|
+
const term = input2.paymentTermDays;
|
|
37484
|
+
if (term !== void 0 && (!Number.isInteger(term) || term < 0 || term > 365))
|
|
37485
|
+
return "maksuehdon pit\xE4\xE4 olla 0\u2013365 kokonaista p\xE4iv\xE4\xE4";
|
|
37486
|
+
if ((input2.notes ?? "").length > 2e3) return "lis\xE4tiedot ovat yli 2000 merkki\xE4";
|
|
37487
|
+
for (let index = 0; index < input2.lines.length; index += 1) {
|
|
37488
|
+
const line = input2.lines[index];
|
|
37489
|
+
const row = `rivi ${index + 1}`;
|
|
37490
|
+
if (!line) return `${row} puuttuu`;
|
|
37491
|
+
if (!line.description.trim() || line.description.length > 500)
|
|
37492
|
+
return `${row}: kuvauksen pit\xE4\xE4 olla 1\u2013500 merkki\xE4`;
|
|
37493
|
+
if (line.unit.length > 20) return `${row}: yksikk\xF6 on yli 20 merkki\xE4`;
|
|
37494
|
+
if (!Number.isFinite(line.quantity) || line.quantity <= 0 || line.quantity > 1e6)
|
|
37495
|
+
return `${row}: m\xE4\xE4r\xE4n pit\xE4\xE4 olla yli 0 ja enint\xE4\xE4n 1 000 000`;
|
|
37496
|
+
if (!Number.isFinite(line.unitPrice) || line.unitPrice < 0 || line.unitPrice > 1e9)
|
|
37497
|
+
return `${row}: yksikk\xF6hinnan pit\xE4\xE4 olla 0\u20131 000 000 000 \u20AC`;
|
|
37498
|
+
if (line.quantity * line.unitPrice > 1e12) return `${row}: rivin summa on liian suuri`;
|
|
37418
37499
|
}
|
|
37419
37500
|
return null;
|
|
37420
37501
|
}
|
|
37502
|
+
function prepareInvoiceDraft(input2, facts) {
|
|
37503
|
+
const invalid = inputError(input2);
|
|
37504
|
+
if (invalid) return { ok: false, error: invoiceDraftError("INVALID_INPUT", invalid) };
|
|
37505
|
+
const { customer } = facts;
|
|
37506
|
+
const lines = [];
|
|
37507
|
+
for (const [index, line] of input2.lines.entries()) {
|
|
37508
|
+
const treatment = resolveInvoiceVatTreatment(line.vatRate, line.vatTreatmentCode);
|
|
37509
|
+
if ("message" in treatment) return { ok: false, error: treatment };
|
|
37510
|
+
const ineligible = invoiceVatTreatmentError(
|
|
37511
|
+
treatment.code,
|
|
37512
|
+
facts.sellerAlvRegistered,
|
|
37513
|
+
customer,
|
|
37514
|
+
input2.invoiceDate
|
|
37515
|
+
);
|
|
37516
|
+
if (ineligible) return { ok: false, error: ineligible };
|
|
37517
|
+
const amounts = calculateLineTotals(
|
|
37518
|
+
line.quantity,
|
|
37519
|
+
line.unitPrice,
|
|
37520
|
+
treatment.rate,
|
|
37521
|
+
input2.priceEntryMode
|
|
37522
|
+
);
|
|
37523
|
+
lines.push({
|
|
37524
|
+
line_number: index + 1,
|
|
37525
|
+
description: line.description,
|
|
37526
|
+
quantity: line.quantity,
|
|
37527
|
+
unit: line.unit,
|
|
37528
|
+
unit_price: line.unitPrice,
|
|
37529
|
+
entered_unit_price: line.unitPrice,
|
|
37530
|
+
vat_rate: treatment.rate,
|
|
37531
|
+
vat_treatment_code: treatment.code,
|
|
37532
|
+
vat_treatment_overridden: true,
|
|
37533
|
+
price_entry_mode: input2.priceEntryMode,
|
|
37534
|
+
default_vat_treatment_code: treatment.code,
|
|
37535
|
+
reverse_charge_confirmed: input2.reverseChargeConfirmed ?? false,
|
|
37536
|
+
eu_goods_transport_confirmed: input2.euGoodsTransportConfirmed ?? false,
|
|
37537
|
+
subtotal: amounts.subtotal,
|
|
37538
|
+
vat_amount: amounts.vatAmount,
|
|
37539
|
+
total: amounts.total
|
|
37540
|
+
});
|
|
37541
|
+
}
|
|
37542
|
+
const totals = calculateInvoiceTotals(lines);
|
|
37543
|
+
return {
|
|
37544
|
+
ok: true,
|
|
37545
|
+
draft: {
|
|
37546
|
+
customerId: customer.id,
|
|
37547
|
+
customerName: customer.name,
|
|
37548
|
+
invoiceDate: input2.invoiceDate,
|
|
37549
|
+
dueDate: addDaysIso(
|
|
37550
|
+
input2.invoiceDate,
|
|
37551
|
+
input2.paymentTermDays ?? customer.payment_term_days ?? 14
|
|
37552
|
+
),
|
|
37553
|
+
notes: input2.notes ?? "",
|
|
37554
|
+
lines,
|
|
37555
|
+
subtotal: totals.subtotal,
|
|
37556
|
+
vatTotal: totals.vatTotal,
|
|
37557
|
+
total: totals.total
|
|
37558
|
+
}
|
|
37559
|
+
};
|
|
37560
|
+
}
|
|
37561
|
+
function invoiceDraftRpcArgs(companyId2, draft, audit) {
|
|
37562
|
+
return {
|
|
37563
|
+
p_company_id: companyId2,
|
|
37564
|
+
p_customer_id: draft.customerId,
|
|
37565
|
+
p_invoice_number: null,
|
|
37566
|
+
p_reference_number: null,
|
|
37567
|
+
p_invoice_date: draft.invoiceDate,
|
|
37568
|
+
p_due_date: draft.dueDate,
|
|
37569
|
+
p_status: "draft",
|
|
37570
|
+
p_subtotal: draft.subtotal,
|
|
37571
|
+
p_vat_total: draft.vatTotal,
|
|
37572
|
+
p_total: draft.total,
|
|
37573
|
+
p_notes: draft.notes,
|
|
37574
|
+
p_lines: draft.lines,
|
|
37575
|
+
...audit ? { p_audit_action: audit.action, p_audit_details: audit.details } : {}
|
|
37576
|
+
};
|
|
37577
|
+
}
|
|
37578
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
37579
|
+
var CONFIRMED_ERROR_CODE = /^(?:[0-9A-Z]{5}|PGRST\d{3})$/;
|
|
37580
|
+
function invoiceDraftWriteOutcome(response) {
|
|
37581
|
+
const { data, error: error61 } = response;
|
|
37582
|
+
if (error61) {
|
|
37583
|
+
const code = error61.code ?? "";
|
|
37584
|
+
if (!CONFIRMED_ERROR_CODE.test(code))
|
|
37585
|
+
return { ok: false, error: invoiceDraftError("OUTCOME_UNKNOWN") };
|
|
37586
|
+
if ((error61.message ?? "").startsWith("AAL2_REQUIRED"))
|
|
37587
|
+
return { ok: false, error: invoiceDraftError("MFA_REQUIRED") };
|
|
37588
|
+
const mapped = {
|
|
37589
|
+
"23505": "INVOICE_NUMBER_CONFLICT",
|
|
37590
|
+
"55000": "FISCAL_YEAR_CLOSED",
|
|
37591
|
+
"42501": "PERMISSION_DENIED",
|
|
37592
|
+
"23514": "INVALID_LINE"
|
|
37593
|
+
};
|
|
37594
|
+
return { ok: false, error: invoiceDraftError(mapped[code] ?? "WRITE_REJECTED") };
|
|
37595
|
+
}
|
|
37596
|
+
if (typeof data !== "string" || !UUID.test(data))
|
|
37597
|
+
return { ok: false, error: invoiceDraftError("OUTCOME_UNKNOWN") };
|
|
37598
|
+
return { ok: true, invoiceId: data };
|
|
37599
|
+
}
|
|
37421
37600
|
|
|
37422
37601
|
// src/lib/invoice-balances.ts
|
|
37423
37602
|
var CREDIT_PAGE_SIZE = 500;
|
|
@@ -37455,19 +37634,6 @@ function calculateInvoiceBalance(total2, payments, creditedTotal) {
|
|
|
37455
37634
|
}
|
|
37456
37635
|
|
|
37457
37636
|
// src/tools/invoices.ts
|
|
37458
|
-
function generateReferenceNumber(baseNumber) {
|
|
37459
|
-
const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
|
|
37460
|
-
if (digits.length < 3 || digits.length > 19) {
|
|
37461
|
-
throw new Error("Base number must be 3-19 digits without leading zeros");
|
|
37462
|
-
}
|
|
37463
|
-
const weights = [7, 3, 1];
|
|
37464
|
-
let sum = 0;
|
|
37465
|
-
for (let i = digits.length - 1, w = 0; i >= 0; i--, w++) {
|
|
37466
|
-
sum += parseInt(digits[i]) * weights[w % 3];
|
|
37467
|
-
}
|
|
37468
|
-
const checkDigit = (10 - sum % 10) % 10;
|
|
37469
|
-
return digits + checkDigit;
|
|
37470
|
-
}
|
|
37471
37637
|
var LIST_INVOICES_ROW_SHAPE = {
|
|
37472
37638
|
id: external_exports.string().uuid(),
|
|
37473
37639
|
document_type: external_exports.enum(["invoice", "credit_note"]),
|
|
@@ -37519,6 +37685,96 @@ var GET_INVOICE_OUTPUT_SHAPE = {
|
|
|
37519
37685
|
credited_total: external_exports.number(),
|
|
37520
37686
|
remaining_balance: external_exports.number()
|
|
37521
37687
|
};
|
|
37688
|
+
var CREATE_INVOICE_INPUT_SHAPE = {
|
|
37689
|
+
customerId: external_exports.string().uuid().describe("Customer UUID"),
|
|
37690
|
+
invoiceDate: ISO_DATE_SCHEMA.describe("Invoice date (YYYY-MM-DD)"),
|
|
37691
|
+
paymentTermDays: external_exports.number().min(0).max(365).default(14).describe("Payment term in days (default 14)"),
|
|
37692
|
+
notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
|
|
37693
|
+
priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
|
|
37694
|
+
reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
|
|
37695
|
+
euGoodsTransportConfirmed: external_exports.boolean().default(false).describe("Explicit confirmation that intra-EU goods are transported from Finland to another EU country"),
|
|
37696
|
+
lines: external_exports.array(
|
|
37697
|
+
external_exports.object({
|
|
37698
|
+
description: external_exports.string().min(1).max(500).describe("Line item description"),
|
|
37699
|
+
// DB enforces CHECK (quantity > 0); reject 0/negative/Infinity at
|
|
37700
|
+
// the boundary so the agent gets a clear error instead of an
|
|
37701
|
+
// opaque 23514 deep in create_invoice_with_lines.
|
|
37702
|
+
quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
|
|
37703
|
+
unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
|
|
37704
|
+
unitPrice: external_exports.number().finite().min(0).max(1e9).describe("Entered unit price in EUR (must be >= 0)"),
|
|
37705
|
+
vatRate: external_exports.enum(INVOICE_VAT_RATES).default("25.5").describe("VAT rate"),
|
|
37706
|
+
vatTreatmentCode: external_exports.enum(INVOICE_VAT_TREATMENT_CODES).optional().describe("Structured VAT treatment. Required when VAT rate is 0. service_outside_eu is ONLY for AVL 65 general-rule B2B services: verify business use, evidence of buyer status and receiving establishment outside the EU; special place-of-supply services are excluded. Check foreign buyer liability and invoice notes. PDF delivery only; e-invoice delivery is blocked.")
|
|
37707
|
+
})
|
|
37708
|
+
).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
|
|
37709
|
+
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
|
|
37710
|
+
};
|
|
37711
|
+
async function createInvoiceDraftCommand(ctx, params) {
|
|
37712
|
+
const fail = (error62) => toErrorContent(`${error62.code}: ${error62.message}`);
|
|
37713
|
+
const { data: customer, error: customerError } = await ctx.supabase.from("customers").select(INVOICE_DRAFT_CUSTOMER_COLUMNS).eq("id", params.customerId).eq("company_id", ctx.companyId).maybeSingle();
|
|
37714
|
+
if (customerError) return fail(invoiceDraftError("LOOKUP_FAILED"));
|
|
37715
|
+
if (!customer) {
|
|
37716
|
+
return fail(
|
|
37717
|
+
invoiceDraftError("CUSTOMER_NOT_FOUND", "hae tunniste kirjapro_list_customers-ty\xF6kalulla")
|
|
37718
|
+
);
|
|
37719
|
+
}
|
|
37720
|
+
const { data: company, error: companyError } = await ctx.supabase.from("companies").select("alv_registered").eq("id", ctx.companyId).single();
|
|
37721
|
+
if (companyError || !company) return fail(invoiceDraftError("LOOKUP_FAILED"));
|
|
37722
|
+
const prepared = prepareInvoiceDraft(
|
|
37723
|
+
{
|
|
37724
|
+
invoiceDate: params.invoiceDate,
|
|
37725
|
+
paymentTermDays: params.paymentTermDays,
|
|
37726
|
+
priceEntryMode: params.priceEntryMode,
|
|
37727
|
+
notes: params.notes,
|
|
37728
|
+
reverseChargeConfirmed: params.reverseChargeConfirmed,
|
|
37729
|
+
euGoodsTransportConfirmed: params.euGoodsTransportConfirmed,
|
|
37730
|
+
lines: params.lines
|
|
37731
|
+
},
|
|
37732
|
+
{ sellerAlvRegistered: company.alv_registered, customer }
|
|
37733
|
+
);
|
|
37734
|
+
if (!prepared.ok) return fail(prepared.error);
|
|
37735
|
+
const { draft } = prepared;
|
|
37736
|
+
const { data, error: error61, status } = await ctx.supabase.rpc(
|
|
37737
|
+
"create_invoice_with_lines",
|
|
37738
|
+
// NULL number/reference = DB allocation; generated types do not mark them nullable.
|
|
37739
|
+
invoiceDraftRpcArgs(ctx.companyId, draft)
|
|
37740
|
+
);
|
|
37741
|
+
const outcome = invoiceDraftWriteOutcome({ data, error: error61 });
|
|
37742
|
+
if (isWriteOutcomeUnknown(error61, status) || !outcome.ok && outcome.error.code === "OUTCOME_UNKNOWN") {
|
|
37743
|
+
return toWriteOutcomeUnknown("kirjapro_create_invoice", "kirjapro_list_invoices");
|
|
37744
|
+
}
|
|
37745
|
+
if (!outcome.ok) {
|
|
37746
|
+
console.error("[kirjapro-mcp] create_invoice rejected:", { code: error61?.code });
|
|
37747
|
+
return fail(outcome.error);
|
|
37748
|
+
}
|
|
37749
|
+
const { data: invoice, error: fetchError } = await ctx.supabase.from("invoices").select(
|
|
37750
|
+
"id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at"
|
|
37751
|
+
).eq("id", outcome.invoiceId).eq("company_id", ctx.companyId).single();
|
|
37752
|
+
if (fetchError || !invoice) {
|
|
37753
|
+
return toResult({
|
|
37754
|
+
message: "Lasku luotiin, mutta sen tietojen haku ep\xE4onnistui.",
|
|
37755
|
+
invoice: {
|
|
37756
|
+
id: outcome.invoiceId,
|
|
37757
|
+
status: "draft",
|
|
37758
|
+
invoice_date: draft.invoiceDate,
|
|
37759
|
+
due_date: draft.dueDate,
|
|
37760
|
+
subtotal: draft.subtotal,
|
|
37761
|
+
vat_total: draft.vatTotal,
|
|
37762
|
+
total: draft.total,
|
|
37763
|
+
customer_name: customer.name,
|
|
37764
|
+
lines: draft.lines
|
|
37765
|
+
},
|
|
37766
|
+
warning: "Tarkista lasku kirjapro_get_invoice-ty\xF6kalulla."
|
|
37767
|
+
});
|
|
37768
|
+
}
|
|
37769
|
+
return toResult({
|
|
37770
|
+
message: `Invoice ${invoice.invoice_number} created for ${customer.name}`,
|
|
37771
|
+
invoice: {
|
|
37772
|
+
...invoice,
|
|
37773
|
+
customer_name: customer.name,
|
|
37774
|
+
lines: draft.lines
|
|
37775
|
+
}
|
|
37776
|
+
});
|
|
37777
|
+
}
|
|
37522
37778
|
function registerInvoiceTools(server2) {
|
|
37523
37779
|
server2.registerTool(
|
|
37524
37780
|
"kirjapro_list_invoices",
|
|
@@ -37570,196 +37826,18 @@ function registerInvoiceTools(server2) {
|
|
|
37570
37826
|
"kirjapro_create_invoice",
|
|
37571
37827
|
{
|
|
37572
37828
|
description: "Create a new sales invoice with line items. Automatically generates invoice number, reference number (viitenumero), and calculates VAT. Returns the complete invoice.",
|
|
37573
|
-
inputSchema:
|
|
37574
|
-
customerId: external_exports.string().uuid().describe("Customer UUID"),
|
|
37575
|
-
invoiceDate: ISO_DATE_SCHEMA.describe("Invoice date (YYYY-MM-DD)"),
|
|
37576
|
-
paymentTermDays: external_exports.number().min(0).max(365).default(14).describe("Payment term in days (default 14)"),
|
|
37577
|
-
notes: external_exports.string().max(2e3).optional().describe("Notes printed on the invoice"),
|
|
37578
|
-
priceEntryMode: external_exports.enum(["vat_exclusive", "vat_inclusive"]).default("vat_exclusive").describe("Whether entered unit prices exclude or include VAT"),
|
|
37579
|
-
reverseChargeConfirmed: external_exports.boolean().default(false).describe("Explicit document-level confirmation that the business buyer qualifies for the selected reverse-charge treatment"),
|
|
37580
|
-
euGoodsTransportConfirmed: external_exports.boolean().default(false).describe("Explicit confirmation that intra-EU goods are transported from Finland to another EU country"),
|
|
37581
|
-
lines: external_exports.array(
|
|
37582
|
-
external_exports.object({
|
|
37583
|
-
description: external_exports.string().min(1).max(500).describe("Line item description"),
|
|
37584
|
-
// DB enforces CHECK (quantity > 0); reject 0/negative/Infinity at
|
|
37585
|
-
// the boundary so the agent gets a clear error instead of an
|
|
37586
|
-
// opaque 23514 deep in create_invoice_with_lines.
|
|
37587
|
-
quantity: external_exports.number().finite().positive().max(1e6).describe("Quantity (must be > 0)"),
|
|
37588
|
-
unit: external_exports.string().max(20).default("kpl").describe("Unit (kpl, h, kk, etc.)"),
|
|
37589
|
-
unitPrice: external_exports.number().finite().min(0).max(1e9).describe("Entered unit price in EUR (must be >= 0)"),
|
|
37590
|
-
vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).default("25.5").describe("VAT rate"),
|
|
37591
|
-
vatTreatmentCode: external_exports.enum([
|
|
37592
|
-
"standard_25_5",
|
|
37593
|
-
"standard_13_5",
|
|
37594
|
-
"standard_10",
|
|
37595
|
-
"not_vat_registered",
|
|
37596
|
-
"reverse_construction",
|
|
37597
|
-
"reverse_scrap",
|
|
37598
|
-
"reverse_eu_service",
|
|
37599
|
-
"export_outside_eu",
|
|
37600
|
-
"service_outside_eu",
|
|
37601
|
-
"intra_eu_goods",
|
|
37602
|
-
"exempt_healthcare",
|
|
37603
|
-
"exempt_social_welfare",
|
|
37604
|
-
"exempt_education",
|
|
37605
|
-
"exempt_financial",
|
|
37606
|
-
"exempt_insurance",
|
|
37607
|
-
"exempt_other"
|
|
37608
|
-
]).optional().describe("Structured VAT treatment. Required when VAT rate is 0. service_outside_eu is ONLY for AVL 65 general-rule B2B services: verify business use, evidence of buyer status and receiving establishment outside the EU; special place-of-supply services are excluded. Check foreign buyer liability and invoice notes. PDF delivery only; e-invoice delivery is blocked.")
|
|
37609
|
-
})
|
|
37610
|
-
).min(1).max(MAX_DOCUMENT_LINES).describe(`Invoice line items (max ${MAX_DOCUMENT_LINES})`),
|
|
37611
|
-
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
|
|
37612
|
-
},
|
|
37829
|
+
inputSchema: CREATE_INVOICE_INPUT_SHAPE,
|
|
37613
37830
|
annotations: WRITE_ANNOTATIONS
|
|
37614
37831
|
},
|
|
37615
37832
|
async (params) => {
|
|
37616
|
-
const
|
|
37617
|
-
const companyId2 = getCompanyId();
|
|
37833
|
+
const ctx = { supabase: getSupabase(), companyId: getCompanyId() };
|
|
37618
37834
|
return withIdempotency(
|
|
37619
|
-
|
|
37620
|
-
|
|
37835
|
+
ctx.supabase,
|
|
37836
|
+
ctx.companyId,
|
|
37621
37837
|
"kirjapro_create_invoice",
|
|
37622
37838
|
params.idempotencyKey,
|
|
37623
37839
|
params,
|
|
37624
|
-
|
|
37625
|
-
const { data: customer, error: custError } = await supabase2.from("customers").select("id, name, customer_kind, business_id, vat_number, country").eq("id", params.customerId).eq("company_id", companyId2).single();
|
|
37626
|
-
if (custError || !customer) {
|
|
37627
|
-
return toErrorContent(
|
|
37628
|
-
`Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`
|
|
37629
|
-
);
|
|
37630
|
-
}
|
|
37631
|
-
const { data: company, error: companyError } = await supabase2.from("companies").select("alv_registered").eq("id", companyId2).single();
|
|
37632
|
-
if (companyError || !company) {
|
|
37633
|
-
return toErrorContent("Failed to verify the seller VAT registration status.");
|
|
37634
|
-
}
|
|
37635
|
-
const { data: invoiceNumber, error: numError } = await supabase2.rpc(
|
|
37636
|
-
"get_next_invoice_number",
|
|
37637
|
-
{
|
|
37638
|
-
p_company_id: companyId2
|
|
37639
|
-
}
|
|
37640
|
-
);
|
|
37641
|
-
if (numError || !invoiceNumber) {
|
|
37642
|
-
return toErrorContent(
|
|
37643
|
-
numError ? sanitizeDbError("Failed to generate invoice number", numError) : "Failed to generate invoice number."
|
|
37644
|
-
);
|
|
37645
|
-
}
|
|
37646
|
-
const referenceNumber = generateReferenceNumber(
|
|
37647
|
-
invoiceNumber.replace(/\D/g, "")
|
|
37648
|
-
);
|
|
37649
|
-
const dueDateStr = addDaysToIsoDate(
|
|
37650
|
-
params.invoiceDate,
|
|
37651
|
-
params.paymentTermDays
|
|
37652
|
-
);
|
|
37653
|
-
const treatmentRates = {
|
|
37654
|
-
standard_25_5: 25.5,
|
|
37655
|
-
standard_13_5: 13.5,
|
|
37656
|
-
standard_10: 10,
|
|
37657
|
-
not_vat_registered: 0,
|
|
37658
|
-
reverse_construction: 0,
|
|
37659
|
-
reverse_scrap: 0,
|
|
37660
|
-
reverse_eu_service: 0,
|
|
37661
|
-
export_outside_eu: 0,
|
|
37662
|
-
service_outside_eu: 0,
|
|
37663
|
-
intra_eu_goods: 0,
|
|
37664
|
-
exempt_healthcare: 0,
|
|
37665
|
-
exempt_social_welfare: 0,
|
|
37666
|
-
exempt_education: 0,
|
|
37667
|
-
exempt_financial: 0,
|
|
37668
|
-
exempt_insurance: 0,
|
|
37669
|
-
exempt_other: 0
|
|
37670
|
-
};
|
|
37671
|
-
const resolvedTreatments = params.lines.map((line) => {
|
|
37672
|
-
const inputRate = parseFloat(line.vatRate);
|
|
37673
|
-
const code = line.vatTreatmentCode ?? (inputRate === 25.5 ? "standard_25_5" : inputRate === 13.5 ? "standard_13_5" : inputRate === 10 ? "standard_10" : null);
|
|
37674
|
-
if (!code) return { error: "0 % VAT requires a structured vatTreatmentCode." };
|
|
37675
|
-
const eligibilityError = validateVatTreatmentEligibility({
|
|
37676
|
-
code,
|
|
37677
|
-
sellerAlvRegistered: company.alv_registered,
|
|
37678
|
-
customer,
|
|
37679
|
-
documentDate: params.invoiceDate
|
|
37680
|
-
});
|
|
37681
|
-
if (eligibilityError) return { error: eligibilityError };
|
|
37682
|
-
return { code, rate: treatmentRates[code] ?? inputRate };
|
|
37683
|
-
});
|
|
37684
|
-
const treatmentError = resolvedTreatments.find((item) => "error" in item);
|
|
37685
|
-
if (treatmentError && "error" in treatmentError) {
|
|
37686
|
-
return toErrorContent(treatmentError.error);
|
|
37687
|
-
}
|
|
37688
|
-
const calculatedLines = params.lines.map((line, i) => {
|
|
37689
|
-
const resolved = resolvedTreatments[i];
|
|
37690
|
-
if (!resolved || !("code" in resolved)) throw new Error("VAT treatment validation failed");
|
|
37691
|
-
const vatRate = resolved.rate;
|
|
37692
|
-
const vatTreatmentCode = resolved.code;
|
|
37693
|
-
const gross = roundCents(line.quantity * line.unitPrice);
|
|
37694
|
-
const vatAmount = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross * vatRate / (100 + vatRate)) : roundCents(gross * (vatRate / 100));
|
|
37695
|
-
const subtotal2 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? roundCents(gross - vatAmount) : gross;
|
|
37696
|
-
const total3 = params.priceEntryMode === "vat_inclusive" && vatRate > 0 ? gross : roundCents(subtotal2 + vatAmount);
|
|
37697
|
-
return {
|
|
37698
|
-
line_number: i + 1,
|
|
37699
|
-
description: line.description,
|
|
37700
|
-
quantity: line.quantity,
|
|
37701
|
-
unit: line.unit,
|
|
37702
|
-
unit_price: line.unitPrice,
|
|
37703
|
-
entered_unit_price: line.unitPrice,
|
|
37704
|
-
vat_rate: vatRate,
|
|
37705
|
-
vat_treatment_code: vatTreatmentCode,
|
|
37706
|
-
vat_treatment_overridden: true,
|
|
37707
|
-
price_entry_mode: params.priceEntryMode,
|
|
37708
|
-
default_vat_treatment_code: vatTreatmentCode,
|
|
37709
|
-
reverse_charge_confirmed: params.reverseChargeConfirmed,
|
|
37710
|
-
reverse_charge_confirmation_text: params.reverseChargeConfirmed ? "Buyer qualification explicitly confirmed through MCP invoice creation." : null,
|
|
37711
|
-
eu_goods_transport_confirmed: params.euGoodsTransportConfirmed,
|
|
37712
|
-
subtotal: subtotal2,
|
|
37713
|
-
vat_amount: vatAmount,
|
|
37714
|
-
total: total3
|
|
37715
|
-
};
|
|
37716
|
-
});
|
|
37717
|
-
const subtotal = roundCents(
|
|
37718
|
-
calculatedLines.reduce((s, l) => s + l.subtotal, 0)
|
|
37719
|
-
);
|
|
37720
|
-
const vatTotal = roundCents(
|
|
37721
|
-
calculatedLines.reduce((s, l) => s + l.vat_amount, 0)
|
|
37722
|
-
);
|
|
37723
|
-
const total2 = roundCents(subtotal + vatTotal);
|
|
37724
|
-
const { data: invoiceId, error: invError } = await supabase2.rpc(
|
|
37725
|
-
"create_invoice_with_lines",
|
|
37726
|
-
{
|
|
37727
|
-
p_company_id: companyId2,
|
|
37728
|
-
p_customer_id: params.customerId,
|
|
37729
|
-
p_invoice_number: invoiceNumber,
|
|
37730
|
-
p_reference_number: referenceNumber,
|
|
37731
|
-
p_invoice_date: params.invoiceDate,
|
|
37732
|
-
p_due_date: dueDateStr,
|
|
37733
|
-
p_status: "draft",
|
|
37734
|
-
p_subtotal: subtotal,
|
|
37735
|
-
p_vat_total: vatTotal,
|
|
37736
|
-
p_total: total2,
|
|
37737
|
-
p_notes: params.notes || "",
|
|
37738
|
-
p_lines: calculatedLines
|
|
37739
|
-
}
|
|
37740
|
-
);
|
|
37741
|
-
if (invError || !invoiceId) {
|
|
37742
|
-
return toErrorContent(
|
|
37743
|
-
invError ? sanitizeDbError("Failed to create invoice", invError) : "Failed to create invoice."
|
|
37744
|
-
);
|
|
37745
|
-
}
|
|
37746
|
-
const { data: invoice, error: fetchError } = await supabase2.from("invoices").select(
|
|
37747
|
-
"id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at"
|
|
37748
|
-
).eq("id", invoiceId).eq("company_id", companyId2).single();
|
|
37749
|
-
if (fetchError || !invoice) {
|
|
37750
|
-
return toErrorContent(
|
|
37751
|
-
fetchError ? sanitizeDbError("Invoice created but fetch failed", fetchError) : "Invoice created but could not be retrieved."
|
|
37752
|
-
);
|
|
37753
|
-
}
|
|
37754
|
-
return toResult({
|
|
37755
|
-
message: `Invoice ${invoiceNumber} created for ${customer.name}`,
|
|
37756
|
-
invoice: {
|
|
37757
|
-
...invoice,
|
|
37758
|
-
customer_name: customer.name,
|
|
37759
|
-
lines: calculatedLines
|
|
37760
|
-
}
|
|
37761
|
-
});
|
|
37762
|
-
}
|
|
37840
|
+
() => createInvoiceDraftCommand(ctx, params)
|
|
37763
37841
|
);
|
|
37764
37842
|
}
|
|
37765
37843
|
);
|
|
@@ -37772,7 +37850,8 @@ function registerInvoiceTools(server2) {
|
|
|
37772
37850
|
channel: external_exports.enum(["email", "einvoice"]).describe("Delivery channel: email or einvoice"),
|
|
37773
37851
|
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
|
|
37774
37852
|
},
|
|
37775
|
-
|
|
37853
|
+
// Delivers through email or Maventa: an external effect (openWorldHint).
|
|
37854
|
+
annotations: WRITE_EXTERNAL_ANNOTATIONS
|
|
37776
37855
|
},
|
|
37777
37856
|
async (params) => {
|
|
37778
37857
|
const supabase2 = getSupabase();
|
|
@@ -38132,6 +38211,43 @@ function registerInvoiceTools(server2) {
|
|
|
38132
38211
|
);
|
|
38133
38212
|
}
|
|
38134
38213
|
|
|
38214
|
+
// src/finnish-business-id.ts
|
|
38215
|
+
function validateFinnishBusinessIdCore(input2) {
|
|
38216
|
+
const cleaned = input2.replace(/\s/g, "").toUpperCase();
|
|
38217
|
+
const match = cleaned.match(/^(\d{7})-(\d)$/);
|
|
38218
|
+
if (!match) return { valid: false, reason: "format" };
|
|
38219
|
+
const digits = match[1];
|
|
38220
|
+
const actualCheckDigit = Number(match[2]);
|
|
38221
|
+
if (!digits || !Number.isInteger(actualCheckDigit)) {
|
|
38222
|
+
return { valid: false, reason: "format" };
|
|
38223
|
+
}
|
|
38224
|
+
const weights = [7, 9, 10, 5, 8, 4, 2];
|
|
38225
|
+
let sum = 0;
|
|
38226
|
+
for (let index = 0; index < weights.length; index += 1) {
|
|
38227
|
+
const digit = digits[index];
|
|
38228
|
+
const weight = weights[index];
|
|
38229
|
+
if (digit === void 0 || weight === void 0) {
|
|
38230
|
+
return { valid: false, reason: "format" };
|
|
38231
|
+
}
|
|
38232
|
+
sum += Number(digit) * weight;
|
|
38233
|
+
}
|
|
38234
|
+
const remainder = sum % 11;
|
|
38235
|
+
if (remainder === 1) return { valid: false, reason: "remainder_one" };
|
|
38236
|
+
const expectedCheckDigit = remainder === 0 ? 0 : 11 - remainder;
|
|
38237
|
+
if (actualCheckDigit !== expectedCheckDigit) {
|
|
38238
|
+
return {
|
|
38239
|
+
valid: false,
|
|
38240
|
+
reason: "checksum",
|
|
38241
|
+
expectedCheckDigit,
|
|
38242
|
+
actualCheckDigit
|
|
38243
|
+
};
|
|
38244
|
+
}
|
|
38245
|
+
return { valid: true };
|
|
38246
|
+
}
|
|
38247
|
+
function isValidFinnishBusinessId(input2) {
|
|
38248
|
+
return validateFinnishBusinessIdCore(input2).valid;
|
|
38249
|
+
}
|
|
38250
|
+
|
|
38135
38251
|
// src/tools/customers.ts
|
|
38136
38252
|
var CUSTOMER_STATS_RPC_SCHEMA = external_exports.object({
|
|
38137
38253
|
invoice_count: external_exports.number().int().nonnegative(),
|
|
@@ -39695,7 +39811,7 @@ function validateFinnishIBAN(input2) {
|
|
|
39695
39811
|
}
|
|
39696
39812
|
return { valid: true };
|
|
39697
39813
|
}
|
|
39698
|
-
function
|
|
39814
|
+
function generateReferenceNumber(baseNumber) {
|
|
39699
39815
|
const digits = baseNumber.replace(/\D/g, "").replace(/^0+/, "");
|
|
39700
39816
|
if (digits.length < 3 || digits.length > 19) {
|
|
39701
39817
|
return { reference: "", error: "Base number must be 3-19 digits without leading zeros." };
|
|
@@ -39756,7 +39872,7 @@ function registerValidationTools(server2) {
|
|
|
39756
39872
|
annotations: READ_ONLY_ANNOTATIONS
|
|
39757
39873
|
},
|
|
39758
39874
|
async (params) => {
|
|
39759
|
-
const result =
|
|
39875
|
+
const result = generateReferenceNumber(params.baseNumber);
|
|
39760
39876
|
return toResult({
|
|
39761
39877
|
input: params.baseNumber,
|
|
39762
39878
|
...result
|
|
@@ -41501,6 +41617,81 @@ function registerBookingTools(server2) {
|
|
|
41501
41617
|
);
|
|
41502
41618
|
}
|
|
41503
41619
|
|
|
41620
|
+
// src/vat.ts
|
|
41621
|
+
var EU_COUNTRY_CODES2 = /* @__PURE__ */ new Set([
|
|
41622
|
+
"AT",
|
|
41623
|
+
"BE",
|
|
41624
|
+
"BG",
|
|
41625
|
+
"HR",
|
|
41626
|
+
"CY",
|
|
41627
|
+
"CZ",
|
|
41628
|
+
"DE",
|
|
41629
|
+
"DK",
|
|
41630
|
+
"EE",
|
|
41631
|
+
"ES",
|
|
41632
|
+
"FI",
|
|
41633
|
+
"FR",
|
|
41634
|
+
"GR",
|
|
41635
|
+
"HU",
|
|
41636
|
+
"IE",
|
|
41637
|
+
"IT",
|
|
41638
|
+
"LT",
|
|
41639
|
+
"LU",
|
|
41640
|
+
"LV",
|
|
41641
|
+
"MT",
|
|
41642
|
+
"NL",
|
|
41643
|
+
"PL",
|
|
41644
|
+
"PT",
|
|
41645
|
+
"RO",
|
|
41646
|
+
"SE",
|
|
41647
|
+
"SI",
|
|
41648
|
+
"SK"
|
|
41649
|
+
]);
|
|
41650
|
+
function normalizedVatNumber(value) {
|
|
41651
|
+
return (value ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
41652
|
+
}
|
|
41653
|
+
function validateVatTreatmentEligibility({
|
|
41654
|
+
code,
|
|
41655
|
+
sellerAlvRegistered,
|
|
41656
|
+
customer,
|
|
41657
|
+
documentDate
|
|
41658
|
+
}) {
|
|
41659
|
+
if (!sellerAlvRegistered && code !== "not_vat_registered") {
|
|
41660
|
+
return "A seller outside the VAT register can only use not_vat_registered.";
|
|
41661
|
+
}
|
|
41662
|
+
if (sellerAlvRegistered && code === "not_vat_registered") {
|
|
41663
|
+
return "A VAT-registered seller cannot use not_vat_registered.";
|
|
41664
|
+
}
|
|
41665
|
+
if (code === "standard_25_5" && documentDate < "2024-09-01") {
|
|
41666
|
+
return "standard_25_5 is not valid before 2024-09-01.";
|
|
41667
|
+
}
|
|
41668
|
+
if (code === "standard_13_5" && documentDate < "2026-01-01") {
|
|
41669
|
+
return "standard_13_5 is not valid before 2026-01-01.";
|
|
41670
|
+
}
|
|
41671
|
+
const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu";
|
|
41672
|
+
if (requiresBusiness && customer?.customer_kind !== "business") {
|
|
41673
|
+
return "The selected VAT treatment requires a business customer.";
|
|
41674
|
+
}
|
|
41675
|
+
if ((code === "reverse_construction" || code === "reverse_scrap") && !isValidFinnishBusinessId(customer?.business_id ?? "")) {
|
|
41676
|
+
return "Domestic reverse charge requires a Finnish buyer business ID in the form 1234567-8.";
|
|
41677
|
+
}
|
|
41678
|
+
if (code === "reverse_eu_service" || code === "intra_eu_goods") {
|
|
41679
|
+
const country = (customer?.country ?? "").trim().toUpperCase();
|
|
41680
|
+
const vatNumber = normalizedVatNumber(customer?.vat_number);
|
|
41681
|
+
const expectedPrefix = country === "GR" ? "EL" : country;
|
|
41682
|
+
if (!EU_COUNTRY_CODES2.has(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix)) {
|
|
41683
|
+
return "EU sale requires a non-Finnish EU country and a matching buyer VAT number.";
|
|
41684
|
+
}
|
|
41685
|
+
}
|
|
41686
|
+
if (code === "export_outside_eu" || code === "service_outside_eu") {
|
|
41687
|
+
const country = (customer?.country ?? "").trim().toUpperCase();
|
|
41688
|
+
if (!country || EU_COUNTRY_CODES2.has(country)) {
|
|
41689
|
+
return "Export outside the EU requires a buyer country outside the EU.";
|
|
41690
|
+
}
|
|
41691
|
+
}
|
|
41692
|
+
return null;
|
|
41693
|
+
}
|
|
41694
|
+
|
|
41504
41695
|
// src/tools/offers.ts
|
|
41505
41696
|
var OFFER_DETAIL_OUTPUT_SHAPE = {
|
|
41506
41697
|
id: external_exports.string(),
|
|
@@ -42962,7 +43153,7 @@ var VAT_COMPLIANCE = {
|
|
|
42962
43153
|
],
|
|
42963
43154
|
booking: "D expense/asset for net amount plus non-deductible self-assessed VAT; D ALV-saamiset only for deductible VAT; C bank/payable for supplier amount; C ALV-velka for full self-assessed VAT. Use the canonical VAT purchase command, not unlinked VAT account rows.",
|
|
42964
43155
|
note: "Net VAT effect is zero only with a full deduction. Report the full assessed VAT and only the deductible share. The seller does not charge VAT on a reverse-charge invoice; this does not remove the buyer\u2019s VAT liability.",
|
|
42965
|
-
implementation_limits: "
|
|
43156
|
+
implementation_limits: "kirjapro_book_purchase (full surface) takes deductionPercent 0\u2013100 with a documented deductionReason, including for reverse charge: the full assessed VAT is payable (ALV-velka) and only the deductible share reaches ALV-saamiset and field 307. The rest is expense. Entertainment and non-registered buyers use 0.",
|
|
42966
43157
|
example: { net_purchase: 100, rate: FINNISH_RATES_2026.vat.standard, deductible_share: "60%", output_vat: exampleOutputVat, deductible_vat: exampleDeductibleVat, non_deductible_vat: exampleNonDeductibleVat, expense: roundCents(100 + exampleNonDeductibleVat), payable_vat: exampleNonDeductibleVat },
|
|
42967
43158
|
omavero_fields: "EU goods: 305 (VAT) / 313 (base); EU services: 306 / 314; services from outside the EU: VAT in 301-303 by rate (no base field); imports: 304 / 310; domestic construction/scrap purchases: 318 / 320, sales 319. The deductible part of each is in 307."
|
|
42968
43159
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirjapro/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.
|
|
5
|
+
"version": "0.13.0",
|
|
6
6
|
"homepage": "https://kirjapro.fi",
|
|
7
7
|
"repository": "https://github.com/Jaollo/kirja-pro",
|
|
8
8
|
"license": "MIT",
|