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