@kirjapro/mcp-server 0.5.5 → 0.5.6
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/README.md +6 -5
- package/dist/index.js +672 -182
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -20980,6 +20980,27 @@ async function initLegacyAuth() {
|
|
|
20980
20980
|
console.error(
|
|
20981
20981
|
`[kirjapro-mcp] Authenticated as ${userData.user.email} | Company: ${company.name} | Tier: ${tier}`
|
|
20982
20982
|
);
|
|
20983
|
+
try {
|
|
20984
|
+
const payload = JSON.parse(
|
|
20985
|
+
Buffer.from(supabaseAccessToken.split(".")[1], "base64url").toString()
|
|
20986
|
+
);
|
|
20987
|
+
if (typeof payload.exp === "number") {
|
|
20988
|
+
const expiresInMs = payload.exp * 1e3 - Date.now();
|
|
20989
|
+
if (expiresInMs > 0 && expiresInMs < 24 * 60 * 60 * 1e3) {
|
|
20990
|
+
const warnMs = Math.max(0, expiresInMs - 5 * 60 * 1e3);
|
|
20991
|
+
const timer = setTimeout(() => {
|
|
20992
|
+
console.error(
|
|
20993
|
+
"[kirjapro-mcp] WARNING: Legacy auth token expires in ~5 minutes. Restart with a fresh token or switch to KIRJAPRO_API_KEY for automatic refresh."
|
|
20994
|
+
);
|
|
20995
|
+
}, warnMs);
|
|
20996
|
+
timer.unref?.();
|
|
20997
|
+
console.error(
|
|
20998
|
+
`[kirjapro-mcp] Token expires in ${Math.round(expiresInMs / 6e4)} minutes. Consider switching to KIRJAPRO_API_KEY for automatic refresh.`
|
|
20999
|
+
);
|
|
21000
|
+
}
|
|
21001
|
+
}
|
|
21002
|
+
} catch {
|
|
21003
|
+
}
|
|
20983
21004
|
}
|
|
20984
21005
|
async function initAuth() {
|
|
20985
21006
|
const apiKey = process.env.KIRJAPRO_API_KEY;
|
|
@@ -21020,6 +21041,18 @@ function formatCurrency(amount) {
|
|
|
21020
21041
|
currency: "EUR"
|
|
21021
21042
|
}).format(amount);
|
|
21022
21043
|
}
|
|
21044
|
+
function helsinkiDateString(now = /* @__PURE__ */ new Date()) {
|
|
21045
|
+
const parts = new Intl.DateTimeFormat("fi-FI", {
|
|
21046
|
+
timeZone: "Europe/Helsinki",
|
|
21047
|
+
year: "numeric",
|
|
21048
|
+
month: "2-digit",
|
|
21049
|
+
day: "2-digit"
|
|
21050
|
+
}).formatToParts(now);
|
|
21051
|
+
const year = parts.find((p) => p.type === "year")?.value ?? "1970";
|
|
21052
|
+
const month = parts.find((p) => p.type === "month")?.value ?? "01";
|
|
21053
|
+
const day = parts.find((p) => p.type === "day")?.value ?? "01";
|
|
21054
|
+
return `${year}-${month}-${day}`;
|
|
21055
|
+
}
|
|
21023
21056
|
function clampPagination(limit, offset) {
|
|
21024
21057
|
const safeLimit = Math.min(Math.max(limit ?? 50, 1), 200);
|
|
21025
21058
|
const safeOffset = Math.max(offset ?? 0, 0);
|
|
@@ -21145,6 +21178,18 @@ function tagUntrusted(value, maxLen = 500) {
|
|
|
21145
21178
|
const cleaned = value.replace(ZERO_WIDTH, "").replace(CONTROL, " ").replace(INJECTION_MARKERS, "\u27E6redacted\u27E7").replace(/<\/?untrusted-external>/gi, "").slice(0, maxLen);
|
|
21146
21179
|
return `<untrusted-external>${cleaned}</untrusted-external>`;
|
|
21147
21180
|
}
|
|
21181
|
+
function tagCustomer(row) {
|
|
21182
|
+
if (row == null) return row;
|
|
21183
|
+
const wrapped = { ...row };
|
|
21184
|
+
if ("name" in row) wrapped.name = tagUntrusted(row.name);
|
|
21185
|
+
if ("contact_person" in row)
|
|
21186
|
+
wrapped.contact_person = tagUntrusted(row.contact_person);
|
|
21187
|
+
if ("address" in row) wrapped.address = tagUntrusted(row.address);
|
|
21188
|
+
if ("city" in row) wrapped.city = tagUntrusted(row.city);
|
|
21189
|
+
if ("email" in row) wrapped.email = tagUntrusted(row.email);
|
|
21190
|
+
if ("notes" in row) wrapped.notes = tagUntrusted(row.notes);
|
|
21191
|
+
return wrapped;
|
|
21192
|
+
}
|
|
21148
21193
|
function addDaysToIsoDate(date3, days) {
|
|
21149
21194
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date3);
|
|
21150
21195
|
if (!match) {
|
|
@@ -21181,37 +21226,76 @@ function toDryRunResponse(tool, preview) {
|
|
|
21181
21226
|
}
|
|
21182
21227
|
async function withIdempotency(supabase2, companyId2, toolName, key, fn) {
|
|
21183
21228
|
if (!key) return fn();
|
|
21229
|
+
const CLAIM_STALE_MS = 6e4;
|
|
21230
|
+
const CLAIM_WAIT_MS = 1500;
|
|
21231
|
+
const filter = () => supabase2.from("mcp_idempotency_keys").select("response_payload, created_at").eq("company_id", companyId2).eq("tool_name", toolName).eq("key", key).maybeSingle();
|
|
21184
21232
|
try {
|
|
21185
|
-
const { data: existing } = await
|
|
21233
|
+
const { data: existing } = await filter();
|
|
21186
21234
|
if (existing?.response_payload) {
|
|
21187
21235
|
return existing.response_payload;
|
|
21188
21236
|
}
|
|
21237
|
+
if (existing && !existing.response_payload) {
|
|
21238
|
+
const age = Date.now() - new Date(existing.created_at).getTime();
|
|
21239
|
+
if (age > CLAIM_STALE_MS) {
|
|
21240
|
+
await supabase2.from("mcp_idempotency_keys").delete().eq("company_id", companyId2).eq("tool_name", toolName).eq("key", key);
|
|
21241
|
+
} else {
|
|
21242
|
+
await new Promise((r) => setTimeout(r, CLAIM_WAIT_MS));
|
|
21243
|
+
const { data: retried } = await filter();
|
|
21244
|
+
if (retried?.response_payload) {
|
|
21245
|
+
return retried.response_payload;
|
|
21246
|
+
}
|
|
21247
|
+
}
|
|
21248
|
+
}
|
|
21189
21249
|
} catch (err) {
|
|
21190
21250
|
console.error("[kirjapro-mcp] idempotency lookup failed:", {
|
|
21191
21251
|
tool: toolName,
|
|
21192
21252
|
err: err.message
|
|
21193
21253
|
});
|
|
21194
21254
|
}
|
|
21255
|
+
let claimed = false;
|
|
21256
|
+
try {
|
|
21257
|
+
const { error: claimErr } = await supabase2.from("mcp_idempotency_keys").insert({
|
|
21258
|
+
company_id: companyId2,
|
|
21259
|
+
key,
|
|
21260
|
+
tool_name: toolName,
|
|
21261
|
+
response_payload: null
|
|
21262
|
+
});
|
|
21263
|
+
claimed = !claimErr;
|
|
21264
|
+
if (claimErr) {
|
|
21265
|
+
await new Promise((r) => setTimeout(r, CLAIM_WAIT_MS));
|
|
21266
|
+
const { data: retried } = await filter();
|
|
21267
|
+
if (retried?.response_payload) {
|
|
21268
|
+
return retried.response_payload;
|
|
21269
|
+
}
|
|
21270
|
+
}
|
|
21271
|
+
} catch (err) {
|
|
21272
|
+
console.error("[kirjapro-mcp] idempotency claim failed:", {
|
|
21273
|
+
tool: toolName,
|
|
21274
|
+
err: err.message
|
|
21275
|
+
});
|
|
21276
|
+
}
|
|
21195
21277
|
const result = await fn();
|
|
21196
|
-
if (!result.isError) {
|
|
21278
|
+
if (!result.isError && claimed) {
|
|
21197
21279
|
try {
|
|
21198
|
-
await supabase2.from("mcp_idempotency_keys").
|
|
21199
|
-
company_id: companyId2,
|
|
21200
|
-
key,
|
|
21201
|
-
tool_name: toolName,
|
|
21280
|
+
await supabase2.from("mcp_idempotency_keys").update({
|
|
21202
21281
|
response_payload: result
|
|
21203
|
-
});
|
|
21282
|
+
}).eq("company_id", companyId2).eq("tool_name", toolName).eq("key", key);
|
|
21204
21283
|
} catch (err) {
|
|
21205
|
-
console.error("[kirjapro-mcp] idempotency cache
|
|
21284
|
+
console.error("[kirjapro-mcp] idempotency cache update failed:", {
|
|
21206
21285
|
tool: toolName,
|
|
21207
21286
|
err: err.message
|
|
21208
21287
|
});
|
|
21209
21288
|
}
|
|
21289
|
+
} else if (result.isError && claimed) {
|
|
21290
|
+
try {
|
|
21291
|
+
await supabase2.from("mcp_idempotency_keys").delete().eq("company_id", companyId2).eq("tool_name", toolName).eq("key", key);
|
|
21292
|
+
} catch {
|
|
21293
|
+
}
|
|
21210
21294
|
}
|
|
21211
21295
|
return result;
|
|
21212
21296
|
}
|
|
21213
21297
|
async function resolveOpenFiscalYear(supabase2, companyId2) {
|
|
21214
|
-
const { data, error: error2 } = await supabase2.from("fiscal_years").select("id").eq("company_id", companyId2).eq("is_closed", false);
|
|
21298
|
+
const { data, error: error2 } = await supabase2.from("fiscal_years").select("id").eq("company_id", companyId2).eq("is_closed", false).order("end_date", { ascending: false }).limit(2);
|
|
21215
21299
|
if (error2) {
|
|
21216
21300
|
return toErrorContent(sanitizeDbError("Failed to query fiscal years", error2));
|
|
21217
21301
|
}
|
|
@@ -21228,6 +21312,19 @@ async function resolveOpenFiscalYear(supabase2, companyId2) {
|
|
|
21228
21312
|
return { id: data[0].id };
|
|
21229
21313
|
}
|
|
21230
21314
|
|
|
21315
|
+
// src/schemas.ts
|
|
21316
|
+
var PAGINATED_OUTPUT_SHAPE = {
|
|
21317
|
+
results: external_exports.array(external_exports.record(external_exports.unknown())),
|
|
21318
|
+
total_count: external_exports.number(),
|
|
21319
|
+
has_more: external_exports.boolean(),
|
|
21320
|
+
next_offset: external_exports.number()
|
|
21321
|
+
};
|
|
21322
|
+
var VALIDATION_OUTPUT_SHAPE = {
|
|
21323
|
+
input: external_exports.string(),
|
|
21324
|
+
valid: external_exports.boolean(),
|
|
21325
|
+
error: external_exports.string().optional()
|
|
21326
|
+
};
|
|
21327
|
+
|
|
21231
21328
|
// src/tools/accounts.ts
|
|
21232
21329
|
function registerAccountTools(server2) {
|
|
21233
21330
|
server2.registerTool(
|
|
@@ -21242,6 +21339,7 @@ function registerAccountTools(server2) {
|
|
|
21242
21339
|
activeOnly: external_exports.boolean().default(true).describe("Only show active accounts"),
|
|
21243
21340
|
includeIds: external_exports.boolean().default(false).describe("Include UUID id and parent_id fields (rarely needed)")
|
|
21244
21341
|
},
|
|
21342
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
21245
21343
|
annotations: READ_ONLY_ANNOTATIONS
|
|
21246
21344
|
},
|
|
21247
21345
|
async (params) => {
|
|
@@ -21265,12 +21363,22 @@ function registerAccountTools(server2) {
|
|
|
21265
21363
|
if (error2) {
|
|
21266
21364
|
return toErrorContent(sanitizeDbError("Failed to list accounts", error2));
|
|
21267
21365
|
}
|
|
21268
|
-
return
|
|
21366
|
+
return toResult(paginate(data || [], count || 0, offset, limit));
|
|
21269
21367
|
}
|
|
21270
21368
|
);
|
|
21271
21369
|
}
|
|
21272
21370
|
|
|
21273
21371
|
// src/tools/transactions.ts
|
|
21372
|
+
var TRANSACTION_DETAIL_OUTPUT_SHAPE = {
|
|
21373
|
+
id: external_exports.string(),
|
|
21374
|
+
transaction_date: external_exports.string(),
|
|
21375
|
+
description: external_exports.string().nullable(),
|
|
21376
|
+
document_number: external_exports.string().nullable(),
|
|
21377
|
+
journal_entries: external_exports.array(external_exports.record(external_exports.unknown())),
|
|
21378
|
+
total_debit: external_exports.number(),
|
|
21379
|
+
total_credit: external_exports.number(),
|
|
21380
|
+
balanced: external_exports.boolean()
|
|
21381
|
+
};
|
|
21274
21382
|
var LIST_TRANSACTIONS_ROW_SHAPE = {
|
|
21275
21383
|
id: external_exports.string().uuid(),
|
|
21276
21384
|
transaction_date: external_exports.string(),
|
|
@@ -21366,6 +21474,7 @@ function registerTransactionTools(server2) {
|
|
|
21366
21474
|
limit: external_exports.number().min(1).max(200).default(50).describe("Max results (default 50)"),
|
|
21367
21475
|
offset: external_exports.number().min(0).default(0).describe("Skip first N results")
|
|
21368
21476
|
},
|
|
21477
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
21369
21478
|
annotations: READ_ONLY_ANNOTATIONS
|
|
21370
21479
|
},
|
|
21371
21480
|
async (params) => {
|
|
@@ -21443,12 +21552,12 @@ function registerTransactionTools(server2) {
|
|
|
21443
21552
|
const filteredTotal = results.length;
|
|
21444
21553
|
const paginatedResults = results.slice(offset, offset + limit);
|
|
21445
21554
|
const cappedWarning = (count || 0) >= AMOUNT_FILTER_CAP ? " (results capped at 1000 rows \u2014 narrow your date range for complete results)" : "";
|
|
21446
|
-
return
|
|
21555
|
+
return toResult({
|
|
21447
21556
|
...paginate(paginatedResults, filteredTotal, offset, limit),
|
|
21448
21557
|
amount_filter_note: `Filtered client-side from ${count || 0} transactions${cappedWarning}`
|
|
21449
21558
|
});
|
|
21450
21559
|
}
|
|
21451
|
-
return
|
|
21560
|
+
return toResult(paginate(results, count || 0, offset, limit));
|
|
21452
21561
|
}
|
|
21453
21562
|
);
|
|
21454
21563
|
server2.registerTool(
|
|
@@ -21463,8 +21572,8 @@ function registerTransactionTools(server2) {
|
|
|
21463
21572
|
entries: external_exports.array(
|
|
21464
21573
|
external_exports.object({
|
|
21465
21574
|
accountNumber: external_exports.string().describe('Account number (e.g. "4000") or UUID'),
|
|
21466
|
-
debitAmount: external_exports.number().min(0).default(0).describe("Debit amount in EUR"),
|
|
21467
|
-
creditAmount: external_exports.number().min(0).default(0).describe("Credit amount in EUR")
|
|
21575
|
+
debitAmount: external_exports.number().finite().min(0).max(1e9).default(0).describe("Debit amount in EUR"),
|
|
21576
|
+
creditAmount: external_exports.number().finite().min(0).max(1e9).default(0).describe("Credit amount in EUR")
|
|
21468
21577
|
})
|
|
21469
21578
|
).min(2).describe("Journal entries (min 2, must balance)"),
|
|
21470
21579
|
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
|
|
@@ -21592,9 +21701,12 @@ function registerTransactionTools(server2) {
|
|
|
21592
21701
|
server2.registerTool(
|
|
21593
21702
|
"kirjapro_create_transactions_batch",
|
|
21594
21703
|
{
|
|
21595
|
-
description:
|
|
21704
|
+
description: 'Create multiple transactions in one call (up to 50). Default mode "atomic" rolls back ALL on any failure; "best_effort" collects per-row errors. Prefer kirjapro_create_from_template_batch for standard patterns.',
|
|
21596
21705
|
inputSchema: {
|
|
21597
21706
|
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
|
|
21707
|
+
mode: external_exports.enum(["atomic", "best_effort"]).default("atomic").describe(
|
|
21708
|
+
'Batch mode. "atomic" (default): all-or-nothing, rolls back on any row failure. "best_effort": commits what it can, returns per-row errors.'
|
|
21709
|
+
),
|
|
21598
21710
|
transactions: external_exports.array(
|
|
21599
21711
|
external_exports.object({
|
|
21600
21712
|
transactionDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Date (YYYY-MM-DD)"),
|
|
@@ -21603,8 +21715,8 @@ function registerTransactionTools(server2) {
|
|
|
21603
21715
|
entries: external_exports.array(
|
|
21604
21716
|
external_exports.object({
|
|
21605
21717
|
accountNumber: external_exports.string().describe('Account number (e.g. "4000")'),
|
|
21606
|
-
debitAmount: external_exports.number().min(0).default(0),
|
|
21607
|
-
creditAmount: external_exports.number().min(0).default(0)
|
|
21718
|
+
debitAmount: external_exports.number().finite().min(0).max(1e9).default(0),
|
|
21719
|
+
creditAmount: external_exports.number().finite().min(0).max(1e9).default(0)
|
|
21608
21720
|
})
|
|
21609
21721
|
).min(2)
|
|
21610
21722
|
})
|
|
@@ -21644,11 +21756,9 @@ function registerTransactionTools(server2) {
|
|
|
21644
21756
|
)
|
|
21645
21757
|
)
|
|
21646
21758
|
];
|
|
21647
|
-
const uuidIdentifiers = allAccountNumbers.filter(
|
|
21648
|
-
(identifier) => isUuid(identifier)
|
|
21649
|
-
);
|
|
21759
|
+
const uuidIdentifiers = allAccountNumbers.filter((id) => isUuid(id));
|
|
21650
21760
|
const numberIdentifiers = allAccountNumbers.filter(
|
|
21651
|
-
(
|
|
21761
|
+
(id) => !isUuid(id)
|
|
21652
21762
|
);
|
|
21653
21763
|
const [accountsByNumber, accountsById] = await Promise.all([
|
|
21654
21764
|
numberIdentifiers.length > 0 ? supabase2.from("accounts").select("id, account_number").eq("company_id", companyId2).in("account_number", numberIdentifiers) : Promise.resolve({ data: [], error: null }),
|
|
@@ -21669,12 +21779,12 @@ function registerTransactionTools(server2) {
|
|
|
21669
21779
|
accountMap.set(account.account_number, account.id);
|
|
21670
21780
|
accountMap.set(account.id, account.id);
|
|
21671
21781
|
}
|
|
21672
|
-
const
|
|
21673
|
-
const
|
|
21782
|
+
const preValidated = [];
|
|
21783
|
+
const preFailed = [];
|
|
21674
21784
|
for (let i = 0; i < params.transactions.length; i++) {
|
|
21675
21785
|
const tx = params.transactions[i];
|
|
21676
21786
|
if (tx.transactionDate < fy.start_date || tx.transactionDate > fy.end_date) {
|
|
21677
|
-
|
|
21787
|
+
preFailed.push({
|
|
21678
21788
|
index: i,
|
|
21679
21789
|
error: `Date ${tx.transactionDate} outside fiscal year range.`
|
|
21680
21790
|
});
|
|
@@ -21684,7 +21794,7 @@ function registerTransactionTools(server2) {
|
|
|
21684
21794
|
(e) => !accountMap.has(e.accountNumber)
|
|
21685
21795
|
);
|
|
21686
21796
|
if (missing.length > 0) {
|
|
21687
|
-
|
|
21797
|
+
preFailed.push({
|
|
21688
21798
|
index: i,
|
|
21689
21799
|
error: `Unknown accounts: ${missing.map((m) => m.accountNumber).join(", ")}`
|
|
21690
21800
|
});
|
|
@@ -21697,46 +21807,78 @@ function registerTransactionTools(server2) {
|
|
|
21697
21807
|
tx.entries.reduce((s, e) => s + e.creditAmount, 0)
|
|
21698
21808
|
);
|
|
21699
21809
|
if (totalDebit !== totalCredit) {
|
|
21700
|
-
|
|
21810
|
+
preFailed.push({
|
|
21701
21811
|
index: i,
|
|
21702
21812
|
error: `Unbalanced: debits ${totalDebit} != credits ${totalCredit}`
|
|
21703
21813
|
});
|
|
21704
21814
|
continue;
|
|
21705
21815
|
}
|
|
21706
|
-
|
|
21707
|
-
|
|
21708
|
-
|
|
21709
|
-
credit_amount: e.creditAmount
|
|
21710
|
-
}));
|
|
21711
|
-
const { data: newTxId, error: txErr } = await supabase2.rpc(
|
|
21712
|
-
"create_transaction_with_entries",
|
|
21713
|
-
{
|
|
21714
|
-
p_company_id: companyId2,
|
|
21715
|
-
p_fiscal_year_id: fiscalYearId,
|
|
21716
|
-
p_transaction_date: tx.transactionDate,
|
|
21717
|
-
p_description: tx.description,
|
|
21718
|
-
p_document_number: tx.documentNumber || "",
|
|
21719
|
-
p_entries: entries
|
|
21720
|
-
}
|
|
21721
|
-
);
|
|
21722
|
-
if (txErr || !newTxId) {
|
|
21723
|
-
failed.push({
|
|
21724
|
-
index: i,
|
|
21725
|
-
error: txErr?.message || "No transaction ID returned"
|
|
21726
|
-
});
|
|
21727
|
-
continue;
|
|
21728
|
-
}
|
|
21729
|
-
created.push({
|
|
21730
|
-
id: newTxId,
|
|
21816
|
+
preValidated.push({
|
|
21817
|
+
index: i,
|
|
21818
|
+
transaction_date: tx.transactionDate,
|
|
21731
21819
|
description: tx.description,
|
|
21732
|
-
|
|
21820
|
+
document_number: tx.documentNumber || "",
|
|
21821
|
+
entries: tx.entries.map((e) => ({
|
|
21822
|
+
account_id: accountMap.get(e.accountNumber),
|
|
21823
|
+
debit_amount: e.debitAmount,
|
|
21824
|
+
credit_amount: e.creditAmount
|
|
21825
|
+
}))
|
|
21826
|
+
});
|
|
21827
|
+
}
|
|
21828
|
+
if (params.mode === "atomic" && preFailed.length > 0) {
|
|
21829
|
+
return toErrorContent(
|
|
21830
|
+
`Atomic batch aborted: ${preFailed.length} transaction(s) failed validation. Fix all errors or use mode "best_effort" for partial commits.
|
|
21831
|
+
` + preFailed.map((f) => ` [${f.index}] ${f.error}`).join("\n")
|
|
21832
|
+
);
|
|
21833
|
+
}
|
|
21834
|
+
if (preValidated.length === 0) {
|
|
21835
|
+
return toTextContent({
|
|
21836
|
+
mode: params.mode,
|
|
21837
|
+
created_count: 0,
|
|
21838
|
+
failed_count: preFailed.length,
|
|
21839
|
+
created: [],
|
|
21840
|
+
failed: preFailed
|
|
21733
21841
|
});
|
|
21734
21842
|
}
|
|
21843
|
+
const rpcTransactions = preValidated.map((v) => ({
|
|
21844
|
+
transaction_date: v.transaction_date,
|
|
21845
|
+
description: v.description,
|
|
21846
|
+
document_number: v.document_number,
|
|
21847
|
+
entries: v.entries
|
|
21848
|
+
}));
|
|
21849
|
+
const { data: rpcResult, error: rpcError } = await supabase2.rpc(
|
|
21850
|
+
"create_transactions_batch_atomic",
|
|
21851
|
+
{
|
|
21852
|
+
p_company_id: companyId2,
|
|
21853
|
+
p_fiscal_year_id: fiscalYearId,
|
|
21854
|
+
p_transactions: rpcTransactions,
|
|
21855
|
+
p_mode: params.mode
|
|
21856
|
+
}
|
|
21857
|
+
);
|
|
21858
|
+
if (rpcError) {
|
|
21859
|
+
return toErrorContent(
|
|
21860
|
+
sanitizeDbError("Batch create failed", rpcError)
|
|
21861
|
+
);
|
|
21862
|
+
}
|
|
21863
|
+
const result = rpcResult;
|
|
21864
|
+
const created = result.created.map((c) => ({
|
|
21865
|
+
id: c.id,
|
|
21866
|
+
description: c.description,
|
|
21867
|
+
date: c.date,
|
|
21868
|
+
index: preValidated[c.index].index
|
|
21869
|
+
}));
|
|
21870
|
+
const rpcFailed = result.failed.map((f) => ({
|
|
21871
|
+
index: preValidated[f.index].index,
|
|
21872
|
+
error: sanitizeDbError("Failed to create transaction", {
|
|
21873
|
+
message: f.error
|
|
21874
|
+
})
|
|
21875
|
+
}));
|
|
21735
21876
|
return toTextContent({
|
|
21877
|
+
mode: params.mode,
|
|
21736
21878
|
created_count: created.length,
|
|
21737
|
-
failed_count:
|
|
21879
|
+
failed_count: preFailed.length + rpcFailed.length,
|
|
21738
21880
|
created,
|
|
21739
|
-
failed
|
|
21881
|
+
failed: [...preFailed, ...rpcFailed]
|
|
21740
21882
|
});
|
|
21741
21883
|
}
|
|
21742
21884
|
);
|
|
@@ -21749,6 +21891,7 @@ function registerTransactionTools(server2) {
|
|
|
21749
21891
|
inputSchema: {
|
|
21750
21892
|
transactionId: external_exports.string().uuid().describe("Transaction UUID")
|
|
21751
21893
|
},
|
|
21894
|
+
outputSchema: TRANSACTION_DETAIL_OUTPUT_SHAPE,
|
|
21752
21895
|
annotations: READ_ONLY_ANNOTATIONS
|
|
21753
21896
|
},
|
|
21754
21897
|
async (params) => {
|
|
@@ -21775,7 +21918,7 @@ function registerTransactionTools(server2) {
|
|
|
21775
21918
|
const totalCredit = roundCents(
|
|
21776
21919
|
entries.reduce((s, e) => s + (e.credit_amount || 0), 0)
|
|
21777
21920
|
);
|
|
21778
|
-
return
|
|
21921
|
+
return toResult({
|
|
21779
21922
|
...transaction,
|
|
21780
21923
|
total_debit: totalDebit,
|
|
21781
21924
|
total_credit: totalCredit,
|
|
@@ -21875,11 +22018,7 @@ function registerInvoiceTools(server2) {
|
|
|
21875
22018
|
has_more: external_exports.boolean(),
|
|
21876
22019
|
next_offset: external_exports.number()
|
|
21877
22020
|
},
|
|
21878
|
-
annotations:
|
|
21879
|
-
readOnlyHint: true,
|
|
21880
|
-
idempotentHint: true,
|
|
21881
|
-
openWorldHint: false
|
|
21882
|
-
}
|
|
22021
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
21883
22022
|
},
|
|
21884
22023
|
async (params) => {
|
|
21885
22024
|
const supabase2 = getSupabase();
|
|
@@ -21901,7 +22040,13 @@ function registerInvoiceTools(server2) {
|
|
|
21901
22040
|
if (error2) {
|
|
21902
22041
|
return toErrorContent(sanitizeDbError("Failed to list invoices", error2));
|
|
21903
22042
|
}
|
|
21904
|
-
|
|
22043
|
+
const tagged = (data || []).map((row) => ({
|
|
22044
|
+
...row,
|
|
22045
|
+
customers: tagCustomer(
|
|
22046
|
+
row.customers
|
|
22047
|
+
)
|
|
22048
|
+
}));
|
|
22049
|
+
return toResult(paginate(tagged, count || 0, offset, limit));
|
|
21905
22050
|
}
|
|
21906
22051
|
);
|
|
21907
22052
|
server2.registerTool(
|
|
@@ -21918,7 +22063,7 @@ function registerInvoiceTools(server2) {
|
|
|
21918
22063
|
description: external_exports.string().min(1).describe("Line item description"),
|
|
21919
22064
|
quantity: external_exports.number().min(0).describe("Quantity"),
|
|
21920
22065
|
unit: external_exports.string().default("kpl").describe("Unit (kpl, h, kk, etc.)"),
|
|
21921
|
-
unitPrice: external_exports.number().describe("Unit price in EUR (excl. VAT)"),
|
|
22066
|
+
unitPrice: external_exports.number().finite().min(-1e9).max(1e9).describe("Unit price in EUR (excl. VAT). Can be negative for credit lines."),
|
|
21922
22067
|
vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).default("25.5").describe("VAT rate")
|
|
21923
22068
|
})
|
|
21924
22069
|
).min(1).describe("Invoice line items"),
|
|
@@ -22032,7 +22177,7 @@ function registerInvoiceTools(server2) {
|
|
|
22032
22177
|
inputSchema: {
|
|
22033
22178
|
invoiceId: external_exports.string().uuid().describe("Invoice UUID"),
|
|
22034
22179
|
paymentDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Payment date (YYYY-MM-DD)"),
|
|
22035
|
-
amount: external_exports.number().min(0.01).describe("Payment amount in EUR"),
|
|
22180
|
+
amount: external_exports.number().finite().min(0.01).max(1e9).describe("Payment amount in EUR"),
|
|
22036
22181
|
paymentMethod: external_exports.string().default("bank_transfer").describe("Payment method (bank_transfer, cash, card)"),
|
|
22037
22182
|
reference: external_exports.string().optional().describe("Payment reference"),
|
|
22038
22183
|
notes: external_exports.string().optional().describe("Payment notes"),
|
|
@@ -22118,11 +22263,7 @@ function registerInvoiceTools(server2) {
|
|
|
22118
22263
|
invoiceId: external_exports.string().uuid().describe("Invoice UUID")
|
|
22119
22264
|
},
|
|
22120
22265
|
outputSchema: GET_INVOICE_OUTPUT_SHAPE,
|
|
22121
|
-
annotations:
|
|
22122
|
-
readOnlyHint: true,
|
|
22123
|
-
idempotentHint: true,
|
|
22124
|
-
openWorldHint: false
|
|
22125
|
-
}
|
|
22266
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
22126
22267
|
},
|
|
22127
22268
|
async (params) => {
|
|
22128
22269
|
const supabase2 = getSupabase();
|
|
@@ -22167,6 +22308,9 @@ function registerInvoiceTools(server2) {
|
|
|
22167
22308
|
const remainingBalance = roundCents(invoice.total - paidTotal);
|
|
22168
22309
|
return toResult({
|
|
22169
22310
|
...invoice,
|
|
22311
|
+
customers: tagCustomer(
|
|
22312
|
+
invoice.customers
|
|
22313
|
+
),
|
|
22170
22314
|
vat_breakdown: vatBreakdown,
|
|
22171
22315
|
paid_total: paidTotal,
|
|
22172
22316
|
remaining_balance: remainingBalance
|
|
@@ -22223,14 +22367,19 @@ function registerInvoiceTools(server2) {
|
|
|
22223
22367
|
async () => {
|
|
22224
22368
|
const updateFields = { status: newStatus };
|
|
22225
22369
|
if (newStatus === "paid") {
|
|
22226
|
-
updateFields.paid_date = (
|
|
22370
|
+
updateFields.paid_date = helsinkiDateString();
|
|
22227
22371
|
}
|
|
22228
|
-
const { error: updateError } = await supabase2.from("invoices").update(updateFields).eq("id", params.invoiceId).eq("company_id", companyId2);
|
|
22372
|
+
const { data: updated, error: updateError } = await supabase2.from("invoices").update(updateFields).eq("id", params.invoiceId).eq("company_id", companyId2).eq("status", currentStatus).select("id").maybeSingle();
|
|
22229
22373
|
if (updateError) {
|
|
22230
22374
|
return toErrorContent(
|
|
22231
22375
|
sanitizeDbError("Failed to update invoice status", updateError)
|
|
22232
22376
|
);
|
|
22233
22377
|
}
|
|
22378
|
+
if (!updated) {
|
|
22379
|
+
return toErrorContent(
|
|
22380
|
+
`Invoice ${invoice.invoice_number} status changed concurrently (was "${currentStatus}" when read). Re-read with kirjapro_get_invoice and decide whether to retry.`
|
|
22381
|
+
);
|
|
22382
|
+
}
|
|
22234
22383
|
return toTextContent({
|
|
22235
22384
|
message: `Invoice ${invoice.invoice_number} status changed from "${currentStatus}" to "${newStatus}"`,
|
|
22236
22385
|
invoice_number: invoice.invoice_number,
|
|
@@ -22279,22 +22428,16 @@ function registerInvoiceTools(server2) {
|
|
|
22279
22428
|
"kirjapro_delete_invoice",
|
|
22280
22429
|
params.idempotencyKey,
|
|
22281
22430
|
async () => {
|
|
22282
|
-
const { error:
|
|
22283
|
-
|
|
22284
|
-
|
|
22285
|
-
|
|
22286
|
-
|
|
22287
|
-
|
|
22288
|
-
|
|
22289
|
-
if (
|
|
22290
|
-
return toErrorContent(
|
|
22291
|
-
sanitizeDbError("Failed to delete invoice lines", linesError)
|
|
22292
|
-
);
|
|
22293
|
-
}
|
|
22294
|
-
const { error: deleteError } = await supabase2.from("invoices").delete().eq("id", params.invoiceId);
|
|
22295
|
-
if (deleteError) {
|
|
22431
|
+
const { error: rpcError } = await supabase2.rpc(
|
|
22432
|
+
"delete_draft_invoice_atomic",
|
|
22433
|
+
{
|
|
22434
|
+
p_company_id: companyId2,
|
|
22435
|
+
p_invoice_id: params.invoiceId
|
|
22436
|
+
}
|
|
22437
|
+
);
|
|
22438
|
+
if (rpcError) {
|
|
22296
22439
|
return toErrorContent(
|
|
22297
|
-
sanitizeDbError("Failed to delete invoice",
|
|
22440
|
+
sanitizeDbError("Failed to delete invoice", rpcError)
|
|
22298
22441
|
);
|
|
22299
22442
|
}
|
|
22300
22443
|
return toTextContent({
|
|
@@ -22308,6 +22451,34 @@ function registerInvoiceTools(server2) {
|
|
|
22308
22451
|
}
|
|
22309
22452
|
|
|
22310
22453
|
// src/tools/customers.ts
|
|
22454
|
+
var CUSTOMER_DETAIL_OUTPUT_SHAPE = {
|
|
22455
|
+
id: external_exports.string(),
|
|
22456
|
+
customer_number: external_exports.number(),
|
|
22457
|
+
name: external_exports.string(),
|
|
22458
|
+
business_id: external_exports.string().nullable(),
|
|
22459
|
+
email: external_exports.string().nullable(),
|
|
22460
|
+
payment_term_days: external_exports.number(),
|
|
22461
|
+
is_active: external_exports.boolean(),
|
|
22462
|
+
stats: external_exports.object({
|
|
22463
|
+
invoice_count: external_exports.number(),
|
|
22464
|
+
total_revenue: external_exports.number(),
|
|
22465
|
+
total_revenue_formatted: external_exports.string(),
|
|
22466
|
+
outstanding_balance: external_exports.number(),
|
|
22467
|
+
outstanding_balance_formatted: external_exports.string()
|
|
22468
|
+
}).passthrough()
|
|
22469
|
+
};
|
|
22470
|
+
var PRH_LOOKUP_OUTPUT_SHAPE = {
|
|
22471
|
+
business_id: external_exports.string(),
|
|
22472
|
+
name: external_exports.string(),
|
|
22473
|
+
registration_date: external_exports.string().nullable(),
|
|
22474
|
+
company_form: external_exports.string().nullable(),
|
|
22475
|
+
address: external_exports.string().nullable(),
|
|
22476
|
+
postal_code: external_exports.string().nullable(),
|
|
22477
|
+
city: external_exports.string().nullable(),
|
|
22478
|
+
country: external_exports.string().nullable(),
|
|
22479
|
+
website: external_exports.string().nullable(),
|
|
22480
|
+
phone: external_exports.string().nullable()
|
|
22481
|
+
};
|
|
22311
22482
|
function validateFinnishBusinessId(input) {
|
|
22312
22483
|
const match = input.match(/^(\d{7})-(\d)$/);
|
|
22313
22484
|
if (!match) return false;
|
|
@@ -22336,6 +22507,7 @@ function registerCustomerTools(server2) {
|
|
|
22336
22507
|
offset: external_exports.number().min(0).default(0).describe("Skip first N results"),
|
|
22337
22508
|
search: external_exports.string().optional().describe("Search by name, email, or business ID")
|
|
22338
22509
|
},
|
|
22510
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
22339
22511
|
annotations: READ_ONLY_ANNOTATIONS
|
|
22340
22512
|
},
|
|
22341
22513
|
async (params) => {
|
|
@@ -22357,7 +22529,7 @@ function registerCustomerTools(server2) {
|
|
|
22357
22529
|
if (error2) {
|
|
22358
22530
|
return toErrorContent(sanitizeDbError("Failed to list customers", error2));
|
|
22359
22531
|
}
|
|
22360
|
-
return
|
|
22532
|
+
return toResult(paginate((data || []).map((c) => tagCustomer(c)), count || 0, offset, limit));
|
|
22361
22533
|
}
|
|
22362
22534
|
);
|
|
22363
22535
|
server2.registerTool(
|
|
@@ -22416,13 +22588,13 @@ function registerCustomerTools(server2) {
|
|
|
22416
22588
|
city: sanitizeText(params.city),
|
|
22417
22589
|
country: params.country,
|
|
22418
22590
|
payment_term_days: params.paymentTermDays
|
|
22419
|
-
}).select().single();
|
|
22591
|
+
}).select("id, customer_number, name, business_id, contact_person, email, phone, address, postal_code, city, country, payment_term_days, notes, created_at").single();
|
|
22420
22592
|
if (error2) {
|
|
22421
22593
|
return toErrorContent(sanitizeDbError("Failed to create customer", error2));
|
|
22422
22594
|
}
|
|
22423
22595
|
return toTextContent({
|
|
22424
22596
|
message: `Customer "${params.name}" created with number ${customerNumber}`,
|
|
22425
|
-
customer
|
|
22597
|
+
customer: tagCustomer(customer)
|
|
22426
22598
|
});
|
|
22427
22599
|
}
|
|
22428
22600
|
);
|
|
@@ -22435,6 +22607,7 @@ function registerCustomerTools(server2) {
|
|
|
22435
22607
|
inputSchema: {
|
|
22436
22608
|
customerId: external_exports.string().uuid().describe("Customer UUID")
|
|
22437
22609
|
},
|
|
22610
|
+
outputSchema: CUSTOMER_DETAIL_OUTPUT_SHAPE,
|
|
22438
22611
|
annotations: READ_ONLY_ANNOTATIONS
|
|
22439
22612
|
},
|
|
22440
22613
|
async (params) => {
|
|
@@ -22456,8 +22629,8 @@ function registerCustomerTools(server2) {
|
|
|
22456
22629
|
return s + (i.total || 0) - paid;
|
|
22457
22630
|
}, 0)
|
|
22458
22631
|
);
|
|
22459
|
-
return
|
|
22460
|
-
...customer,
|
|
22632
|
+
return toResult({
|
|
22633
|
+
...tagCustomer(customer),
|
|
22461
22634
|
stats: {
|
|
22462
22635
|
invoice_count: invoiceCount,
|
|
22463
22636
|
total_revenue: totalRevenue,
|
|
@@ -22522,13 +22695,13 @@ function registerCustomerTools(server2) {
|
|
|
22522
22695
|
"kirjapro_update_customer",
|
|
22523
22696
|
params.idempotencyKey,
|
|
22524
22697
|
async () => {
|
|
22525
|
-
const { data: updated, error: updateError } = await supabase2.from("customers").update(updates).eq("id", params.customerId).eq("company_id", companyId2).select().single();
|
|
22698
|
+
const { data: updated, error: updateError } = await supabase2.from("customers").update(updates).eq("id", params.customerId).eq("company_id", companyId2).select("id, customer_number, name, business_id, contact_person, email, phone, address, postal_code, city, country, payment_term_days, notes, created_at, updated_at").single();
|
|
22526
22699
|
if (updateError) {
|
|
22527
22700
|
return toErrorContent(sanitizeDbError("Failed to update customer", updateError));
|
|
22528
22701
|
}
|
|
22529
22702
|
return toTextContent({
|
|
22530
22703
|
message: `Customer "${updated.name}" updated`,
|
|
22531
|
-
customer: updated,
|
|
22704
|
+
customer: tagCustomer(updated),
|
|
22532
22705
|
fields_changed: Object.keys(updates)
|
|
22533
22706
|
});
|
|
22534
22707
|
}
|
|
@@ -22542,6 +22715,7 @@ function registerCustomerTools(server2) {
|
|
|
22542
22715
|
inputSchema: {
|
|
22543
22716
|
businessId: external_exports.string().regex(/^\d{7}-\d$/).describe('Finnish Y-tunnus (e.g. "1234567-8")')
|
|
22544
22717
|
},
|
|
22718
|
+
outputSchema: PRH_LOOKUP_OUTPUT_SHAPE,
|
|
22545
22719
|
annotations: READ_ONLY_EXTERNAL_ANNOTATIONS
|
|
22546
22720
|
},
|
|
22547
22721
|
async (params) => {
|
|
@@ -22560,7 +22734,7 @@ function registerCustomerTools(server2) {
|
|
|
22560
22734
|
return toErrorContent(result.error);
|
|
22561
22735
|
}
|
|
22562
22736
|
const company = result.data.company;
|
|
22563
|
-
return
|
|
22737
|
+
return toResult({
|
|
22564
22738
|
business_id: company.businessId,
|
|
22565
22739
|
name: tagUntrusted(company.name),
|
|
22566
22740
|
registration_date: company.registrationDate,
|
|
@@ -22595,8 +22769,11 @@ var DASHBOARD_OUTPUT_SHAPE = {
|
|
|
22595
22769
|
invoice_stats: external_exports.object({
|
|
22596
22770
|
draft: INVOICE_STAT_BUCKET,
|
|
22597
22771
|
sent: INVOICE_STAT_BUCKET,
|
|
22772
|
+
partial: INVOICE_STAT_BUCKET,
|
|
22598
22773
|
paid: INVOICE_STAT_BUCKET,
|
|
22599
|
-
overdue: INVOICE_STAT_BUCKET
|
|
22774
|
+
overdue: INVOICE_STAT_BUCKET,
|
|
22775
|
+
cancelled: INVOICE_STAT_BUCKET,
|
|
22776
|
+
no_due_date: INVOICE_STAT_BUCKET
|
|
22600
22777
|
}),
|
|
22601
22778
|
bank_summary: external_exports.object({
|
|
22602
22779
|
accounts: external_exports.array(
|
|
@@ -22636,9 +22813,10 @@ var INCOME_STATEMENT_OUTPUT_SHAPE = {
|
|
|
22636
22813
|
};
|
|
22637
22814
|
var ALV_SUMMARY_OUTPUT_SHAPE = {
|
|
22638
22815
|
period: external_exports.object({ from: external_exports.string(), to: external_exports.string() }),
|
|
22639
|
-
output_vat: external_exports.unknown(),
|
|
22640
|
-
input_vat: external_exports.unknown(),
|
|
22641
|
-
|
|
22816
|
+
output_vat: external_exports.unknown().optional(),
|
|
22817
|
+
input_vat: external_exports.unknown().optional(),
|
|
22818
|
+
net_vat_payable: external_exports.number().optional(),
|
|
22819
|
+
net_vat_payable_formatted: external_exports.string().optional(),
|
|
22642
22820
|
omavero_fields: external_exports.unknown().optional(),
|
|
22643
22821
|
warning: external_exports.string().optional(),
|
|
22644
22822
|
message: external_exports.string().optional()
|
|
@@ -22687,6 +22865,11 @@ var AGING_BUCKET_SHAPE = external_exports.object({
|
|
|
22687
22865
|
});
|
|
22688
22866
|
var AR_AGING_OUTPUT_SHAPE = {
|
|
22689
22867
|
buckets: external_exports.array(AGING_BUCKET_SHAPE),
|
|
22868
|
+
no_due_date: external_exports.object({
|
|
22869
|
+
count: external_exports.number(),
|
|
22870
|
+
total: external_exports.number(),
|
|
22871
|
+
invoices: external_exports.array(external_exports.record(external_exports.unknown()))
|
|
22872
|
+
}).passthrough(),
|
|
22690
22873
|
total_receivables: external_exports.number(),
|
|
22691
22874
|
total_receivables_formatted: external_exports.string(),
|
|
22692
22875
|
days_sales_outstanding: external_exports.number()
|
|
@@ -22712,7 +22895,9 @@ var GENERAL_LEDGER_OUTPUT_SHAPE = {
|
|
|
22712
22895
|
entry_count: external_exports.number(),
|
|
22713
22896
|
opening_balance: external_exports.number(),
|
|
22714
22897
|
closing_balance: external_exports.number(),
|
|
22715
|
-
closing_balance_formatted: external_exports.string()
|
|
22898
|
+
closing_balance_formatted: external_exports.string(),
|
|
22899
|
+
capped: external_exports.boolean(),
|
|
22900
|
+
capped_note: external_exports.string().nullable()
|
|
22716
22901
|
};
|
|
22717
22902
|
function registerReportTools(server2) {
|
|
22718
22903
|
server2.registerTool(
|
|
@@ -22767,20 +22952,31 @@ function registerReportTools(server2) {
|
|
|
22767
22952
|
expenses += (entry.debit_amount || 0) - (entry.credit_amount || 0);
|
|
22768
22953
|
}
|
|
22769
22954
|
}
|
|
22770
|
-
const now = (
|
|
22955
|
+
const now = helsinkiDateString();
|
|
22771
22956
|
const invoices = invoiceResult.data || [];
|
|
22772
22957
|
const invoiceStats = {
|
|
22773
22958
|
draft: { count: 0, total: 0 },
|
|
22774
22959
|
sent: { count: 0, total: 0 },
|
|
22960
|
+
partial: { count: 0, total: 0 },
|
|
22775
22961
|
paid: { count: 0, total: 0 },
|
|
22776
|
-
overdue: { count: 0, total: 0 }
|
|
22962
|
+
overdue: { count: 0, total: 0 },
|
|
22963
|
+
cancelled: { count: 0, total: 0 },
|
|
22964
|
+
no_due_date: { count: 0, total: 0 }
|
|
22777
22965
|
};
|
|
22778
22966
|
for (const inv of invoices) {
|
|
22779
|
-
const
|
|
22780
|
-
|
|
22781
|
-
|
|
22782
|
-
|
|
22967
|
+
const hasDueDate = typeof inv.due_date === "string" && inv.due_date.length > 0;
|
|
22968
|
+
let status;
|
|
22969
|
+
if (inv.status === "sent" && !hasDueDate) {
|
|
22970
|
+
status = "no_due_date";
|
|
22971
|
+
} else if (inv.status === "sent" && hasDueDate && inv.due_date < now) {
|
|
22972
|
+
status = "overdue";
|
|
22973
|
+
} else if (inv.status in invoiceStats) {
|
|
22974
|
+
status = inv.status;
|
|
22975
|
+
} else {
|
|
22976
|
+
continue;
|
|
22783
22977
|
}
|
|
22978
|
+
invoiceStats[status].count++;
|
|
22979
|
+
invoiceStats[status].total += inv.total || 0;
|
|
22784
22980
|
}
|
|
22785
22981
|
const bankAccounts = (bankResult.data || []).map((b) => ({
|
|
22786
22982
|
name: b.account_name || "Unnamed",
|
|
@@ -23012,15 +23208,40 @@ function registerReportTools(server2) {
|
|
|
23012
23208
|
];
|
|
23013
23209
|
let totalReceivables = 0;
|
|
23014
23210
|
let weightedDays = 0;
|
|
23211
|
+
const noDueDate = [];
|
|
23015
23212
|
for (const inv of invoices || []) {
|
|
23016
|
-
|
|
23017
|
-
|
|
23018
|
-
|
|
23213
|
+
if (!inv.due_date) {
|
|
23214
|
+
noDueDate.push({
|
|
23215
|
+
invoice_number: inv.invoice_number,
|
|
23216
|
+
customer: tagUntrusted(inv.customers?.name),
|
|
23217
|
+
total: inv.total || 0
|
|
23218
|
+
});
|
|
23219
|
+
totalReceivables += inv.total || 0;
|
|
23220
|
+
continue;
|
|
23221
|
+
}
|
|
23222
|
+
const dueMs = Date.parse(inv.due_date);
|
|
23223
|
+
if (Number.isNaN(dueMs)) {
|
|
23224
|
+
noDueDate.push({
|
|
23225
|
+
invoice_number: inv.invoice_number,
|
|
23226
|
+
customer: tagUntrusted(inv.customers?.name),
|
|
23227
|
+
total: inv.total || 0
|
|
23228
|
+
});
|
|
23229
|
+
totalReceivables += inv.total || 0;
|
|
23230
|
+
continue;
|
|
23231
|
+
}
|
|
23232
|
+
const daysOverdue = Math.max(
|
|
23233
|
+
0,
|
|
23234
|
+
Math.floor((now.getTime() - dueMs) / 864e5)
|
|
23235
|
+
);
|
|
23236
|
+
const bucket = buckets.find(
|
|
23237
|
+
(b) => daysOverdue >= b.min && daysOverdue <= b.max
|
|
23238
|
+
);
|
|
23239
|
+
if (!bucket) continue;
|
|
23019
23240
|
bucket.count++;
|
|
23020
23241
|
bucket.total += inv.total || 0;
|
|
23021
23242
|
bucket.invoices.push({
|
|
23022
23243
|
invoice_number: inv.invoice_number,
|
|
23023
|
-
customer: inv.customers?.name,
|
|
23244
|
+
customer: tagUntrusted(inv.customers?.name),
|
|
23024
23245
|
total: inv.total,
|
|
23025
23246
|
due_date: inv.due_date,
|
|
23026
23247
|
days_overdue: daysOverdue
|
|
@@ -23037,6 +23258,13 @@ function registerReportTools(server2) {
|
|
|
23037
23258
|
total_formatted: formatCurrency(b.total),
|
|
23038
23259
|
invoices: b.invoices
|
|
23039
23260
|
})),
|
|
23261
|
+
no_due_date: {
|
|
23262
|
+
count: noDueDate.length,
|
|
23263
|
+
total: roundCents(
|
|
23264
|
+
noDueDate.reduce((s, i) => s + (i.total || 0), 0)
|
|
23265
|
+
),
|
|
23266
|
+
invoices: noDueDate
|
|
23267
|
+
},
|
|
23040
23268
|
total_receivables: roundCents(totalReceivables),
|
|
23041
23269
|
total_receivables_formatted: formatCurrency(totalReceivables),
|
|
23042
23270
|
days_sales_outstanding: dso
|
|
@@ -23073,10 +23301,11 @@ function registerReportTools(server2) {
|
|
|
23073
23301
|
`Account "${params.accountNumber}" not found. Use kirjapro_list_accounts to see available accounts.`
|
|
23074
23302
|
);
|
|
23075
23303
|
}
|
|
23304
|
+
const LEDGER_FETCH_LIMIT = 5e3;
|
|
23076
23305
|
let query = supabase2.from("journal_entries").select(`
|
|
23077
23306
|
debit_amount, credit_amount,
|
|
23078
23307
|
transaction:transactions!inner(id, transaction_date, description, document_number, fiscal_year_id, company_id)
|
|
23079
|
-
`).eq("account_id", account.id).eq("transaction.fiscal_year_id", fiscalYearId).eq("transaction.company_id", companyId2).order("transaction(transaction_date)", { ascending: true }).order("id", { ascending: true });
|
|
23308
|
+
`).eq("account_id", account.id).eq("transaction.fiscal_year_id", fiscalYearId).eq("transaction.company_id", companyId2).order("transaction(transaction_date)", { ascending: true }).order("id", { ascending: true }).range(0, LEDGER_FETCH_LIMIT);
|
|
23080
23309
|
if (params.dateFrom) {
|
|
23081
23310
|
query = query.gte("transaction.transaction_date", params.dateFrom);
|
|
23082
23311
|
}
|
|
@@ -23089,9 +23318,27 @@ function registerReportTools(server2) {
|
|
|
23089
23318
|
}
|
|
23090
23319
|
const debitNormal = ["asset", "expense"];
|
|
23091
23320
|
const isDebitNormal = debitNormal.includes(account.account_type);
|
|
23092
|
-
let
|
|
23321
|
+
let openingBalance = 0;
|
|
23322
|
+
if (params.dateFrom) {
|
|
23323
|
+
const { data: priorEntries, error: priorErr } = await supabase2.from("journal_entries").select(`
|
|
23324
|
+
debit_amount, credit_amount,
|
|
23325
|
+
transaction:transactions!inner(fiscal_year_id, company_id, transaction_date)
|
|
23326
|
+
`).eq("account_id", account.id).eq("transaction.fiscal_year_id", fiscalYearId).eq("transaction.company_id", companyId2).lt("transaction.transaction_date", params.dateFrom);
|
|
23327
|
+
if (!priorErr && priorEntries) {
|
|
23328
|
+
for (const e of priorEntries) {
|
|
23329
|
+
const d = e.debit_amount || 0;
|
|
23330
|
+
const c = e.credit_amount || 0;
|
|
23331
|
+
openingBalance += isDebitNormal ? d - c : c - d;
|
|
23332
|
+
}
|
|
23333
|
+
openingBalance = roundCents(openingBalance);
|
|
23334
|
+
}
|
|
23335
|
+
}
|
|
23336
|
+
const fetched = entries || [];
|
|
23337
|
+
const capped = fetched.length > LEDGER_FETCH_LIMIT;
|
|
23338
|
+
const consumed = capped ? fetched.slice(0, LEDGER_FETCH_LIMIT) : fetched;
|
|
23339
|
+
let runningBalance = openingBalance;
|
|
23093
23340
|
const ledgerEntries = [];
|
|
23094
|
-
for (const entry of
|
|
23341
|
+
for (const entry of consumed) {
|
|
23095
23342
|
const tx = entry.transaction;
|
|
23096
23343
|
const debit = entry.debit_amount || 0;
|
|
23097
23344
|
const credit = entry.credit_amount || 0;
|
|
@@ -23119,9 +23366,11 @@ function registerReportTools(server2) {
|
|
|
23119
23366
|
},
|
|
23120
23367
|
entries: ledgerEntries,
|
|
23121
23368
|
entry_count: ledgerEntries.length,
|
|
23122
|
-
opening_balance:
|
|
23369
|
+
opening_balance: openingBalance,
|
|
23123
23370
|
closing_balance: roundCents(runningBalance),
|
|
23124
|
-
closing_balance_formatted: formatCurrency(runningBalance)
|
|
23371
|
+
closing_balance_formatted: formatCurrency(runningBalance),
|
|
23372
|
+
capped,
|
|
23373
|
+
capped_note: capped ? `Result truncated at ${LEDGER_FETCH_LIMIT} entries. Closing balance is computed from fetched rows only and may not reflect the full fiscal-year total. Narrow the date range with dateFrom/dateTo to see the remainder.` : null
|
|
23125
23374
|
});
|
|
23126
23375
|
}
|
|
23127
23376
|
);
|
|
@@ -23253,13 +23502,12 @@ function registerBankingTools(server2) {
|
|
|
23253
23502
|
matchError ? sanitizeDbError("Failed to confirm reconciliation match", matchError) : "Failed to confirm reconciliation match."
|
|
23254
23503
|
);
|
|
23255
23504
|
}
|
|
23256
|
-
const
|
|
23257
|
-
const newStatus = newPaidTotal >= invoice.total ? "paid" : "partial";
|
|
23505
|
+
const result = matchResult;
|
|
23258
23506
|
return toTextContent({
|
|
23259
23507
|
message: `Bank transaction matched to invoice ${invoice.invoice_number}`,
|
|
23260
23508
|
payment_amount: paymentAmount,
|
|
23261
|
-
invoice_status:
|
|
23262
|
-
remaining_balance: roundCents(remaining - paymentAmount),
|
|
23509
|
+
invoice_status: result.invoice_status || "partial",
|
|
23510
|
+
remaining_balance: result.remaining_balance ?? roundCents(remaining - paymentAmount),
|
|
23263
23511
|
bank_transaction_reconciled: true
|
|
23264
23512
|
});
|
|
23265
23513
|
}
|
|
@@ -23271,6 +23519,10 @@ function registerBankingTools(server2) {
|
|
|
23271
23519
|
{
|
|
23272
23520
|
description: "List active bank accounts with balances, IBAN, and sync status.",
|
|
23273
23521
|
inputSchema: {},
|
|
23522
|
+
outputSchema: {
|
|
23523
|
+
accounts: external_exports.array(external_exports.record(external_exports.unknown())),
|
|
23524
|
+
count: external_exports.number()
|
|
23525
|
+
},
|
|
23274
23526
|
annotations: READ_ONLY_ANNOTATIONS
|
|
23275
23527
|
},
|
|
23276
23528
|
async () => {
|
|
@@ -23282,7 +23534,7 @@ function registerBankingTools(server2) {
|
|
|
23282
23534
|
if (error2) {
|
|
23283
23535
|
return toErrorContent(sanitizeDbError("Failed to list bank accounts", error2));
|
|
23284
23536
|
}
|
|
23285
|
-
return
|
|
23537
|
+
return toResult({
|
|
23286
23538
|
accounts: data || [],
|
|
23287
23539
|
count: (data || []).length
|
|
23288
23540
|
});
|
|
@@ -23302,6 +23554,7 @@ function registerBankingTools(server2) {
|
|
|
23302
23554
|
limit: external_exports.number().min(1).max(200).default(50).describe("Max results (default 50)"),
|
|
23303
23555
|
offset: external_exports.number().min(0).default(0).describe("Skip first N results")
|
|
23304
23556
|
},
|
|
23557
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
23305
23558
|
annotations: READ_ONLY_ANNOTATIONS
|
|
23306
23559
|
},
|
|
23307
23560
|
async (params) => {
|
|
@@ -23343,15 +23596,21 @@ function registerBankingTools(server2) {
|
|
|
23343
23596
|
const tagged = (data || []).map((tx) => ({
|
|
23344
23597
|
...tx,
|
|
23345
23598
|
counterparty_name: tagUntrusted(tx.counterparty_name),
|
|
23599
|
+
counterparty_iban: tagUntrusted(tx.counterparty_iban),
|
|
23346
23600
|
reference: tagUntrusted(tx.reference),
|
|
23347
23601
|
message: tagUntrusted(tx.message)
|
|
23348
23602
|
}));
|
|
23349
|
-
return
|
|
23603
|
+
return toResult(paginate(tagged, count || 0, offset, limit));
|
|
23350
23604
|
}
|
|
23351
23605
|
);
|
|
23352
23606
|
}
|
|
23353
23607
|
|
|
23354
23608
|
// src/tools/validation.ts
|
|
23609
|
+
var REFERENCE_OUTPUT_SHAPE = {
|
|
23610
|
+
input: external_exports.string(),
|
|
23611
|
+
reference: external_exports.string(),
|
|
23612
|
+
error: external_exports.string().optional()
|
|
23613
|
+
};
|
|
23355
23614
|
function validateFinnishBusinessId2(input) {
|
|
23356
23615
|
const match = input.match(/^(\d{7})-(\d)$/);
|
|
23357
23616
|
if (!match) {
|
|
@@ -23419,11 +23678,12 @@ function registerValidationTools(server2) {
|
|
|
23419
23678
|
inputSchema: {
|
|
23420
23679
|
businessId: external_exports.string().describe('Y-tunnus to validate (e.g. "1234567-8")')
|
|
23421
23680
|
},
|
|
23681
|
+
outputSchema: VALIDATION_OUTPUT_SHAPE,
|
|
23422
23682
|
annotations: READ_ONLY_ANNOTATIONS
|
|
23423
23683
|
},
|
|
23424
23684
|
async (params) => {
|
|
23425
23685
|
const result = validateFinnishBusinessId2(params.businessId);
|
|
23426
|
-
return
|
|
23686
|
+
return toResult({
|
|
23427
23687
|
input: params.businessId,
|
|
23428
23688
|
...result
|
|
23429
23689
|
});
|
|
@@ -23436,11 +23696,12 @@ function registerValidationTools(server2) {
|
|
|
23436
23696
|
inputSchema: {
|
|
23437
23697
|
iban: external_exports.string().describe('Finnish IBAN to validate (e.g. "FI2112345678901234")')
|
|
23438
23698
|
},
|
|
23699
|
+
outputSchema: VALIDATION_OUTPUT_SHAPE,
|
|
23439
23700
|
annotations: READ_ONLY_ANNOTATIONS
|
|
23440
23701
|
},
|
|
23441
23702
|
async (params) => {
|
|
23442
23703
|
const result = validateFinnishIBAN(params.iban);
|
|
23443
|
-
return
|
|
23704
|
+
return toResult({
|
|
23444
23705
|
input: params.iban,
|
|
23445
23706
|
...result
|
|
23446
23707
|
});
|
|
@@ -23453,11 +23714,12 @@ function registerValidationTools(server2) {
|
|
|
23453
23714
|
inputSchema: {
|
|
23454
23715
|
baseNumber: external_exports.string().describe('Base number (1-19 digits, e.g. invoice number "00042")')
|
|
23455
23716
|
},
|
|
23717
|
+
outputSchema: REFERENCE_OUTPUT_SHAPE,
|
|
23456
23718
|
annotations: READ_ONLY_ANNOTATIONS
|
|
23457
23719
|
},
|
|
23458
23720
|
async (params) => {
|
|
23459
23721
|
const result = generateReferenceNumber2(params.baseNumber);
|
|
23460
|
-
return
|
|
23722
|
+
return toResult({
|
|
23461
23723
|
input: params.baseNumber,
|
|
23462
23724
|
...result
|
|
23463
23725
|
});
|
|
@@ -23472,6 +23734,16 @@ function registerFiscalYearTools(server2) {
|
|
|
23472
23734
|
{
|
|
23473
23735
|
description: "List fiscal years (tilikaudet) for the company. Shows start/end dates and open/closed status. Many tools require a fiscal year ID \u2014 use this to find the right one.",
|
|
23474
23736
|
inputSchema: {},
|
|
23737
|
+
outputSchema: {
|
|
23738
|
+
fiscal_years: external_exports.array(external_exports.object({
|
|
23739
|
+
id: external_exports.string().uuid(),
|
|
23740
|
+
start_date: external_exports.string(),
|
|
23741
|
+
end_date: external_exports.string(),
|
|
23742
|
+
is_closed: external_exports.boolean(),
|
|
23743
|
+
created_at: external_exports.string()
|
|
23744
|
+
}).passthrough()),
|
|
23745
|
+
count: external_exports.number()
|
|
23746
|
+
},
|
|
23475
23747
|
annotations: READ_ONLY_ANNOTATIONS
|
|
23476
23748
|
},
|
|
23477
23749
|
async () => {
|
|
@@ -23481,7 +23753,7 @@ function registerFiscalYearTools(server2) {
|
|
|
23481
23753
|
if (error2) {
|
|
23482
23754
|
return toErrorContent(sanitizeDbError("Failed to list fiscal years", error2));
|
|
23483
23755
|
}
|
|
23484
|
-
return
|
|
23756
|
+
return toResult({
|
|
23485
23757
|
fiscal_years: data || [],
|
|
23486
23758
|
count: (data || []).length
|
|
23487
23759
|
});
|
|
@@ -23926,6 +24198,13 @@ function registerTemplateTools(server2) {
|
|
|
23926
24198
|
{
|
|
23927
24199
|
description: "List available transaction templates. Templates handle double-entry balancing and VAT splitting automatically.",
|
|
23928
24200
|
inputSchema: {},
|
|
24201
|
+
outputSchema: {
|
|
24202
|
+
templates: external_exports.array(external_exports.object({
|
|
24203
|
+
id: external_exports.string(),
|
|
24204
|
+
description: external_exports.string(),
|
|
24205
|
+
vatRate: external_exports.number().nullable()
|
|
24206
|
+
}))
|
|
24207
|
+
},
|
|
23929
24208
|
annotations: READ_ONLY_ANNOTATIONS
|
|
23930
24209
|
},
|
|
23931
24210
|
async () => {
|
|
@@ -23934,7 +24213,7 @@ function registerTemplateTools(server2) {
|
|
|
23934
24213
|
description: t.description,
|
|
23935
24214
|
vatRate: t.vatRate
|
|
23936
24215
|
}));
|
|
23937
|
-
return
|
|
24216
|
+
return toResult({ templates: list });
|
|
23938
24217
|
}
|
|
23939
24218
|
);
|
|
23940
24219
|
server2.registerTool(
|
|
@@ -23945,7 +24224,7 @@ function registerTemplateTools(server2) {
|
|
|
23945
24224
|
template: external_exports.string().describe('Template ID (e.g. "cash_sale_25", "purchase_expense")'),
|
|
23946
24225
|
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
|
|
23947
24226
|
transactionDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Transaction date (YYYY-MM-DD)"),
|
|
23948
|
-
amount: external_exports.number().positive().describe("Gross amount in EUR"),
|
|
24227
|
+
amount: external_exports.number().finite().positive().max(1e9).describe("Gross amount in EUR"),
|
|
23949
24228
|
description: external_exports.string().min(1).max(500).describe("Transaction description"),
|
|
23950
24229
|
documentNumber: external_exports.string().optional().describe("Document/receipt number"),
|
|
23951
24230
|
withholdingRate: external_exports.number().min(0).max(100).optional().describe("Withholding tax % for salary_payment (default 20)"),
|
|
@@ -23995,14 +24274,17 @@ function registerTemplateTools(server2) {
|
|
|
23995
24274
|
server2.registerTool(
|
|
23996
24275
|
"kirjapro_create_from_template_batch",
|
|
23997
24276
|
{
|
|
23998
|
-
description:
|
|
24277
|
+
description: 'Create multiple transactions from templates in one call (up to 50). Default mode "atomic" rolls back ALL on any failure; "best_effort" collects per-row errors.',
|
|
23999
24278
|
inputSchema: {
|
|
24000
24279
|
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
|
|
24280
|
+
mode: external_exports.enum(["atomic", "best_effort"]).default("atomic").describe(
|
|
24281
|
+
'Batch mode. "atomic" (default): all-or-nothing, rolls back on any row failure. "best_effort": commits what it can, returns per-row errors.'
|
|
24282
|
+
),
|
|
24001
24283
|
transactions: external_exports.array(
|
|
24002
24284
|
external_exports.object({
|
|
24003
24285
|
template: external_exports.string().describe("Template ID"),
|
|
24004
24286
|
transactionDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Date (YYYY-MM-DD)"),
|
|
24005
|
-
amount: external_exports.number().positive().describe("Gross amount in EUR"),
|
|
24287
|
+
amount: external_exports.number().finite().positive().max(1e9).describe("Gross amount in EUR"),
|
|
24006
24288
|
description: external_exports.string().min(1).max(500),
|
|
24007
24289
|
documentNumber: external_exports.string().optional(),
|
|
24008
24290
|
withholdingRate: external_exports.number().min(0).max(100).optional(),
|
|
@@ -24023,41 +24305,126 @@ function registerTemplateTools(server2) {
|
|
|
24023
24305
|
"kirjapro_create_from_template_batch",
|
|
24024
24306
|
params.idempotencyKey,
|
|
24025
24307
|
async () => {
|
|
24026
|
-
|
|
24027
|
-
|
|
24308
|
+
let fiscalYearId = params.fiscalYearId;
|
|
24309
|
+
if (!fiscalYearId) {
|
|
24310
|
+
const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
|
|
24311
|
+
if ("isError" in resolved) return resolved;
|
|
24312
|
+
fiscalYearId = resolved.id;
|
|
24313
|
+
}
|
|
24314
|
+
const { data: fy, error: fyError } = await supabase2.from("fiscal_years").select("id, is_closed, start_date, end_date").eq("id", fiscalYearId).eq("company_id", companyId2).single();
|
|
24315
|
+
if (fyError || !fy) {
|
|
24316
|
+
return toErrorContent(
|
|
24317
|
+
`Fiscal year ${fiscalYearId} not found. Use kirjapro_get_dashboard or read kirjapro://fiscal-years resource.`
|
|
24318
|
+
);
|
|
24319
|
+
}
|
|
24320
|
+
if (fy.is_closed) {
|
|
24321
|
+
return toErrorContent(
|
|
24322
|
+
`Fiscal year is closed (${fy.start_date} to ${fy.end_date}).`
|
|
24323
|
+
);
|
|
24324
|
+
}
|
|
24325
|
+
const expanded = [];
|
|
24326
|
+
const preFailed = [];
|
|
24028
24327
|
for (let i = 0; i < params.transactions.length; i++) {
|
|
24029
24328
|
const tx = params.transactions[i];
|
|
24030
|
-
const
|
|
24329
|
+
const result2 = expandTemplate(tx.template, tx.amount, {
|
|
24031
24330
|
withholdingRate: tx.withholdingRate,
|
|
24032
24331
|
tyelRate: tx.tyelRate,
|
|
24033
24332
|
accountOverrides: tx.accountOverrides
|
|
24034
24333
|
});
|
|
24035
|
-
if ("error" in
|
|
24036
|
-
|
|
24334
|
+
if ("error" in result2) {
|
|
24335
|
+
preFailed.push({ index: i, error: result2.error });
|
|
24037
24336
|
continue;
|
|
24038
24337
|
}
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
24043
|
-
|
|
24044
|
-
|
|
24045
|
-
}
|
|
24046
|
-
|
|
24047
|
-
|
|
24338
|
+
if (tx.transactionDate < fy.start_date || tx.transactionDate > fy.end_date) {
|
|
24339
|
+
preFailed.push({
|
|
24340
|
+
index: i,
|
|
24341
|
+
error: `Date ${tx.transactionDate} outside fiscal year range (${fy.start_date} to ${fy.end_date}).`
|
|
24342
|
+
});
|
|
24343
|
+
continue;
|
|
24344
|
+
}
|
|
24345
|
+
const totalDebit = roundCents(
|
|
24346
|
+
result2.entries.reduce((s, e) => s + e.debitAmount, 0)
|
|
24347
|
+
);
|
|
24348
|
+
const totalCredit = roundCents(
|
|
24349
|
+
result2.entries.reduce((s, e) => s + e.creditAmount, 0)
|
|
24350
|
+
);
|
|
24351
|
+
if (Math.abs(totalDebit - totalCredit) >= 0.01) {
|
|
24352
|
+
preFailed.push({
|
|
24353
|
+
index: i,
|
|
24354
|
+
error: `Template produced unbalanced entries: debits ${totalDebit.toFixed(2)} != credits ${totalCredit.toFixed(2)}.`
|
|
24355
|
+
});
|
|
24048
24356
|
continue;
|
|
24049
24357
|
}
|
|
24050
|
-
|
|
24051
|
-
|
|
24358
|
+
expanded.push({
|
|
24359
|
+
index: i,
|
|
24360
|
+
transactionDate: tx.transactionDate,
|
|
24052
24361
|
description: tx.description,
|
|
24053
|
-
|
|
24362
|
+
documentNumber: tx.documentNumber || "",
|
|
24363
|
+
entries: result2.entries
|
|
24054
24364
|
});
|
|
24055
24365
|
}
|
|
24366
|
+
if (params.mode === "atomic" && preFailed.length > 0) {
|
|
24367
|
+
return toErrorContent(
|
|
24368
|
+
`Atomic batch aborted: ${preFailed.length} transaction(s) failed validation. Fix all errors or use mode "best_effort" for partial commits.
|
|
24369
|
+
` + preFailed.map((f) => ` [${f.index}] ${f.error}`).join("\n")
|
|
24370
|
+
);
|
|
24371
|
+
}
|
|
24372
|
+
if (expanded.length === 0) {
|
|
24373
|
+
return toTextContent({
|
|
24374
|
+
mode: params.mode,
|
|
24375
|
+
created_count: 0,
|
|
24376
|
+
failed_count: preFailed.length,
|
|
24377
|
+
created: [],
|
|
24378
|
+
failed: preFailed
|
|
24379
|
+
});
|
|
24380
|
+
}
|
|
24381
|
+
const allAccountNumbers = [
|
|
24382
|
+
...new Set(
|
|
24383
|
+
expanded.flatMap((e) => e.entries.map((en) => en.accountNumber))
|
|
24384
|
+
)
|
|
24385
|
+
];
|
|
24386
|
+
const accountResult = await resolveAccountNumbers(allAccountNumbers);
|
|
24387
|
+
if ("isError" in accountResult) return accountResult;
|
|
24388
|
+
const accountMap = accountResult;
|
|
24389
|
+
const rpcTransactions = expanded.map((e) => ({
|
|
24390
|
+
transaction_date: e.transactionDate,
|
|
24391
|
+
description: e.description,
|
|
24392
|
+
document_number: e.documentNumber,
|
|
24393
|
+
entries: toJournalEntries(e.entries, accountMap)
|
|
24394
|
+
}));
|
|
24395
|
+
const { data: rpcResult, error: rpcError } = await supabase2.rpc(
|
|
24396
|
+
"create_transactions_batch_atomic",
|
|
24397
|
+
{
|
|
24398
|
+
p_company_id: companyId2,
|
|
24399
|
+
p_fiscal_year_id: fiscalYearId,
|
|
24400
|
+
p_transactions: rpcTransactions,
|
|
24401
|
+
p_mode: params.mode
|
|
24402
|
+
}
|
|
24403
|
+
);
|
|
24404
|
+
if (rpcError) {
|
|
24405
|
+
return toErrorContent(
|
|
24406
|
+
sanitizeDbError("Batch create failed", rpcError)
|
|
24407
|
+
);
|
|
24408
|
+
}
|
|
24409
|
+
const result = rpcResult;
|
|
24410
|
+
const created = result.created.map((c) => ({
|
|
24411
|
+
id: c.id,
|
|
24412
|
+
description: c.description,
|
|
24413
|
+
date: c.date,
|
|
24414
|
+
index: expanded[c.index].index
|
|
24415
|
+
}));
|
|
24416
|
+
const rpcFailed = result.failed.map((f) => ({
|
|
24417
|
+
index: expanded[f.index].index,
|
|
24418
|
+
error: sanitizeDbError("Failed to create transaction", {
|
|
24419
|
+
message: f.error
|
|
24420
|
+
})
|
|
24421
|
+
}));
|
|
24056
24422
|
return toTextContent({
|
|
24423
|
+
mode: params.mode,
|
|
24057
24424
|
created_count: created.length,
|
|
24058
|
-
failed_count:
|
|
24425
|
+
failed_count: preFailed.length + rpcFailed.length,
|
|
24059
24426
|
created,
|
|
24060
|
-
failed
|
|
24427
|
+
failed: [...preFailed, ...rpcFailed]
|
|
24061
24428
|
});
|
|
24062
24429
|
}
|
|
24063
24430
|
);
|
|
@@ -24079,7 +24446,9 @@ function registerBookingTools(server2) {
|
|
|
24079
24446
|
{
|
|
24080
24447
|
description: "Book a purchase (osto) with automatic VAT splitting. Supports reverse charge (AVL 9\xA7) and entertainment (edustus, AVL 114\xA7). Rate rules: food/restaurants 13.5%, insurance 0% (exempt), alcohol 25.5%.",
|
|
24081
24448
|
inputSchema: {
|
|
24082
|
-
amount: external_exports.number().positive().describe(
|
|
24449
|
+
amount: external_exports.number().finite().positive().max(1e9).describe(
|
|
24450
|
+
"EUR amount. Standard purchase: GROSS (VAT-inclusive) \u2014 VAT is split out of this. Reverse charge (reverseCharge=true): NET foreign-invoice amount \u2014 the foreign supplier does not charge Finnish VAT, so the parameter must be net. VAT is self-assessed on top per AVL 9\xA7 producing equal debit to ALV-saamiset and credit to ALV-velka. Entertainment (edustus=true): GROSS (full amount to expense, no VAT deduction per AVL 114\xA7)."
|
|
24451
|
+
),
|
|
24083
24452
|
description: external_exports.string().min(1).max(500).describe("Transaction description"),
|
|
24084
24453
|
date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Transaction date (YYYY-MM-DD)"),
|
|
24085
24454
|
vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).describe("Finnish VAT rate"),
|
|
@@ -24164,7 +24533,7 @@ function registerBookingTools(server2) {
|
|
|
24164
24533
|
{
|
|
24165
24534
|
description: "Book salary payment (palkanmaksu). Creates salary + employer cost transactions with withholding, TyEL, TTV, sotu. Default rates are 2026 Finnish social insurance rates.",
|
|
24166
24535
|
inputSchema: {
|
|
24167
|
-
grossSalary: external_exports.number().positive().describe("Gross salary EUR"),
|
|
24536
|
+
grossSalary: external_exports.number().finite().positive().max(1e8).describe("Gross salary EUR"),
|
|
24168
24537
|
employeeName: external_exports.string().min(1).describe("Employee name (for description)"),
|
|
24169
24538
|
date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Payment date (YYYY-MM-DD)"),
|
|
24170
24539
|
withholdingRate: external_exports.number().min(0).max(100).describe("Withholding tax % (ennakonpid\xE4tys)"),
|
|
@@ -24380,7 +24749,7 @@ function registerBookingTools(server2) {
|
|
|
24380
24749
|
{
|
|
24381
24750
|
description: "Book a sale (myynti) with automatic VAT splitting. Payment types: invoice (default, D Myyntisaamiset), cash (D bank), prepaid.",
|
|
24382
24751
|
inputSchema: {
|
|
24383
|
-
amount: external_exports.number().positive().describe("Gross EUR amount (VAT-inclusive)"),
|
|
24752
|
+
amount: external_exports.number().finite().positive().max(1e9).describe("Gross EUR amount (VAT-inclusive)"),
|
|
24384
24753
|
description: external_exports.string().min(1).max(500).describe('Transaction description (e.g. "Konsultointi Acme Oy, 40h \xD7 95 \u20AC")'),
|
|
24385
24754
|
date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Transaction date (YYYY-MM-DD)"),
|
|
24386
24755
|
vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).describe("Finnish VAT rate"),
|
|
@@ -24460,6 +24829,19 @@ function registerBookingTools(server2) {
|
|
|
24460
24829
|
}
|
|
24461
24830
|
|
|
24462
24831
|
// src/tools/offers.ts
|
|
24832
|
+
var OFFER_DETAIL_OUTPUT_SHAPE = {
|
|
24833
|
+
id: external_exports.string(),
|
|
24834
|
+
offer_number: external_exports.string(),
|
|
24835
|
+
type: external_exports.string(),
|
|
24836
|
+
title: external_exports.string(),
|
|
24837
|
+
status: external_exports.string(),
|
|
24838
|
+
subtotal: external_exports.number(),
|
|
24839
|
+
vat_total: external_exports.number(),
|
|
24840
|
+
total: external_exports.number(),
|
|
24841
|
+
customers: external_exports.record(external_exports.unknown()).nullable(),
|
|
24842
|
+
offer_lines: external_exports.array(external_exports.record(external_exports.unknown())),
|
|
24843
|
+
offer_events: external_exports.array(external_exports.record(external_exports.unknown()))
|
|
24844
|
+
};
|
|
24463
24845
|
function registerOfferTools(server2) {
|
|
24464
24846
|
server2.registerTool(
|
|
24465
24847
|
"kirjapro_list_offers",
|
|
@@ -24471,6 +24853,7 @@ function registerOfferTools(server2) {
|
|
|
24471
24853
|
limit: external_exports.number().min(1).max(200).default(50).describe("Max results (default 50)"),
|
|
24472
24854
|
offset: external_exports.number().min(0).default(0).describe("Skip first N results")
|
|
24473
24855
|
},
|
|
24856
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
24474
24857
|
annotations: READ_ONLY_ANNOTATIONS
|
|
24475
24858
|
},
|
|
24476
24859
|
async (params) => {
|
|
@@ -24493,7 +24876,13 @@ function registerOfferTools(server2) {
|
|
|
24493
24876
|
if (error2) {
|
|
24494
24877
|
return toErrorContent(sanitizeDbError("Failed to list offers", error2));
|
|
24495
24878
|
}
|
|
24496
|
-
|
|
24879
|
+
const tagged = (data || []).map((row) => ({
|
|
24880
|
+
...row,
|
|
24881
|
+
customers: tagCustomer(
|
|
24882
|
+
row.customers
|
|
24883
|
+
)
|
|
24884
|
+
}));
|
|
24885
|
+
return toResult(paginate(tagged, count || 0, offset, limit));
|
|
24497
24886
|
}
|
|
24498
24887
|
);
|
|
24499
24888
|
server2.registerTool(
|
|
@@ -24503,6 +24892,7 @@ function registerOfferTools(server2) {
|
|
|
24503
24892
|
inputSchema: {
|
|
24504
24893
|
offerId: external_exports.string().uuid().describe("Offer UUID")
|
|
24505
24894
|
},
|
|
24895
|
+
outputSchema: OFFER_DETAIL_OUTPUT_SHAPE,
|
|
24506
24896
|
annotations: READ_ONLY_ANNOTATIONS
|
|
24507
24897
|
},
|
|
24508
24898
|
async (params) => {
|
|
@@ -24521,7 +24911,12 @@ function registerOfferTools(server2) {
|
|
|
24521
24911
|
`Offer ${params.offerId} not found. Use kirjapro_list_offers to find offer IDs.`
|
|
24522
24912
|
);
|
|
24523
24913
|
}
|
|
24524
|
-
return
|
|
24914
|
+
return toResult({
|
|
24915
|
+
...offer,
|
|
24916
|
+
customers: tagCustomer(
|
|
24917
|
+
offer.customers
|
|
24918
|
+
)
|
|
24919
|
+
});
|
|
24525
24920
|
}
|
|
24526
24921
|
);
|
|
24527
24922
|
server2.registerTool(
|
|
@@ -24540,7 +24935,7 @@ function registerOfferTools(server2) {
|
|
|
24540
24935
|
description: external_exports.string().min(1).describe("Line item description"),
|
|
24541
24936
|
quantity: external_exports.number().min(0).describe("Quantity"),
|
|
24542
24937
|
unit: external_exports.string().default("kpl").describe("Unit (kpl, h, kk, etc.)"),
|
|
24543
|
-
unitPrice: external_exports.number().describe("Unit price in EUR (excl. VAT)"),
|
|
24938
|
+
unitPrice: external_exports.number().finite().min(-1e9).max(1e9).describe("Unit price in EUR (excl. VAT). Can be negative for credit lines."),
|
|
24544
24939
|
vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).default("25.5").describe("VAT rate")
|
|
24545
24940
|
})).min(1).describe("Line items"),
|
|
24546
24941
|
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
|
|
@@ -24870,7 +25265,7 @@ function registerOfferTools(server2) {
|
|
|
24870
25265
|
`Offer ${offer.offer_number} has already been converted to invoice ${offer.invoice_id}.`
|
|
24871
25266
|
);
|
|
24872
25267
|
}
|
|
24873
|
-
const today = (
|
|
25268
|
+
const today = helsinkiDateString();
|
|
24874
25269
|
const { data: result, error: rpcError } = await supabase2.rpc("convert_offer_to_invoice", {
|
|
24875
25270
|
p_offer_id: params.offerId,
|
|
24876
25271
|
p_company_id: companyId2,
|
|
@@ -24898,6 +25293,16 @@ function registerOfferTools(server2) {
|
|
|
24898
25293
|
}
|
|
24899
25294
|
|
|
24900
25295
|
// src/tools/recurring-invoices.ts
|
|
25296
|
+
var RECURRING_INVOICE_DETAIL_OUTPUT_SHAPE = {
|
|
25297
|
+
id: external_exports.string(),
|
|
25298
|
+
name: external_exports.string(),
|
|
25299
|
+
frequency: external_exports.string(),
|
|
25300
|
+
next_date: external_exports.string().nullable(),
|
|
25301
|
+
is_active: external_exports.boolean(),
|
|
25302
|
+
template_data: external_exports.record(external_exports.unknown()).nullable(),
|
|
25303
|
+
customer: external_exports.record(external_exports.unknown()).nullable(),
|
|
25304
|
+
recent_logs: external_exports.array(external_exports.record(external_exports.unknown()))
|
|
25305
|
+
};
|
|
24901
25306
|
function registerRecurringInvoiceTools(server2) {
|
|
24902
25307
|
server2.registerTool(
|
|
24903
25308
|
"kirjapro_list_recurring_invoices",
|
|
@@ -24908,6 +25313,7 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
24908
25313
|
offset: external_exports.number().min(0).default(0).describe("Skip first N results"),
|
|
24909
25314
|
activeOnly: external_exports.boolean().default(true).describe("Only show active templates (default true)")
|
|
24910
25315
|
},
|
|
25316
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
24911
25317
|
annotations: READ_ONLY_ANNOTATIONS
|
|
24912
25318
|
},
|
|
24913
25319
|
async (params) => {
|
|
@@ -24926,7 +25332,13 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
24926
25332
|
if (error2) {
|
|
24927
25333
|
return toErrorContent(sanitizeDbError("Failed to list recurring invoices", error2));
|
|
24928
25334
|
}
|
|
24929
|
-
|
|
25335
|
+
const tagged = (data || []).map((row) => ({
|
|
25336
|
+
...row,
|
|
25337
|
+
customer: tagCustomer(
|
|
25338
|
+
row.customer
|
|
25339
|
+
)
|
|
25340
|
+
}));
|
|
25341
|
+
return toResult(paginate(tagged, count || 0, offset, limit));
|
|
24930
25342
|
}
|
|
24931
25343
|
);
|
|
24932
25344
|
server2.registerTool(
|
|
@@ -24936,13 +25348,14 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
24936
25348
|
inputSchema: {
|
|
24937
25349
|
recurringInvoiceId: external_exports.string().uuid().describe("Recurring invoice UUID")
|
|
24938
25350
|
},
|
|
25351
|
+
outputSchema: RECURRING_INVOICE_DETAIL_OUTPUT_SHAPE,
|
|
24939
25352
|
annotations: READ_ONLY_ANNOTATIONS
|
|
24940
25353
|
},
|
|
24941
25354
|
async (params) => {
|
|
24942
25355
|
const supabase2 = getSupabase();
|
|
24943
25356
|
const companyId2 = getCompanyId();
|
|
24944
25357
|
const { data, error: error2 } = await supabase2.from("recurring_invoices").select(
|
|
24945
|
-
"
|
|
25358
|
+
"id, company_id, customer_id, name, frequency, next_date, end_date, is_active, auto_send, template_data, last_generated_at, times_generated, created_at, updated_at, customer:customers(id, name, business_id, email)"
|
|
24946
25359
|
).eq("id", params.recurringInvoiceId).eq("company_id", companyId2).single();
|
|
24947
25360
|
if (error2 || !data) {
|
|
24948
25361
|
return toErrorContent(
|
|
@@ -24950,8 +25363,11 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
24950
25363
|
);
|
|
24951
25364
|
}
|
|
24952
25365
|
const { data: logs } = await supabase2.from("recurring_invoice_logs").select("id, status, scheduled_date, invoice_number, error_message, created_at").eq("recurring_invoice_id", params.recurringInvoiceId).eq("company_id", companyId2).order("created_at", { ascending: false }).limit(10);
|
|
24953
|
-
return
|
|
25366
|
+
return toResult({
|
|
24954
25367
|
...data,
|
|
25368
|
+
customer: tagCustomer(
|
|
25369
|
+
data.customer
|
|
25370
|
+
),
|
|
24955
25371
|
recent_logs: logs || []
|
|
24956
25372
|
});
|
|
24957
25373
|
}
|
|
@@ -24971,7 +25387,7 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
24971
25387
|
description: external_exports.string().describe("Line item description"),
|
|
24972
25388
|
quantity: external_exports.number().positive().describe("Quantity"),
|
|
24973
25389
|
unit: external_exports.string().default("kpl").describe('Unit (default "kpl")'),
|
|
24974
|
-
unitPrice: external_exports.number().describe("Unit price (
|
|
25390
|
+
unitPrice: external_exports.number().finite().min(-1e9).max(1e9).describe("Unit price in EUR (excl. VAT). Can be negative for credit lines."),
|
|
24975
25391
|
vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).describe("VAT rate")
|
|
24976
25392
|
})).min(1).describe("Invoice line items"),
|
|
24977
25393
|
notes: external_exports.string().optional().describe("Invoice notes"),
|
|
@@ -25017,7 +25433,7 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
25017
25433
|
is_active: true,
|
|
25018
25434
|
auto_send: params.autoSend,
|
|
25019
25435
|
template_data: templateData
|
|
25020
|
-
}).select().single();
|
|
25436
|
+
}).select("id, name, frequency, next_date, end_date, is_active, auto_send, created_at, updated_at").single();
|
|
25021
25437
|
if (error2) {
|
|
25022
25438
|
return toErrorContent(sanitizeDbError("Failed to create recurring invoice", error2));
|
|
25023
25439
|
}
|
|
@@ -25045,7 +25461,7 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
25045
25461
|
description: external_exports.string().describe("Line item description"),
|
|
25046
25462
|
quantity: external_exports.number().positive().describe("Quantity"),
|
|
25047
25463
|
unit: external_exports.string().default("kpl").describe("Unit"),
|
|
25048
|
-
unitPrice: external_exports.number().describe("Unit price (
|
|
25464
|
+
unitPrice: external_exports.number().finite().min(-1e9).max(1e9).describe("Unit price in EUR (excl. VAT). Can be negative for credit lines."),
|
|
25049
25465
|
vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).describe("VAT rate")
|
|
25050
25466
|
})).min(1).optional().describe("Replace all invoice lines"),
|
|
25051
25467
|
notes: external_exports.string().optional().nullable().describe("Invoice notes (null to remove)"),
|
|
@@ -25057,12 +25473,19 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
25057
25473
|
async (params) => {
|
|
25058
25474
|
const supabase2 = getSupabase();
|
|
25059
25475
|
const companyId2 = getCompanyId();
|
|
25060
|
-
const { data: existing, error: fetchError } = await supabase2.from("recurring_invoices").select("id, name, template_data").eq("id", params.recurringInvoiceId).eq("company_id", companyId2).single();
|
|
25476
|
+
const { data: existing, error: fetchError } = await supabase2.from("recurring_invoices").select("id, name, template_data, next_date, end_date").eq("id", params.recurringInvoiceId).eq("company_id", companyId2).single();
|
|
25061
25477
|
if (fetchError || !existing) {
|
|
25062
25478
|
return toErrorContent(
|
|
25063
25479
|
`Recurring invoice ${params.recurringInvoiceId} not found. Use kirjapro_list_recurring_invoices to find IDs.`
|
|
25064
25480
|
);
|
|
25065
25481
|
}
|
|
25482
|
+
const effectiveNext = params.nextDate !== void 0 ? params.nextDate : existing.next_date;
|
|
25483
|
+
const effectiveEnd = params.endDate !== void 0 ? params.endDate : existing.end_date;
|
|
25484
|
+
if (effectiveEnd && effectiveNext && effectiveEnd < effectiveNext) {
|
|
25485
|
+
return toErrorContent(
|
|
25486
|
+
`endDate (${effectiveEnd}) must not be earlier than nextDate (${effectiveNext}). Either clear endDate (set to null) or advance nextDate first.`
|
|
25487
|
+
);
|
|
25488
|
+
}
|
|
25066
25489
|
const updates = {};
|
|
25067
25490
|
if (params.name !== void 0) updates.name = params.name;
|
|
25068
25491
|
if (params.frequency !== void 0) updates.frequency = params.frequency;
|
|
@@ -25096,7 +25519,7 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
25096
25519
|
"kirjapro_update_recurring_invoice",
|
|
25097
25520
|
params.idempotencyKey,
|
|
25098
25521
|
async () => {
|
|
25099
|
-
const { data, error: error2 } = await supabase2.from("recurring_invoices").update(updates).eq("id", params.recurringInvoiceId).eq("company_id", companyId2).select().single();
|
|
25522
|
+
const { data, error: error2 } = await supabase2.from("recurring_invoices").update(updates).eq("id", params.recurringInvoiceId).eq("company_id", companyId2).select("id, name, frequency, next_date, end_date, is_active, auto_send, created_at, updated_at").single();
|
|
25100
25523
|
if (error2) {
|
|
25101
25524
|
return toErrorContent(sanitizeDbError("Failed to update recurring invoice", error2));
|
|
25102
25525
|
}
|
|
@@ -25193,7 +25616,8 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
25193
25616
|
}
|
|
25194
25617
|
return toTextContent({
|
|
25195
25618
|
message: `Invoice generated from "${existing.name}"`,
|
|
25196
|
-
|
|
25619
|
+
invoice_id: result.data.invoice_id,
|
|
25620
|
+
invoice_number: result.data.invoice_number
|
|
25197
25621
|
});
|
|
25198
25622
|
}
|
|
25199
25623
|
);
|
|
@@ -25202,6 +25626,23 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
25202
25626
|
}
|
|
25203
25627
|
|
|
25204
25628
|
// src/tools/received-invoices.ts
|
|
25629
|
+
var RECEIVED_INVOICE_DETAIL_OUTPUT_SHAPE = {
|
|
25630
|
+
id: external_exports.string(),
|
|
25631
|
+
status: external_exports.string(),
|
|
25632
|
+
sender_name: external_exports.string().nullable(),
|
|
25633
|
+
sender_business_id: external_exports.string().nullable(),
|
|
25634
|
+
invoice_number: external_exports.string().nullable(),
|
|
25635
|
+
invoice_date: external_exports.string().nullable(),
|
|
25636
|
+
due_date: external_exports.string().nullable(),
|
|
25637
|
+
total_amount: external_exports.number().nullable(),
|
|
25638
|
+
total_amount_formatted: external_exports.string().nullable(),
|
|
25639
|
+
vat_amount: external_exports.number().nullable(),
|
|
25640
|
+
vat_amount_formatted: external_exports.string().nullable(),
|
|
25641
|
+
currency: external_exports.string().nullable(),
|
|
25642
|
+
transaction_id: external_exports.string().nullable(),
|
|
25643
|
+
pdf_storage_path: external_exports.string().nullable(),
|
|
25644
|
+
linked_transaction: external_exports.record(external_exports.unknown()).nullable()
|
|
25645
|
+
};
|
|
25205
25646
|
function registerReceivedInvoiceTools(server2) {
|
|
25206
25647
|
server2.registerTool(
|
|
25207
25648
|
"kirjapro_list_received_invoices",
|
|
@@ -25215,6 +25656,7 @@ function registerReceivedInvoiceTools(server2) {
|
|
|
25215
25656
|
dateFrom: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Invoice date from (YYYY-MM-DD)"),
|
|
25216
25657
|
dateTo: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Invoice date to (YYYY-MM-DD)")
|
|
25217
25658
|
},
|
|
25659
|
+
outputSchema: PAGINATED_OUTPUT_SHAPE,
|
|
25218
25660
|
annotations: READ_ONLY_ANNOTATIONS
|
|
25219
25661
|
},
|
|
25220
25662
|
async (params) => {
|
|
@@ -25251,9 +25693,10 @@ function registerReceivedInvoiceTools(server2) {
|
|
|
25251
25693
|
sender_business_id: tagUntrusted(inv.sender_business_id),
|
|
25252
25694
|
invoice_number: tagUntrusted(inv.invoice_number),
|
|
25253
25695
|
payment_reference: tagUntrusted(inv.payment_reference),
|
|
25696
|
+
iban: tagUntrusted(inv.iban),
|
|
25254
25697
|
total_amount_formatted: inv.total_amount != null ? formatCurrency(inv.total_amount) : null
|
|
25255
25698
|
}));
|
|
25256
|
-
return
|
|
25699
|
+
return toResult(paginate(results, count || 0, offset, limit));
|
|
25257
25700
|
}
|
|
25258
25701
|
);
|
|
25259
25702
|
server2.registerTool(
|
|
@@ -25263,12 +25706,15 @@ function registerReceivedInvoiceTools(server2) {
|
|
|
25263
25706
|
inputSchema: {
|
|
25264
25707
|
receivedInvoiceId: external_exports.string().uuid().describe("Received invoice UUID")
|
|
25265
25708
|
},
|
|
25709
|
+
outputSchema: RECEIVED_INVOICE_DETAIL_OUTPUT_SHAPE,
|
|
25266
25710
|
annotations: READ_ONLY_ANNOTATIONS
|
|
25267
25711
|
},
|
|
25268
25712
|
async (params) => {
|
|
25269
25713
|
const supabase2 = getSupabase();
|
|
25270
25714
|
const companyId2 = getCompanyId();
|
|
25271
|
-
const { data, error: error2 } = await supabase2.from("received_invoices").select(
|
|
25715
|
+
const { data, error: error2 } = await supabase2.from("received_invoices").select(
|
|
25716
|
+
"id, company_id, status, operator, sender_operator, sender_eia, sender_business_id, sender_name, invoice_number, invoice_date, due_date, total_amount, vat_amount, currency, payment_reference, iban, bic, notes, pdf_storage_path, xml_content, transmission_id, transaction_id, purchase_vat_entry_id, received_at, reviewed_at, reviewed_by, created_at, updated_at"
|
|
25717
|
+
).eq("id", params.receivedInvoiceId).eq("company_id", companyId2).single();
|
|
25272
25718
|
if (error2 || !data) {
|
|
25273
25719
|
return toErrorContent(
|
|
25274
25720
|
`Received invoice ${params.receivedInvoiceId} not found. Use kirjapro_list_received_invoices to find IDs.`
|
|
@@ -25279,12 +25725,16 @@ function registerReceivedInvoiceTools(server2) {
|
|
|
25279
25725
|
const { data: tx } = await supabase2.from("transactions").select("id, transaction_date, description, document_number").eq("id", data.transaction_id).eq("company_id", companyId2).single();
|
|
25280
25726
|
linkedTransaction = tx;
|
|
25281
25727
|
}
|
|
25282
|
-
|
|
25283
|
-
|
|
25728
|
+
const { xml_content: _xml, ...rest } = data;
|
|
25729
|
+
return toResult({
|
|
25730
|
+
...rest,
|
|
25284
25731
|
sender_name: tagUntrusted(data.sender_name),
|
|
25285
25732
|
sender_business_id: tagUntrusted(data.sender_business_id),
|
|
25286
25733
|
invoice_number: tagUntrusted(data.invoice_number),
|
|
25287
25734
|
payment_reference: tagUntrusted(data.payment_reference),
|
|
25735
|
+
iban: tagUntrusted(data.iban),
|
|
25736
|
+
bic: tagUntrusted(data.bic),
|
|
25737
|
+
notes: tagUntrusted(data.notes),
|
|
25288
25738
|
total_amount_formatted: data.total_amount != null ? formatCurrency(data.total_amount) : null,
|
|
25289
25739
|
vat_amount_formatted: data.vat_amount != null ? formatCurrency(data.vat_amount) : null,
|
|
25290
25740
|
linked_transaction: linkedTransaction
|
|
@@ -25326,11 +25776,11 @@ var CHART_OF_ACCOUNTS_SUMMARY = {
|
|
|
25326
25776
|
],
|
|
25327
25777
|
note: "Use kirjapro_list_accounts to get full account list with search/filter."
|
|
25328
25778
|
};
|
|
25329
|
-
var
|
|
25330
|
-
{ rate:
|
|
25331
|
-
{ rate:
|
|
25332
|
-
{ rate:
|
|
25333
|
-
{ rate:
|
|
25779
|
+
var VAT_RATE_METADATA = [
|
|
25780
|
+
{ rate: FINNISH_RATES_2026.vat.standard, label: "Yleinen verokanta", description: "Standard rate for most goods and services (from 1.9.2024, was 24%)", basis: "AVL 84\xA7" },
|
|
25781
|
+
{ rate: FINNISH_RATES_2026.vat.reduced, label: "Alennettu verokanta", description: "Food, animal feed, restaurants, transport, accommodation, books, medicine, sports, culture, performing artists, sanitary products, broadcasting (from 1.1.2026, was 14%)", basis: "AVL 85\xA7, 85a\xA7" },
|
|
25782
|
+
{ rate: FINNISH_RATES_2026.vat.secondReduced, label: "Lehdet ja aikakauslehdet", description: "Newspapers and magazines (print and digital) only", basis: "AVL 85a\xA7" },
|
|
25783
|
+
{ rate: FINNISH_RATES_2026.vat.zero, label: "Nollaverokanta", description: "Exports (AVL 70\xA7), intra-EU B2B supplies (AVL 72a\xA7), vessels, international transport. Input VAT deductible", basis: "AVL 55-72h\xA7" },
|
|
25334
25784
|
{ rate: null, label: "Verovapaat myynnit", description: "Healthcare (AVL 34-36\xA7), education (AVL 39-40\xA7), financial services (AVL 41-42\xA7), insurance (AVL 44\xA7). NO input VAT deduction", basis: "AVL 34-44\xA7" }
|
|
25335
25785
|
];
|
|
25336
25786
|
function registerSchemaResources(server2) {
|
|
@@ -25360,7 +25810,10 @@ function registerSchemaResources(server2) {
|
|
|
25360
25810
|
contents: [{
|
|
25361
25811
|
uri: uri.href,
|
|
25362
25812
|
mimeType: "application/json",
|
|
25363
|
-
text: JSON.stringify(
|
|
25813
|
+
text: JSON.stringify({
|
|
25814
|
+
rates: VAT_RATE_METADATA,
|
|
25815
|
+
_meta: freshnessMeta(FINNISH_RATES_2026)
|
|
25816
|
+
})
|
|
25364
25817
|
}]
|
|
25365
25818
|
})
|
|
25366
25819
|
);
|
|
@@ -25378,12 +25831,23 @@ function registerCompanyResources(server2) {
|
|
|
25378
25831
|
async (uri) => {
|
|
25379
25832
|
const supabase2 = getSupabase();
|
|
25380
25833
|
const companyId2 = getCompanyId();
|
|
25381
|
-
const { data: company } = await supabase2.from("companies").select("id, name, business_id, address, postal_code, city, country, created_at").eq("id", companyId2).single();
|
|
25834
|
+
const { data: company, error: error2 } = await supabase2.from("companies").select("id, name, business_id, address, postal_code, city, country, created_at").eq("id", companyId2).single();
|
|
25835
|
+
if (error2 || !company) {
|
|
25836
|
+
return {
|
|
25837
|
+
contents: [{
|
|
25838
|
+
uri: uri.href,
|
|
25839
|
+
mimeType: "application/json",
|
|
25840
|
+
text: JSON.stringify({
|
|
25841
|
+
error: error2 ? sanitizeDbError("Failed to read company", error2) : "Company not found for current context."
|
|
25842
|
+
})
|
|
25843
|
+
}]
|
|
25844
|
+
};
|
|
25845
|
+
}
|
|
25382
25846
|
return {
|
|
25383
25847
|
contents: [{
|
|
25384
25848
|
uri: uri.href,
|
|
25385
25849
|
mimeType: "application/json",
|
|
25386
|
-
text: JSON.stringify(company
|
|
25850
|
+
text: JSON.stringify(company)
|
|
25387
25851
|
}]
|
|
25388
25852
|
};
|
|
25389
25853
|
}
|
|
@@ -25398,12 +25862,25 @@ function registerCompanyResources(server2) {
|
|
|
25398
25862
|
async (uri) => {
|
|
25399
25863
|
const supabase2 = getSupabase();
|
|
25400
25864
|
const companyId2 = getCompanyId();
|
|
25401
|
-
const { data: fiscalYears } = await supabase2.from("fiscal_years").select("id, start_date, end_date, is_closed, created_at").eq("company_id", companyId2).order("start_date", { ascending: false });
|
|
25402
|
-
|
|
25403
|
-
|
|
25404
|
-
|
|
25405
|
-
|
|
25406
|
-
|
|
25865
|
+
const { data: fiscalYears, error: error2 } = await supabase2.from("fiscal_years").select("id, start_date, end_date, is_closed, created_at").eq("company_id", companyId2).order("start_date", { ascending: false });
|
|
25866
|
+
if (error2) {
|
|
25867
|
+
return {
|
|
25868
|
+
contents: [{
|
|
25869
|
+
uri: uri.href,
|
|
25870
|
+
mimeType: "application/json",
|
|
25871
|
+
text: JSON.stringify({
|
|
25872
|
+
error: sanitizeDbError("Failed to read fiscal years", error2)
|
|
25873
|
+
})
|
|
25874
|
+
}]
|
|
25875
|
+
};
|
|
25876
|
+
}
|
|
25877
|
+
const today = helsinkiDateString();
|
|
25878
|
+
const enriched = (fiscalYears || []).map(
|
|
25879
|
+
(fy) => ({
|
|
25880
|
+
...fy,
|
|
25881
|
+
is_current: !fy.is_closed && fy.start_date <= today && fy.end_date >= today
|
|
25882
|
+
})
|
|
25883
|
+
);
|
|
25407
25884
|
return {
|
|
25408
25885
|
contents: [{
|
|
25409
25886
|
uri: uri.href,
|
|
@@ -25791,10 +26268,13 @@ function registerResources(server2) {
|
|
|
25791
26268
|
registerSecurityResources(server2);
|
|
25792
26269
|
}
|
|
25793
26270
|
|
|
26271
|
+
// src/version.ts
|
|
26272
|
+
var VERSION = "0.5.6";
|
|
26273
|
+
|
|
25794
26274
|
// src/index.ts
|
|
25795
26275
|
var server = new McpServer({
|
|
25796
26276
|
name: "kirjapro",
|
|
25797
|
-
version:
|
|
26277
|
+
version: VERSION
|
|
25798
26278
|
});
|
|
25799
26279
|
async function main() {
|
|
25800
26280
|
try {
|
|
@@ -25805,13 +26285,23 @@ async function main() {
|
|
|
25805
26285
|
}
|
|
25806
26286
|
registerTools(server);
|
|
25807
26287
|
registerResources(server);
|
|
26288
|
+
const freshness = freshnessMeta(FINNISH_RATES_2026);
|
|
26289
|
+
if (freshness.stale) {
|
|
26290
|
+
console.error(
|
|
26291
|
+
`[kirjapro-mcp] WARNING: Finnish rates dataset is STALE (valid through ${freshness.valid_through}). Running on expired VAT and social-insurance rates. Upgrade @kirjapro/mcp-server immediately.`
|
|
26292
|
+
);
|
|
26293
|
+
} else if (freshness.days_until_expiry < 30) {
|
|
26294
|
+
console.error(
|
|
26295
|
+
`[kirjapro-mcp] NOTICE: Finnish rates dataset expires in ${freshness.days_until_expiry} days (${freshness.valid_through}). Upgrade @kirjapro/mcp-server before year-end to avoid stale rates.`
|
|
26296
|
+
);
|
|
26297
|
+
}
|
|
25808
26298
|
const transport = new StdioServerTransport();
|
|
25809
26299
|
transport.onclose = () => {
|
|
25810
26300
|
stopTokenRefresh();
|
|
25811
26301
|
process.exit(0);
|
|
25812
26302
|
};
|
|
25813
26303
|
await server.connect(transport);
|
|
25814
|
-
console.error(
|
|
26304
|
+
console.error(`[kirjapro-mcp] Server v${VERSION} started on stdio`);
|
|
25815
26305
|
}
|
|
25816
26306
|
function shutdown() {
|
|
25817
26307
|
stopTokenRefresh();
|