@kirjapro/mcp-server 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +986 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  MCP server for Finnish accounting. Exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude Code, Claude Desktop, and any MCP client.
4
4
 
5
+ > **Full documentation:** [docs/mcp-server.md](../docs/mcp-server.md) — complete tool reference with parameters, usage examples, error guide, and multi-client setup.
6
+
5
7
  ## Requirements
6
8
 
7
9
  - Node.js 20+
package/dist/index.js CHANGED
@@ -20891,6 +20891,7 @@ function scheduleTokenRefresh(apiKey, expiresIn) {
20891
20891
  console.error("[kirjapro-mcp] Refreshing API key token...");
20892
20892
  const auth = await authenticateWithApiKey(apiKey);
20893
20893
  supabase = createClientFromAuth(auth);
20894
+ companyId = auth.company_id;
20894
20895
  console.error("[kirjapro-mcp] Token refreshed successfully");
20895
20896
  } catch (error2) {
20896
20897
  console.error("[kirjapro-mcp] Token refresh failed:", error2.message);
@@ -20925,7 +20926,8 @@ async function initLegacyAuth() {
20925
20926
  if (!kirjaproCompanyId) {
20926
20927
  throw new Error("KIRJAPRO_COMPANY_ID environment variable is required.");
20927
20928
  }
20928
- supabase = createClient(supabaseUrl, supabaseAccessToken, {
20929
+ const anonKey = process.env.SUPABASE_ANON_KEY || supabaseAccessToken;
20930
+ supabase = createClient(supabaseUrl, anonKey, {
20929
20931
  auth: {
20930
20932
  persistSession: false,
20931
20933
  autoRefreshToken: false
@@ -21018,7 +21020,7 @@ function toTextContent(data) {
21018
21020
  content: [
21019
21021
  {
21020
21022
  type: "text",
21021
- text: JSON.stringify(data, null, 2)
21023
+ text: JSON.stringify(data)
21022
21024
  }
21023
21025
  ]
21024
21026
  };
@@ -21035,6 +21037,23 @@ function roundCents(amount) {
21035
21037
  function escapeLike(input) {
21036
21038
  return input.replace(/[%_\\]/g, "\\$&");
21037
21039
  }
21040
+ async function resolveOpenFiscalYear(supabase2, companyId2) {
21041
+ const { data, error: error2 } = await supabase2.from("fiscal_years").select("id").eq("company_id", companyId2).eq("is_closed", false);
21042
+ if (error2) {
21043
+ return toErrorContent(`Failed to query fiscal years: ${error2.message}`);
21044
+ }
21045
+ if (!data || data.length === 0) {
21046
+ return toErrorContent(
21047
+ "No open fiscal year found. Create one or specify fiscalYearId explicitly."
21048
+ );
21049
+ }
21050
+ if (data.length > 1) {
21051
+ return toErrorContent(
21052
+ `Multiple open fiscal years found (${data.length}). Specify fiscalYearId explicitly. Read kirjapro://fiscal-years to list them.`
21053
+ );
21054
+ }
21055
+ return { id: data[0].id };
21056
+ }
21038
21057
 
21039
21058
  // src/tools/accounts.ts
21040
21059
  function registerAccountTools(server2) {
@@ -21046,13 +21065,15 @@ function registerAccountTools(server2) {
21046
21065
  offset: external_exports.number().min(0).default(0).describe("Skip first N results"),
21047
21066
  accountType: external_exports.enum(["asset", "liability", "equity", "revenue", "expense"]).optional().describe("Filter by account type"),
21048
21067
  search: external_exports.string().optional().describe("Search by account name or number"),
21049
- activeOnly: external_exports.boolean().default(true).describe("Only show active accounts")
21068
+ activeOnly: external_exports.boolean().default(true).describe("Only show active accounts"),
21069
+ includeIds: external_exports.boolean().default(false).describe("Include UUID id and parent_id fields (rarely needed)")
21050
21070
  },
21051
21071
  async (params) => {
21052
21072
  const supabase2 = getSupabase();
21053
21073
  const companyId2 = getCompanyId();
21054
21074
  const { limit, offset } = clampPagination(params.limit, params.offset);
21055
- let query = supabase2.from("accounts").select("id, account_number, name, account_type, parent_id, is_active", { count: "exact" }).eq("company_id", companyId2).order("account_number");
21075
+ const selectCols = params.includeIds ? "id, account_number, name, account_type, parent_id, is_active" : "account_number, name, account_type";
21076
+ let query = supabase2.from("accounts").select(selectCols, { count: "exact" }).eq("company_id", companyId2).order("account_number");
21056
21077
  if (params.activeOnly) {
21057
21078
  query = query.eq("is_active", true);
21058
21079
  }
@@ -21079,7 +21100,7 @@ function registerTransactionTools(server2) {
21079
21100
  "kirjapro_list_transactions",
21080
21101
  "List accounting transactions (tositteet) with their journal entries for a fiscal year. Returns transaction date, description, and debit/credit entries with account details.",
21081
21102
  {
21082
- fiscalYearId: external_exports.string().uuid().describe("Fiscal year UUID to filter transactions"),
21103
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
21083
21104
  limit: external_exports.number().min(1).max(200).default(50).describe("Max results (default 50)"),
21084
21105
  offset: external_exports.number().min(0).default(0).describe("Skip first N results")
21085
21106
  },
@@ -21087,18 +21108,32 @@ function registerTransactionTools(server2) {
21087
21108
  const supabase2 = getSupabase();
21088
21109
  const companyId2 = getCompanyId();
21089
21110
  const { limit, offset } = clampPagination(params.limit, params.offset);
21111
+ let fiscalYearId = params.fiscalYearId;
21112
+ if (!fiscalYearId) {
21113
+ const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
21114
+ if ("isError" in resolved) return resolved;
21115
+ fiscalYearId = resolved.id;
21116
+ }
21090
21117
  const { data, count, error: error2 } = await supabase2.from("transactions").select(`
21091
21118
  id, transaction_date, description, document_number, created_at,
21092
21119
  journal_entries (
21093
21120
  id, account_id, debit_amount, credit_amount,
21094
21121
  accounts ( account_number, name )
21095
21122
  )
21096
- `, { count: "exact" }).eq("company_id", companyId2).eq("fiscal_year_id", params.fiscalYearId).order("transaction_date", { ascending: false }).range(offset, offset + limit - 1);
21123
+ `, { count: "exact" }).eq("company_id", companyId2).eq("fiscal_year_id", fiscalYearId).order("transaction_date", { ascending: false }).range(offset, offset + limit - 1);
21097
21124
  if (error2) {
21098
21125
  return toErrorContent(`Failed to list transactions: ${error2.message}`);
21099
21126
  }
21100
21127
  const transactions = (data || []).map((t) => ({
21101
- ...t,
21128
+ id: t.id,
21129
+ transaction_date: t.transaction_date,
21130
+ description: t.description,
21131
+ document_number: t.document_number,
21132
+ entries: (t.journal_entries || []).map((e) => ({
21133
+ account: `${e.accounts?.account_number} ${e.accounts?.name}`,
21134
+ debit: e.debit_amount || 0,
21135
+ credit: e.credit_amount || 0
21136
+ })),
21102
21137
  total_debit: roundCents(
21103
21138
  (t.journal_entries || []).reduce((s, e) => s + (e.debit_amount || 0), 0)
21104
21139
  ),
@@ -21153,7 +21188,19 @@ function registerTransactionTools(server2) {
21153
21188
  const totalCredit = roundCents(
21154
21189
  (t.journal_entries || []).reduce((s, e) => s + (e.credit_amount || 0), 0)
21155
21190
  );
21156
- return { ...t, total_debit: totalDebit, total_credit: totalCredit };
21191
+ return {
21192
+ id: t.id,
21193
+ transaction_date: t.transaction_date,
21194
+ description: t.description,
21195
+ document_number: t.document_number,
21196
+ entries: (t.journal_entries || []).map((e) => ({
21197
+ account: `${e.accounts?.account_number} ${e.accounts?.name}`,
21198
+ debit: e.debit_amount || 0,
21199
+ credit: e.credit_amount || 0
21200
+ })),
21201
+ total_debit: totalDebit,
21202
+ total_credit: totalCredit
21203
+ };
21157
21204
  });
21158
21205
  if (params.minAmount !== void 0) {
21159
21206
  results = results.filter((t) => t.total_debit >= params.minAmount);
@@ -21172,9 +21219,16 @@ function registerTransactionTools(server2) {
21172
21219
  );
21173
21220
  server2.tool(
21174
21221
  "kirjapro_create_transaction",
21175
- 'Create a new accounting transaction (tosite) with journal entries. Entries must balance (total debits = total credits). Accepts account numbers (e.g. "4000") which are resolved to UUIDs server-side.',
21222
+ `Create a new accounting transaction (tosite) with journal entries. Entries must balance (total debits = total credits). Accepts account numbers (e.g. "4000") which are resolved to UUIDs server-side.
21223
+ Common accounts (YSE 2017):
21224
+ 2310 Kassa | 2320 Pankkitili | 2110 Myyntisaamiset | 2140 ALV-saamiset
21225
+ 4030 Ostovelat | 4050 ALV-velka | 4060 Ennakonpid\xE4tysvelka | 4080 TyEL-velka
21226
+ 5010 Myynti 25,5% | 5020 Myynti 13,5% | 5030 Myynti 10% | 5040 Myynti 0%
21227
+ 6010 Ostot | 6030 Ulkopuoliset palvelut | 7010 Palkat | 7020 El\xE4kekulut
21228
+ 8010 Toimitilakulut | 8050 Markkinointikulut | 8060 Hallintokulut
21229
+ Prefer kirjapro_create_from_template for standard patterns.`,
21176
21230
  {
21177
- fiscalYearId: external_exports.string().uuid().describe("Fiscal year UUID"),
21231
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
21178
21232
  transactionDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Transaction date (YYYY-MM-DD)"),
21179
21233
  description: external_exports.string().min(1).max(500).describe("Transaction description"),
21180
21234
  documentNumber: external_exports.string().optional().describe("Document/receipt number"),
@@ -21187,10 +21241,16 @@ function registerTransactionTools(server2) {
21187
21241
  async (params) => {
21188
21242
  const supabase2 = getSupabase();
21189
21243
  const companyId2 = getCompanyId();
21190
- const { data: fy, error: fyError } = await supabase2.from("fiscal_years").select("id, is_closed, start_date, end_date").eq("id", params.fiscalYearId).eq("company_id", companyId2).single();
21244
+ let fiscalYearId = params.fiscalYearId;
21245
+ if (!fiscalYearId) {
21246
+ const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
21247
+ if ("isError" in resolved) return resolved;
21248
+ fiscalYearId = resolved.id;
21249
+ }
21250
+ 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();
21191
21251
  if (fyError || !fy) {
21192
21252
  return toErrorContent(
21193
- `Fiscal year ${params.fiscalYearId} not found. Use kirjapro_get_dashboard with a valid fiscal year ID, or read the kirjapro://fiscal-years resource to list available fiscal years.`
21253
+ `Fiscal year ${fiscalYearId} not found. Use kirjapro_get_dashboard with a valid fiscal year ID, or read the kirjapro://fiscal-years resource to list available fiscal years.`
21194
21254
  );
21195
21255
  }
21196
21256
  if (fy.is_closed) {
@@ -21224,7 +21284,7 @@ function registerTransactionTools(server2) {
21224
21284
  }
21225
21285
  const { data: transaction, error: txError } = await supabase2.from("transactions").insert({
21226
21286
  company_id: companyId2,
21227
- fiscal_year_id: params.fiscalYearId,
21287
+ fiscal_year_id: fiscalYearId,
21228
21288
  transaction_date: params.transactionDate,
21229
21289
  description: params.description,
21230
21290
  document_number: params.documentNumber || null
@@ -21257,9 +21317,16 @@ function registerTransactionTools(server2) {
21257
21317
  );
21258
21318
  server2.tool(
21259
21319
  "kirjapro_create_transactions_batch",
21260
- "Create multiple accounting transactions in one call (up to 50). Ideal for bulk import from bank statements or receipts. Each transaction is validated independently \u2014 valid ones are created even if others fail.",
21320
+ `Create multiple accounting transactions in one call (up to 50). Ideal for bulk import from bank statements or receipts. Each transaction is validated independently \u2014 valid ones are created even if others fail.
21321
+ Common accounts (YSE 2017):
21322
+ 2310 Kassa | 2320 Pankkitili | 2110 Myyntisaamiset | 2140 ALV-saamiset
21323
+ 4030 Ostovelat | 4050 ALV-velka | 4060 Ennakonpid\xE4tysvelka | 4080 TyEL-velka
21324
+ 5010 Myynti 25,5% | 5020 Myynti 13,5% | 5030 Myynti 10% | 5040 Myynti 0%
21325
+ 6010 Ostot | 6030 Ulkopuoliset palvelut | 7010 Palkat | 7020 El\xE4kekulut
21326
+ 8010 Toimitilakulut | 8050 Markkinointikulut | 8060 Hallintokulut
21327
+ Prefer kirjapro_create_from_template_batch for standard patterns.`,
21261
21328
  {
21262
- fiscalYearId: external_exports.string().uuid().describe("Fiscal year UUID"),
21329
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
21263
21330
  transactions: external_exports.array(external_exports.object({
21264
21331
  transactionDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Date (YYYY-MM-DD)"),
21265
21332
  description: external_exports.string().min(1).max(500),
@@ -21274,9 +21341,15 @@ function registerTransactionTools(server2) {
21274
21341
  async (params) => {
21275
21342
  const supabase2 = getSupabase();
21276
21343
  const companyId2 = getCompanyId();
21277
- const { data: fy, error: fyError } = await supabase2.from("fiscal_years").select("id, is_closed, start_date, end_date").eq("id", params.fiscalYearId).eq("company_id", companyId2).single();
21344
+ let fiscalYearId = params.fiscalYearId;
21345
+ if (!fiscalYearId) {
21346
+ const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
21347
+ if ("isError" in resolved) return resolved;
21348
+ fiscalYearId = resolved.id;
21349
+ }
21350
+ 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();
21278
21351
  if (fyError || !fy) {
21279
- return toErrorContent(`Fiscal year ${params.fiscalYearId} not found.`);
21352
+ return toErrorContent(`Fiscal year ${fiscalYearId} not found.`);
21280
21353
  }
21281
21354
  if (fy.is_closed) {
21282
21355
  return toErrorContent(`Fiscal year is closed (${fy.start_date} to ${fy.end_date}).`);
@@ -21307,7 +21380,7 @@ function registerTransactionTools(server2) {
21307
21380
  }
21308
21381
  const { data: newTx, error: txErr } = await supabase2.from("transactions").insert({
21309
21382
  company_id: companyId2,
21310
- fiscal_year_id: params.fiscalYearId,
21383
+ fiscal_year_id: fiscalYearId,
21311
21384
  transaction_date: tx.transactionDate,
21312
21385
  description: tx.description,
21313
21386
  document_number: tx.documentNumber || null
@@ -21376,6 +21449,14 @@ function registerTransactionTools(server2) {
21376
21449
  }
21377
21450
 
21378
21451
  // src/types.ts
21452
+ var VALID_OFFER_TRANSITIONS = {
21453
+ draft: ["sent"],
21454
+ sent: ["viewed", "accepted", "rejected", "expired", "draft"],
21455
+ viewed: ["accepted", "rejected", "expired", "draft"],
21456
+ accepted: ["draft"],
21457
+ rejected: ["draft"],
21458
+ expired: ["draft"]
21459
+ };
21379
21460
  var VALID_TRANSITIONS = {
21380
21461
  draft: ["sent", "cancelled"],
21381
21462
  sent: ["paid", "partial", "overdue", "cancelled", "draft"],
@@ -21410,7 +21491,7 @@ function registerInvoiceTools(server2) {
21410
21491
  "kirjapro_list_invoices",
21411
21492
  "List invoices with customer names. Filter by status (draft, sent, paid, overdue, cancelled).",
21412
21493
  {
21413
- status: external_exports.enum(["draft", "sent", "paid", "overdue", "cancelled"]).optional().describe("Filter by invoice status"),
21494
+ status: external_exports.enum(["draft", "sent", "paid", "partial", "overdue", "cancelled"]).optional().describe("Filter by invoice status"),
21414
21495
  limit: external_exports.number().min(1).max(200).default(50).describe("Max results (default 50)"),
21415
21496
  offset: external_exports.number().min(0).default(0).describe("Skip first N results")
21416
21497
  },
@@ -21571,10 +21652,13 @@ function registerInvoiceTools(server2) {
21571
21652
  const newPaidTotal = roundCents(paidSoFar + params.amount);
21572
21653
  const newStatus = newPaidTotal >= invoice.total ? "paid" : invoice.status;
21573
21654
  if (newStatus !== invoice.status) {
21574
- await supabase2.from("invoices").update({
21655
+ const { error: statusError } = await supabase2.from("invoices").update({
21575
21656
  status: newStatus,
21576
21657
  paid_date: newStatus === "paid" ? params.paymentDate : null
21577
21658
  }).eq("id", params.invoiceId);
21659
+ if (statusError) {
21660
+ return toErrorContent(`Payment recorded but invoice status update failed: ${statusError.message}`);
21661
+ }
21578
21662
  }
21579
21663
  return toTextContent({
21580
21664
  message: `Payment of ${params.amount.toFixed(2)} EUR recorded on invoice ${invoice.invoice_number}`,
@@ -21825,12 +21909,15 @@ function registerCustomerTools(server2) {
21825
21909
  `Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`
21826
21910
  );
21827
21911
  }
21828
- const { data: invoices } = await supabase2.from("invoices").select("total, status").eq("company_id", companyId2).eq("customer_id", params.customerId);
21912
+ const { data: invoices } = await supabase2.from("invoices").select("total, status, invoice_payments(amount)").eq("company_id", companyId2).eq("customer_id", params.customerId);
21829
21913
  const invoiceList = invoices || [];
21830
21914
  const invoiceCount = invoiceList.length;
21831
21915
  const totalRevenue = roundCents(invoiceList.reduce((s, i) => s + (i.total || 0), 0));
21832
21916
  const outstandingBalance = roundCents(
21833
- invoiceList.filter((i) => i.status === "sent" || i.status === "partial" || i.status === "overdue").reduce((s, i) => s + (i.total || 0), 0)
21917
+ invoiceList.filter((i) => i.status === "sent" || i.status === "partial" || i.status === "overdue").reduce((s, i) => {
21918
+ const paid = (i.invoice_payments || []).reduce((ps, p) => ps + (p.amount || 0), 0);
21919
+ return s + (i.total || 0) - paid;
21920
+ }, 0)
21834
21921
  );
21835
21922
  return toTextContent({
21836
21923
  ...customer,
@@ -21912,6 +21999,12 @@ function registerReportTools(server2) {
21912
21999
  async (params) => {
21913
22000
  const supabase2 = getSupabase();
21914
22001
  const companyId2 = getCompanyId();
22002
+ const { data: fiscalYear, error: fyError } = await supabase2.from("fiscal_years").select("start_date, end_date").eq("id", params.fiscalYearId).eq("company_id", companyId2).single();
22003
+ if (fyError || !fiscalYear) {
22004
+ return toErrorContent(
22005
+ `Fiscal year ${params.fiscalYearId} not found. Use kirjapro_list_fiscal_years to find available fiscal years.`
22006
+ );
22007
+ }
21915
22008
  const [entriesResult, txCountResult, invoiceResult, bankResult] = await Promise.all([
21916
22009
  // Revenue & expense totals from journal entries
21917
22010
  supabase2.from("journal_entries").select(`
@@ -21921,8 +22014,8 @@ function registerReportTools(server2) {
21921
22014
  `).eq("transaction.fiscal_year_id", params.fiscalYearId),
21922
22015
  // Transaction count
21923
22016
  supabase2.from("transactions").select("*", { count: "exact", head: true }).eq("company_id", companyId2).eq("fiscal_year_id", params.fiscalYearId),
21924
- // Invoice stats
21925
- supabase2.from("invoices").select("id, status, total, due_date").eq("company_id", companyId2),
22017
+ // Invoice stats scoped to fiscal year date range
22018
+ supabase2.from("invoices").select("id, status, total, due_date").eq("company_id", companyId2).gte("invoice_date", fiscalYear.start_date).lte("invoice_date", fiscalYear.end_date),
21926
22019
  // Bank balances
21927
22020
  supabase2.from("bank_accounts").select("account_name, iban, current_balance").eq("company_id", companyId2).eq("is_active", true)
21928
22021
  ]);
@@ -22305,7 +22398,7 @@ function registerBankingTools(server2) {
22305
22398
  if (payError) {
22306
22399
  return toErrorContent(`Failed to create payment: ${payError.message}`);
22307
22400
  }
22308
- const { data: match, error: _matchError } = await supabase2.from("reconciliation_matches").insert({
22401
+ const { data: match, error: matchError } = await supabase2.from("reconciliation_matches").insert({
22309
22402
  company_id: companyId2,
22310
22403
  bank_transaction_id: params.bankTransactionId,
22311
22404
  match_type: "invoice",
@@ -22315,20 +22408,29 @@ function registerBankingTools(server2) {
22315
22408
  status: "confirmed",
22316
22409
  confirmed_at: (/* @__PURE__ */ new Date()).toISOString()
22317
22410
  }).select("id").single();
22318
- await supabase2.from("bank_transactions").update({
22411
+ if (matchError) {
22412
+ return toErrorContent(`Payment recorded but reconciliation match failed: ${matchError.message}`);
22413
+ }
22414
+ const { error: btUpdateError } = await supabase2.from("bank_transactions").update({
22319
22415
  reconciled_at: (/* @__PURE__ */ new Date()).toISOString(),
22320
22416
  matched_invoice_id: params.invoiceId,
22321
22417
  match_confidence: 1,
22322
22418
  match_method: "mcp_manual",
22323
22419
  reconciliation_match_id: match?.id || null
22324
22420
  }).eq("id", params.bankTransactionId);
22421
+ if (btUpdateError) {
22422
+ return toErrorContent(`Payment and match created but bank transaction update failed: ${btUpdateError.message}`);
22423
+ }
22325
22424
  const newPaidTotal = roundCents(paidSoFar + paymentAmount);
22326
22425
  const newStatus = newPaidTotal >= invoice.total ? "paid" : invoice.status;
22327
22426
  if (newStatus !== invoice.status) {
22328
- await supabase2.from("invoices").update({
22427
+ const { error: statusError } = await supabase2.from("invoices").update({
22329
22428
  status: newStatus,
22330
22429
  paid_date: newStatus === "paid" ? bankTx.booking_date : null
22331
22430
  }).eq("id", params.invoiceId);
22431
+ if (statusError) {
22432
+ return toErrorContent(`Bank transaction matched but invoice status update failed: ${statusError.message}`);
22433
+ }
22332
22434
  }
22333
22435
  return toTextContent({
22334
22436
  message: `Bank transaction matched to invoice ${invoice.invoice_number}`,
@@ -22534,16 +22636,864 @@ function registerFiscalYearTools(server2) {
22534
22636
  );
22535
22637
  }
22536
22638
 
22639
+ // src/tools/templates.ts
22640
+ function splitVat(gross, vatRate) {
22641
+ if (vatRate === 0) return { net: roundCents(gross), vat: 0 };
22642
+ const net = roundCents(gross / (1 + vatRate / 100));
22643
+ const vat = roundCents(gross - net);
22644
+ return { net, vat };
22645
+ }
22646
+ function applyOverrides(entries, overrides) {
22647
+ if (!overrides) return entries;
22648
+ return entries.map((e) => ({
22649
+ ...e,
22650
+ accountNumber: overrides[e.accountNumber] ?? e.accountNumber
22651
+ }));
22652
+ }
22653
+ function saleCashTemplate(revenueAccount, vatRate) {
22654
+ return (amount, opts) => {
22655
+ const { net, vat } = splitVat(amount, vatRate);
22656
+ const entries = [
22657
+ { accountNumber: "2310", debitAmount: roundCents(amount), creditAmount: 0 },
22658
+ { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
22659
+ ];
22660
+ if (vat > 0) {
22661
+ entries.push({ accountNumber: "4050", debitAmount: 0, creditAmount: vat });
22662
+ }
22663
+ return applyOverrides(entries, opts?.accountOverrides);
22664
+ };
22665
+ }
22666
+ function saleBankTemplate(revenueAccount, vatRate) {
22667
+ return (amount, opts) => {
22668
+ const { net, vat } = splitVat(amount, vatRate);
22669
+ const entries = [
22670
+ { accountNumber: "2320", debitAmount: roundCents(amount), creditAmount: 0 },
22671
+ { accountNumber: revenueAccount, debitAmount: 0, creditAmount: net }
22672
+ ];
22673
+ if (vat > 0) {
22674
+ entries.push({ accountNumber: "4050", debitAmount: 0, creditAmount: vat });
22675
+ }
22676
+ return applyOverrides(entries, opts?.accountOverrides);
22677
+ };
22678
+ }
22679
+ var TEMPLATES = [
22680
+ // Cash sales
22681
+ {
22682
+ id: "cash_sale_25",
22683
+ description: "K\xE4teismyynti ALV 25,5%",
22684
+ vatRate: 25.5,
22685
+ expand: saleCashTemplate("5010", 25.5)
22686
+ },
22687
+ {
22688
+ id: "cash_sale_14",
22689
+ description: "K\xE4teismyynti ALV 13,5%",
22690
+ vatRate: 13.5,
22691
+ expand: saleCashTemplate("5020", 13.5)
22692
+ },
22693
+ {
22694
+ id: "cash_sale_10",
22695
+ description: "K\xE4teismyynti ALV 10%",
22696
+ vatRate: 10,
22697
+ expand: saleCashTemplate("5030", 10)
22698
+ },
22699
+ {
22700
+ id: "cash_sale_0",
22701
+ description: "K\xE4teismyynti ALV 0%",
22702
+ vatRate: 0,
22703
+ expand: saleCashTemplate("5040", 0)
22704
+ },
22705
+ // Bank sales
22706
+ {
22707
+ id: "bank_sale_25",
22708
+ description: "Pankkimyynti ALV 25,5%",
22709
+ vatRate: 25.5,
22710
+ expand: saleBankTemplate("5010", 25.5)
22711
+ },
22712
+ {
22713
+ id: "bank_sale_14",
22714
+ description: "Pankkimyynti ALV 13,5%",
22715
+ vatRate: 13.5,
22716
+ expand: saleBankTemplate("5020", 13.5)
22717
+ },
22718
+ {
22719
+ id: "bank_sale_10",
22720
+ description: "Pankkimyynti ALV 10%",
22721
+ vatRate: 10,
22722
+ expand: saleBankTemplate("5030", 10)
22723
+ },
22724
+ // Invoice sale
22725
+ {
22726
+ id: "invoice_sale_25",
22727
+ description: "Laskumyynti ALV 25,5% (myyntisaamiset)",
22728
+ vatRate: 25.5,
22729
+ expand: (amount, opts) => {
22730
+ const { net, vat } = splitVat(amount, 25.5);
22731
+ return applyOverrides([
22732
+ { accountNumber: "2110", debitAmount: roundCents(amount), creditAmount: 0 },
22733
+ { accountNumber: "5010", debitAmount: 0, creditAmount: net },
22734
+ { accountNumber: "4050", debitAmount: 0, creditAmount: vat }
22735
+ ], opts?.accountOverrides);
22736
+ }
22737
+ },
22738
+ // Purchase expenses
22739
+ {
22740
+ id: "purchase_expense",
22741
+ description: "Osto ALV 25,5% (pankkitililt\xE4)",
22742
+ vatRate: 25.5,
22743
+ expand: (amount, opts) => {
22744
+ const { net, vat } = splitVat(amount, 25.5);
22745
+ return applyOverrides([
22746
+ { accountNumber: "6010", debitAmount: net, creditAmount: 0 },
22747
+ { accountNumber: "2140", debitAmount: vat, creditAmount: 0 },
22748
+ { accountNumber: "2320", debitAmount: 0, creditAmount: roundCents(amount) }
22749
+ ], opts?.accountOverrides);
22750
+ }
22751
+ },
22752
+ {
22753
+ id: "service_expense",
22754
+ description: "Ulkopuolinen palvelu ALV 25,5%",
22755
+ vatRate: 25.5,
22756
+ expand: (amount, opts) => {
22757
+ const { net, vat } = splitVat(amount, 25.5);
22758
+ return applyOverrides([
22759
+ { accountNumber: "6030", debitAmount: net, creditAmount: 0 },
22760
+ { accountNumber: "2140", debitAmount: vat, creditAmount: 0 },
22761
+ { accountNumber: "2320", debitAmount: 0, creditAmount: roundCents(amount) }
22762
+ ], opts?.accountOverrides);
22763
+ }
22764
+ },
22765
+ // Rent (no VAT)
22766
+ {
22767
+ id: "rent_payment",
22768
+ description: "Vuokranmaksu (pankkitililt\xE4)",
22769
+ vatRate: null,
22770
+ expand: (amount, opts) => applyOverrides([
22771
+ { accountNumber: "8010", debitAmount: roundCents(amount), creditAmount: 0 },
22772
+ { accountNumber: "2320", debitAmount: 0, creditAmount: roundCents(amount) }
22773
+ ], opts?.accountOverrides)
22774
+ },
22775
+ // Salary
22776
+ {
22777
+ id: "salary_payment",
22778
+ description: "Palkanmaksu (bruttopalkka, ennakonpid\xE4tys, TyEL)",
22779
+ vatRate: null,
22780
+ expand: (amount, opts) => {
22781
+ const gross = roundCents(amount);
22782
+ const withholdingRate = opts?.withholdingRate ?? 20;
22783
+ const tyelEmployeeRate = opts?.tyelRate ?? 7.3;
22784
+ const tyelEmployerRate = 17.55;
22785
+ const withholding = roundCents(gross * withholdingRate / 100);
22786
+ const tyelEmployee = roundCents(gross * tyelEmployeeRate / 100);
22787
+ const tyelEmployer = roundCents(gross * tyelEmployerRate / 100);
22788
+ const netPay = roundCents(gross - withholding - tyelEmployee);
22789
+ return applyOverrides([
22790
+ { accountNumber: "7010", debitAmount: gross, creditAmount: 0 },
22791
+ { accountNumber: "7020", debitAmount: tyelEmployer, creditAmount: 0 },
22792
+ { accountNumber: "2320", debitAmount: 0, creditAmount: netPay },
22793
+ { accountNumber: "4060", debitAmount: 0, creditAmount: withholding },
22794
+ { accountNumber: "4080", debitAmount: 0, creditAmount: roundCents(tyelEmployee + tyelEmployer) }
22795
+ ], opts?.accountOverrides);
22796
+ }
22797
+ },
22798
+ // Bank transfer
22799
+ {
22800
+ id: "bank_transfer",
22801
+ description: "Tilisiirto kassasta pankkitilille",
22802
+ vatRate: null,
22803
+ expand: (amount, opts) => applyOverrides([
22804
+ { accountNumber: "2320", debitAmount: roundCents(amount), creditAmount: 0 },
22805
+ { accountNumber: "2310", debitAmount: 0, creditAmount: roundCents(amount) }
22806
+ ], opts?.accountOverrides)
22807
+ }
22808
+ ];
22809
+ var TEMPLATE_MAP = new Map(TEMPLATES.map((t) => [t.id, t]));
22810
+ function expandTemplate(templateId, amount, opts) {
22811
+ const template = TEMPLATE_MAP.get(templateId);
22812
+ if (!template) {
22813
+ return { error: `Unknown template "${templateId}". Use kirjapro_list_templates to see available templates.` };
22814
+ }
22815
+ if (amount <= 0) {
22816
+ return { error: "Amount must be positive." };
22817
+ }
22818
+ const entries = template.expand(amount, opts);
22819
+ return { entries };
22820
+ }
22821
+ async function resolveAndCreate(params) {
22822
+ const supabase2 = getSupabase();
22823
+ const companyId2 = getCompanyId();
22824
+ let fiscalYearId = params.fiscalYearId;
22825
+ if (!fiscalYearId) {
22826
+ const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
22827
+ if ("isError" in resolved) return resolved;
22828
+ fiscalYearId = resolved.id;
22829
+ }
22830
+ 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();
22831
+ if (fyError || !fy) {
22832
+ return toErrorContent(
22833
+ `Fiscal year ${fiscalYearId} not found. Use kirjapro_get_dashboard or read kirjapro://fiscal-years resource.`
22834
+ );
22835
+ }
22836
+ if (fy.is_closed) {
22837
+ return toErrorContent(`Fiscal year is closed (${fy.start_date} to ${fy.end_date}).`);
22838
+ }
22839
+ if (params.transactionDate < fy.start_date || params.transactionDate > fy.end_date) {
22840
+ return toErrorContent(
22841
+ `Transaction date ${params.transactionDate} outside fiscal year (${fy.start_date} to ${fy.end_date}).`
22842
+ );
22843
+ }
22844
+ const accountNumbers = [...new Set(params.entries.map((e) => e.accountNumber))];
22845
+ const { data: accounts, error: accError } = await supabase2.from("accounts").select("id, account_number").eq("company_id", companyId2).in("account_number", accountNumbers);
22846
+ if (accError) {
22847
+ return toErrorContent(`Failed to resolve accounts: ${accError.message}`);
22848
+ }
22849
+ const accountMap = new Map((accounts || []).map((a) => [a.account_number, a.id]));
22850
+ const missing = accountNumbers.filter((n) => !accountMap.has(n));
22851
+ if (missing.length > 0) {
22852
+ return toErrorContent(`Account numbers not found: ${missing.join(", ")}. Use kirjapro_list_accounts to see available accounts.`);
22853
+ }
22854
+ const totalDebit = roundCents(params.entries.reduce((s, e) => s + e.debitAmount, 0));
22855
+ const totalCredit = roundCents(params.entries.reduce((s, e) => s + e.creditAmount, 0));
22856
+ if (Math.abs(totalDebit - totalCredit) >= 0.01) {
22857
+ return toErrorContent(
22858
+ `Template produced unbalanced entries: debits ${totalDebit.toFixed(2)} != credits ${totalCredit.toFixed(2)}.`
22859
+ );
22860
+ }
22861
+ const { data: transaction, error: txError } = await supabase2.from("transactions").insert({
22862
+ company_id: companyId2,
22863
+ fiscal_year_id: fiscalYearId,
22864
+ transaction_date: params.transactionDate,
22865
+ description: params.description,
22866
+ document_number: params.documentNumber || null
22867
+ }).select().single();
22868
+ if (txError) {
22869
+ return toErrorContent(`Failed to create transaction: ${txError.message}`);
22870
+ }
22871
+ const journalEntries = params.entries.map((e) => ({
22872
+ transaction_id: transaction.id,
22873
+ account_id: accountMap.get(e.accountNumber),
22874
+ debit_amount: e.debitAmount,
22875
+ credit_amount: e.creditAmount
22876
+ }));
22877
+ const { error: jeError } = await supabase2.from("journal_entries").insert(journalEntries);
22878
+ if (jeError) {
22879
+ return toErrorContent(`Transaction created but journal entries failed: ${jeError.message}`);
22880
+ }
22881
+ return { id: transaction.id };
22882
+ }
22883
+ var ACCOUNT_REFERENCE = `
22884
+ Common accounts (YSE 2017):
22885
+ 2310 Kassa | 2320 Pankkitili | 2110 Myyntisaamiset | 2140 ALV-saamiset
22886
+ 4030 Ostovelat | 4050 ALV-velka | 4060 Ennakonpid\xE4tysvelka | 4080 TyEL-velka
22887
+ 5010 Myynti 25,5% | 5020 Myynti 13,5% | 5030 Myynti 10% | 5040 Myynti 0%
22888
+ 6010 Ostot | 6030 Ulkopuoliset palvelut | 7010 Palkat | 7020 El\xE4kekulut
22889
+ 8010 Toimitilakulut | 8050 Markkinointikulut | 8060 Hallintokulut`;
22890
+ function registerTemplateTools(server2) {
22891
+ server2.tool(
22892
+ "kirjapro_list_templates",
22893
+ `List available transaction templates for common Finnish small business bookkeeping patterns. Templates handle double-entry balancing and VAT splitting automatically.${ACCOUNT_REFERENCE}`,
22894
+ {},
22895
+ async () => {
22896
+ const list = TEMPLATES.map((t) => ({
22897
+ id: t.id,
22898
+ description: t.description,
22899
+ vatRate: t.vatRate
22900
+ }));
22901
+ return toTextContent({ templates: list });
22902
+ }
22903
+ );
22904
+ server2.tool(
22905
+ "kirjapro_create_from_template",
22906
+ `Create an accounting transaction from a predefined template. Handles double-entry balancing and VAT splitting automatically. Use kirjapro_list_templates to see available templates. Use accountOverrides to swap default accounts (e.g. {"2320": "2330"} to use a different bank account).${ACCOUNT_REFERENCE}`,
22907
+ {
22908
+ template: external_exports.string().describe('Template ID (e.g. "cash_sale_25", "purchase_expense")'),
22909
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
22910
+ transactionDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Transaction date (YYYY-MM-DD)"),
22911
+ amount: external_exports.number().positive().describe("Gross amount in EUR"),
22912
+ description: external_exports.string().min(1).max(500).describe("Transaction description"),
22913
+ documentNumber: external_exports.string().optional().describe("Document/receipt number"),
22914
+ withholdingRate: external_exports.number().min(0).max(100).optional().describe("Withholding tax % for salary_payment (default 20)"),
22915
+ tyelRate: external_exports.number().min(0).max(100).optional().describe("TyEL employee % for salary_payment (default 7.30)"),
22916
+ accountOverrides: external_exports.record(external_exports.string(), external_exports.string()).optional().describe('Account number swaps, e.g. {"2320": "2330"}')
22917
+ },
22918
+ async (params) => {
22919
+ const result = expandTemplate(params.template, params.amount, {
22920
+ withholdingRate: params.withholdingRate,
22921
+ tyelRate: params.tyelRate,
22922
+ accountOverrides: params.accountOverrides
22923
+ });
22924
+ if ("error" in result) {
22925
+ return toErrorContent(result.error);
22926
+ }
22927
+ const createResult = await resolveAndCreate({
22928
+ fiscalYearId: params.fiscalYearId,
22929
+ transactionDate: params.transactionDate,
22930
+ description: params.description,
22931
+ documentNumber: params.documentNumber,
22932
+ entries: result.entries
22933
+ });
22934
+ if ("isError" in createResult) {
22935
+ return createResult;
22936
+ }
22937
+ return toTextContent({
22938
+ message: "Transaction created from template",
22939
+ template: params.template,
22940
+ transaction_id: createResult.id,
22941
+ entries: result.entries
22942
+ });
22943
+ }
22944
+ );
22945
+ server2.tool(
22946
+ "kirjapro_create_from_template_batch",
22947
+ `Create multiple transactions from templates in one call (up to 50). Each transaction is validated independently \u2014 valid ones are created even if others fail. Ideal for bulk recording of similar transactions (e.g. daily cash sales).${ACCOUNT_REFERENCE}`,
22948
+ {
22949
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
22950
+ transactions: external_exports.array(external_exports.object({
22951
+ template: external_exports.string().describe("Template ID"),
22952
+ transactionDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Date (YYYY-MM-DD)"),
22953
+ amount: external_exports.number().positive().describe("Gross amount in EUR"),
22954
+ description: external_exports.string().min(1).max(500),
22955
+ documentNumber: external_exports.string().optional(),
22956
+ withholdingRate: external_exports.number().min(0).max(100).optional(),
22957
+ tyelRate: external_exports.number().min(0).max(100).optional(),
22958
+ accountOverrides: external_exports.record(external_exports.string(), external_exports.string()).optional()
22959
+ })).min(1).max(50).describe("Array of template transactions to create (max 50)")
22960
+ },
22961
+ async (params) => {
22962
+ const created = [];
22963
+ const failed = [];
22964
+ for (let i = 0; i < params.transactions.length; i++) {
22965
+ const tx = params.transactions[i];
22966
+ const result = expandTemplate(tx.template, tx.amount, {
22967
+ withholdingRate: tx.withholdingRate,
22968
+ tyelRate: tx.tyelRate,
22969
+ accountOverrides: tx.accountOverrides
22970
+ });
22971
+ if ("error" in result) {
22972
+ failed.push({ index: i, error: result.error });
22973
+ continue;
22974
+ }
22975
+ const createResult = await resolveAndCreate({
22976
+ fiscalYearId: params.fiscalYearId,
22977
+ transactionDate: tx.transactionDate,
22978
+ description: tx.description,
22979
+ documentNumber: tx.documentNumber,
22980
+ entries: result.entries
22981
+ });
22982
+ if ("isError" in createResult) {
22983
+ failed.push({ index: i, error: createResult.content[0].text });
22984
+ continue;
22985
+ }
22986
+ created.push({ id: createResult.id, description: tx.description, date: tx.transactionDate });
22987
+ }
22988
+ return toTextContent({
22989
+ created_count: created.length,
22990
+ failed_count: failed.length,
22991
+ created,
22992
+ failed
22993
+ });
22994
+ }
22995
+ );
22996
+ }
22997
+
22998
+ // src/tools/bookings.ts
22999
+ var DEFAULTS = {
23000
+ tyelEmployee: 7.3,
23001
+ tyelEmployer: 17.55,
23002
+ sotu: 1.91,
23003
+ ttvEmployer: 0.31,
23004
+ ttvEmployee: 0.89
23005
+ };
23006
+ function registerBookingTools(server2) {
23007
+ server2.tool(
23008
+ "kirjapro_book_purchase",
23009
+ `Book a purchase (osto) in one call with automatic VAT splitting and Finnish compliance.
23010
+ Handles: standard purchases, reverse charge (EU/non-EU services), entertainment (edustus, AVL 114\xA7 no VAT deduction).
23011
+ Common expense accounts: 6010 Ostot | 6030 Ulkopuoliset palvelut | 8010 Toimitilakulut | 8050 Markkinointikulut | 8060 Hallintokulut | 8070 Vakuutukset | 8080 Tietoliikennekulut`,
23012
+ {
23013
+ amount: external_exports.number().positive().describe("Gross EUR amount (VAT-inclusive)"),
23014
+ description: external_exports.string().min(1).max(500).describe("Transaction description"),
23015
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Transaction date (YYYY-MM-DD)"),
23016
+ vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).describe("Finnish VAT rate"),
23017
+ expenseAccount: external_exports.string().describe('Expense account number (e.g. "8080")'),
23018
+ reverseCharge: external_exports.boolean().default(false).describe("EU reverse charge: D expense + D 2140 + C 4050 + C bank"),
23019
+ edustus: external_exports.boolean().default(false).describe("Entertainment expense (AVL 114\xA7): 0% VAT deduction, full gross to expense"),
23020
+ paymentAccount: external_exports.string().default("2320").describe('Payment account (default "2320" Pankkitili)'),
23021
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
23022
+ documentNumber: external_exports.string().optional().describe("Document/receipt number")
23023
+ },
23024
+ async (params) => {
23025
+ const rate = Number.parseFloat(params.vatRate);
23026
+ const gross = roundCents(params.amount);
23027
+ let entries;
23028
+ if (params.edustus) {
23029
+ entries = [
23030
+ { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
23031
+ { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
23032
+ ];
23033
+ } else if (params.reverseCharge) {
23034
+ const vat = roundCents(gross * rate / 100);
23035
+ entries = [
23036
+ { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
23037
+ { accountNumber: "2140", debitAmount: vat, creditAmount: 0 },
23038
+ // ALV-saamiset (input)
23039
+ { accountNumber: "4050", debitAmount: 0, creditAmount: vat },
23040
+ // ALV-velka (output)
23041
+ { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
23042
+ ];
23043
+ } else if (rate === 0) {
23044
+ entries = [
23045
+ { accountNumber: params.expenseAccount, debitAmount: gross, creditAmount: 0 },
23046
+ { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
23047
+ ];
23048
+ } else {
23049
+ const { net, vat } = splitVat(gross, rate);
23050
+ entries = [
23051
+ { accountNumber: params.expenseAccount, debitAmount: net, creditAmount: 0 },
23052
+ { accountNumber: "2140", debitAmount: vat, creditAmount: 0 },
23053
+ { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: gross }
23054
+ ];
23055
+ }
23056
+ const result = await resolveAndCreate({
23057
+ fiscalYearId: params.fiscalYearId,
23058
+ transactionDate: params.date,
23059
+ description: params.description,
23060
+ documentNumber: params.documentNumber,
23061
+ entries
23062
+ });
23063
+ if ("isError" in result) return result;
23064
+ return toTextContent({
23065
+ message: "Purchase booked",
23066
+ transaction_id: result.id,
23067
+ entries
23068
+ });
23069
+ }
23070
+ );
23071
+ server2.tool(
23072
+ "kirjapro_book_salary",
23073
+ `Book salary payment (palkanmaksu) in one call. Creates 2 balanced transactions:
23074
+ 1. Salary: D 7010(gross) \u2192 C 4060(withholding) + C 4080(TyEL employee) + C 4070(TTV employee) + C 2320(net)
23075
+ 2. Employer costs: D 7020(TyEL employer) + D 7030(sotu + TTV employer) \u2192 C 4080 + C 4070
23076
+ Default rates are 2026 Finnish social insurance rates.`,
23077
+ {
23078
+ grossSalary: external_exports.number().positive().describe("Gross salary EUR"),
23079
+ employeeName: external_exports.string().min(1).describe("Employee name (for description)"),
23080
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Payment date (YYYY-MM-DD)"),
23081
+ withholdingRate: external_exports.number().min(0).max(100).describe("Withholding tax % (ennakonpid\xE4tys)"),
23082
+ tyelEmployeeRate: external_exports.number().min(0).max(100).default(DEFAULTS.tyelEmployee).describe("TyEL employee % (default 7.30)"),
23083
+ tyelEmployerRate: external_exports.number().min(0).max(100).default(DEFAULTS.tyelEmployer).describe("TyEL employer % (default 17.55)"),
23084
+ sotuRate: external_exports.number().min(0).max(100).default(DEFAULTS.sotu).describe("Sairausvakuutusmaksu employer % (default 1.91)"),
23085
+ ttvEmployerRate: external_exports.number().min(0).max(100).default(DEFAULTS.ttvEmployer).describe("Ty\xF6tt\xF6myysvakuutus employer % (default 0.31)"),
23086
+ ttvEmployeeRate: external_exports.number().min(0).max(100).default(DEFAULTS.ttvEmployee).describe("Ty\xF6tt\xF6myysvakuutus employee % (default 0.89)"),
23087
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
23088
+ paymentAccount: external_exports.string().default("2320").describe('Bank account (default "2320")')
23089
+ },
23090
+ async (params) => {
23091
+ const gross = roundCents(params.grossSalary);
23092
+ const withholding = roundCents(gross * params.withholdingRate / 100);
23093
+ const tyelEmployee = roundCents(gross * params.tyelEmployeeRate / 100);
23094
+ const ttvEmployee = roundCents(gross * params.ttvEmployeeRate / 100);
23095
+ const netPay = roundCents(gross - withholding - tyelEmployee - ttvEmployee);
23096
+ const tyelEmployer = roundCents(gross * params.tyelEmployerRate / 100);
23097
+ const sotuAmount = roundCents(gross * params.sotuRate / 100);
23098
+ const ttvEmployer = roundCents(gross * params.ttvEmployerRate / 100);
23099
+ if (netPay < 0) {
23100
+ return toErrorContent(
23101
+ `Net pay is negative (${netPay.toFixed(2)}). Check withholding rate (${params.withholdingRate}%) and deduction rates.`
23102
+ );
23103
+ }
23104
+ const salaryEntries = [
23105
+ { accountNumber: "7010", debitAmount: gross, creditAmount: 0 },
23106
+ { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: netPay },
23107
+ { accountNumber: "4060", debitAmount: 0, creditAmount: withholding },
23108
+ { accountNumber: "4080", debitAmount: 0, creditAmount: tyelEmployee },
23109
+ { accountNumber: "4070", debitAmount: 0, creditAmount: ttvEmployee }
23110
+ ];
23111
+ const salaryResult = await resolveAndCreate({
23112
+ fiscalYearId: params.fiscalYearId,
23113
+ transactionDate: params.date,
23114
+ description: `Palkka ${params.employeeName}`,
23115
+ entries: salaryEntries
23116
+ });
23117
+ if ("isError" in salaryResult) return salaryResult;
23118
+ const employerSotuTtv = roundCents(sotuAmount + ttvEmployer);
23119
+ const employerEntries = [
23120
+ { accountNumber: "7020", debitAmount: tyelEmployer, creditAmount: 0 },
23121
+ { accountNumber: "7030", debitAmount: employerSotuTtv, creditAmount: 0 },
23122
+ { accountNumber: "4080", debitAmount: 0, creditAmount: tyelEmployer },
23123
+ { accountNumber: "4070", debitAmount: 0, creditAmount: employerSotuTtv }
23124
+ ];
23125
+ const employerResult = await resolveAndCreate({
23126
+ fiscalYearId: params.fiscalYearId,
23127
+ transactionDate: params.date,
23128
+ description: `Sivukulut ${params.employeeName}`,
23129
+ entries: employerEntries
23130
+ });
23131
+ if ("isError" in employerResult) return employerResult;
23132
+ return toTextContent({
23133
+ message: "Salary booked (2 transactions)",
23134
+ salary_transaction_id: salaryResult.id,
23135
+ employer_costs_transaction_id: employerResult.id,
23136
+ summary: {
23137
+ gross,
23138
+ withholding,
23139
+ tyelEmployee,
23140
+ ttvEmployee,
23141
+ netPay,
23142
+ tyelEmployer,
23143
+ sotu: sotuAmount,
23144
+ ttvEmployer,
23145
+ totalEmployerCost: roundCents(gross + tyelEmployer + sotuAmount + ttvEmployer)
23146
+ }
23147
+ });
23148
+ }
23149
+ );
23150
+ server2.tool(
23151
+ "kirjapro_book_vat_settlement",
23152
+ `Book ALV-tilitys (VAT settlement) for a month. Queries journal entries on ALV accounts (2140 ALV-saamiset, 4050 ALV-velka) for the month, calculates the net, and creates a settlement transaction.
23153
+ If ALV-velka > ALV-saamiset \u2192 pay to tax authority (D 4050, C 2140, D/C bank for difference).
23154
+ If ALV-saamiset > ALV-velka \u2192 claim refund (D 4050, C 2140, D bank for difference).`,
23155
+ {
23156
+ month: external_exports.string().regex(/^\d{4}-\d{2}$/).describe("Settlement month (YYYY-MM)"),
23157
+ fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved if omitted)"),
23158
+ settlementDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Settlement date (default: 12th of month+1)"),
23159
+ paymentAccount: external_exports.string().default("2320").describe('Bank account (default "2320")')
23160
+ },
23161
+ async (params) => {
23162
+ const supabase2 = getSupabase();
23163
+ const companyId2 = getCompanyId();
23164
+ let fiscalYearId = params.fiscalYearId;
23165
+ if (!fiscalYearId) {
23166
+ const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
23167
+ if ("isError" in resolved) return resolved;
23168
+ fiscalYearId = resolved.id;
23169
+ }
23170
+ const [year, monthStr] = params.month.split("-");
23171
+ const monthStart = `${params.month}-01`;
23172
+ const monthNum = Number.parseInt(monthStr, 10);
23173
+ const nextMonth = monthNum === 12 ? `${Number.parseInt(year, 10) + 1}-01` : `${year}-${String(monthNum + 1).padStart(2, "0")}`;
23174
+ const monthEnd = /* @__PURE__ */ new Date(`${nextMonth}-01`);
23175
+ monthEnd.setDate(monthEnd.getDate() - 1);
23176
+ const monthEndStr = monthEnd.toISOString().slice(0, 10);
23177
+ const settlementDate = params.settlementDate ?? `${nextMonth}-12`;
23178
+ const { data: alvSaamiset, error: err1 } = await supabase2.from("journal_entries").select("debit_amount, credit_amount, accounts!inner(account_number), transactions!inner(transaction_date, company_id, fiscal_year_id)").eq("accounts.account_number", "2140").eq("transactions.company_id", companyId2).eq("transactions.fiscal_year_id", fiscalYearId).gte("transactions.transaction_date", monthStart).lte("transactions.transaction_date", monthEndStr);
23179
+ if (err1) {
23180
+ return toErrorContent(`Failed to query ALV-saamiset: ${err1.message}`);
23181
+ }
23182
+ const { data: alvVelka, error: err2 } = await supabase2.from("journal_entries").select("debit_amount, credit_amount, accounts!inner(account_number), transactions!inner(transaction_date, company_id, fiscal_year_id)").eq("accounts.account_number", "4050").eq("transactions.company_id", companyId2).eq("transactions.fiscal_year_id", fiscalYearId).gte("transactions.transaction_date", monthStart).lte("transactions.transaction_date", monthEndStr);
23183
+ if (err2) {
23184
+ return toErrorContent(`Failed to query ALV-velka: ${err2.message}`);
23185
+ }
23186
+ const saamisetBalance = roundCents(
23187
+ (alvSaamiset || []).reduce((s, e) => s + (e.debit_amount || 0) - (e.credit_amount || 0), 0)
23188
+ );
23189
+ const velkaBalance = roundCents(
23190
+ (alvVelka || []).reduce((s, e) => s + (e.credit_amount || 0) - (e.debit_amount || 0), 0)
23191
+ );
23192
+ if (saamisetBalance === 0 && velkaBalance === 0) {
23193
+ return toTextContent({
23194
+ message: `No VAT entries found for ${params.month}. Nothing to settle.`,
23195
+ alv_saamiset: 0,
23196
+ alv_velka: 0
23197
+ });
23198
+ }
23199
+ const netVat = roundCents(velkaBalance - saamisetBalance);
23200
+ let entries;
23201
+ if (netVat > 0) {
23202
+ entries = [
23203
+ { accountNumber: "4050", debitAmount: velkaBalance, creditAmount: 0 },
23204
+ { accountNumber: "2140", debitAmount: 0, creditAmount: saamisetBalance },
23205
+ { accountNumber: params.paymentAccount, debitAmount: 0, creditAmount: netVat }
23206
+ ];
23207
+ } else if (netVat < 0) {
23208
+ const refund = Math.abs(netVat);
23209
+ entries = [
23210
+ { accountNumber: "4050", debitAmount: velkaBalance, creditAmount: 0 },
23211
+ { accountNumber: params.paymentAccount, debitAmount: refund, creditAmount: 0 },
23212
+ { accountNumber: "2140", debitAmount: 0, creditAmount: saamisetBalance }
23213
+ ];
23214
+ } else {
23215
+ entries = [
23216
+ { accountNumber: "4050", debitAmount: velkaBalance, creditAmount: 0 },
23217
+ { accountNumber: "2140", debitAmount: 0, creditAmount: saamisetBalance }
23218
+ ];
23219
+ }
23220
+ const result = await resolveAndCreate({
23221
+ fiscalYearId,
23222
+ transactionDate: settlementDate,
23223
+ description: `ALV-tilitys ${params.month}`,
23224
+ entries
23225
+ });
23226
+ if ("isError" in result) return result;
23227
+ return toTextContent({
23228
+ message: `VAT settlement booked for ${params.month}`,
23229
+ transaction_id: result.id,
23230
+ alv_saamiset: saamisetBalance,
23231
+ alv_velka: velkaBalance,
23232
+ net_vat: netVat,
23233
+ direction: netVat > 0 ? "pay" : netVat < 0 ? "refund" : "zero"
23234
+ });
23235
+ }
23236
+ );
23237
+ }
23238
+
23239
+ // src/tools/offers.ts
23240
+ function generateOfferNumber(lastNumber, type) {
23241
+ const prefix = type === "sopimus" ? "SOP" : "TAR";
23242
+ if (!lastNumber) return `${prefix}00001`;
23243
+ const numPart = lastNumber.replace(/\D/g, "");
23244
+ const next = parseInt(numPart || "0") + 1;
23245
+ return `${prefix}${next.toString().padStart(5, "0")}`;
23246
+ }
23247
+ function registerOfferTools(server2) {
23248
+ server2.tool(
23249
+ "kirjapro_list_offers",
23250
+ "List offers/contracts with customer names. Filter by status (draft, sent, viewed, accepted, rejected, expired) and type (tarjous, sopimus).",
23251
+ {
23252
+ status: external_exports.enum(["draft", "sent", "viewed", "accepted", "rejected", "expired"]).optional().describe("Filter by offer status"),
23253
+ type: external_exports.enum(["tarjous", "sopimus"]).optional().describe("Filter by type (tarjous = offer, sopimus = contract)"),
23254
+ limit: external_exports.number().min(1).max(200).default(50).describe("Max results (default 50)"),
23255
+ offset: external_exports.number().min(0).default(0).describe("Skip first N results")
23256
+ },
23257
+ async (params) => {
23258
+ const supabase2 = getSupabase();
23259
+ const companyId2 = getCompanyId();
23260
+ const { limit, offset } = clampPagination(params.limit, params.offset);
23261
+ let query = supabase2.from("offers").select(`
23262
+ id, offer_number, type, title, status, subtotal, vat_total, total,
23263
+ valid_from, valid_until, invoice_id, created_at,
23264
+ customers ( name, business_id )
23265
+ `, { count: "exact" }).eq("company_id", companyId2).order("created_at", { ascending: false });
23266
+ if (params.status) {
23267
+ query = query.eq("status", params.status);
23268
+ }
23269
+ if (params.type) {
23270
+ query = query.eq("type", params.type);
23271
+ }
23272
+ query = query.range(offset, offset + limit - 1);
23273
+ const { data, count, error: error2 } = await query;
23274
+ if (error2) {
23275
+ return toErrorContent(`Failed to list offers: ${error2.message}`);
23276
+ }
23277
+ return toTextContent(paginate(data || [], count || 0, offset, limit));
23278
+ }
23279
+ );
23280
+ server2.tool(
23281
+ "kirjapro_get_offer",
23282
+ "Get a single offer/contract with full detail: customer info, line items, events, and VAT breakdown.",
23283
+ {
23284
+ offerId: external_exports.string().uuid().describe("Offer UUID")
23285
+ },
23286
+ async (params) => {
23287
+ const supabase2 = getSupabase();
23288
+ const companyId2 = getCompanyId();
23289
+ const { data: offer, error: error2 } = await supabase2.from("offers").select(`
23290
+ id, offer_number, type, title, content, status,
23291
+ subtotal, vat_total, total, notes, valid_from, valid_until,
23292
+ invoice_id, sent_at, created_at,
23293
+ customers ( id, name, business_id, email, address, postal_code, city ),
23294
+ offer_lines ( id, line_number, description, quantity, unit, unit_price, vat_rate, subtotal, vat_amount, total ),
23295
+ offer_events ( id, event_type, created_at, metadata )
23296
+ `).eq("id", params.offerId).eq("company_id", companyId2).single();
23297
+ if (error2 || !offer) {
23298
+ return toErrorContent(
23299
+ `Offer ${params.offerId} not found. Use kirjapro_list_offers to find offer IDs.`
23300
+ );
23301
+ }
23302
+ return toTextContent(offer);
23303
+ }
23304
+ );
23305
+ server2.tool(
23306
+ "kirjapro_create_offer",
23307
+ "Create a new offer (tarjous) or contract (sopimus) with line items. Automatically generates offer number and calculates VAT. Returns the created offer.",
23308
+ {
23309
+ type: external_exports.enum(["tarjous", "sopimus"]).default("tarjous").describe("Type: tarjous (offer) or sopimus (contract)"),
23310
+ title: external_exports.string().min(1).describe("Offer title"),
23311
+ customerId: external_exports.string().uuid().optional().describe("Customer UUID (optional)"),
23312
+ content: external_exports.string().optional().describe("Free-text content/description"),
23313
+ validFrom: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Valid from date (YYYY-MM-DD)"),
23314
+ validUntil: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Valid until date (YYYY-MM-DD)"),
23315
+ notes: external_exports.string().optional().describe("Internal notes (not visible to customer)"),
23316
+ lines: external_exports.array(external_exports.object({
23317
+ description: external_exports.string().min(1).describe("Line item description"),
23318
+ quantity: external_exports.number().min(0).describe("Quantity"),
23319
+ unit: external_exports.string().default("kpl").describe("Unit (kpl, h, kk, etc.)"),
23320
+ unitPrice: external_exports.number().describe("Unit price in EUR (excl. VAT)"),
23321
+ vatRate: external_exports.enum(["25.5", "13.5", "10", "0"]).default("25.5").describe("VAT rate")
23322
+ })).min(1).describe("Line items")
23323
+ },
23324
+ async (params) => {
23325
+ const supabase2 = getSupabase();
23326
+ const companyId2 = getCompanyId();
23327
+ if (params.customerId) {
23328
+ const { data: customer, error: custError } = await supabase2.from("customers").select("id, name").eq("id", params.customerId).eq("company_id", companyId2).single();
23329
+ if (custError || !customer) {
23330
+ return toErrorContent(
23331
+ `Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`
23332
+ );
23333
+ }
23334
+ }
23335
+ const { data: lastOffer } = await supabase2.from("offers").select("offer_number").eq("company_id", companyId2).eq("type", params.type).order("created_at", { ascending: false }).limit(1).single();
23336
+ const offerNumber = generateOfferNumber(lastOffer?.offer_number || null, params.type);
23337
+ const calculatedLines = params.lines.map((line, i) => {
23338
+ const vatRate = parseFloat(line.vatRate);
23339
+ const subtotal2 = roundCents(line.quantity * line.unitPrice);
23340
+ const vatAmount = roundCents(subtotal2 * (vatRate / 100));
23341
+ const total2 = roundCents(subtotal2 + vatAmount);
23342
+ return {
23343
+ line_number: i + 1,
23344
+ description: line.description,
23345
+ quantity: line.quantity,
23346
+ unit: line.unit,
23347
+ unit_price: line.unitPrice,
23348
+ vat_rate: vatRate,
23349
+ subtotal: subtotal2,
23350
+ vat_amount: vatAmount,
23351
+ total: total2
23352
+ };
23353
+ });
23354
+ const subtotal = roundCents(calculatedLines.reduce((s, l) => s + l.subtotal, 0));
23355
+ const vatTotal = roundCents(calculatedLines.reduce((s, l) => s + l.vat_amount, 0));
23356
+ const total = roundCents(subtotal + vatTotal);
23357
+ const { data: offerId, error: rpcError } = await supabase2.rpc("create_offer_with_lines", {
23358
+ p_company_id: companyId2,
23359
+ p_customer_id: params.customerId || null,
23360
+ p_type: params.type,
23361
+ p_title: params.title,
23362
+ p_offer_number: offerNumber,
23363
+ p_content: params.content || null,
23364
+ p_status: "draft",
23365
+ p_valid_from: params.validFrom || null,
23366
+ p_valid_until: params.validUntil || null,
23367
+ p_notes: params.notes || null,
23368
+ p_subtotal: subtotal,
23369
+ p_vat_total: vatTotal,
23370
+ p_total: total,
23371
+ p_lines: calculatedLines
23372
+ });
23373
+ if (rpcError) {
23374
+ return toErrorContent(`Failed to create offer: ${rpcError.message}`);
23375
+ }
23376
+ return toTextContent({
23377
+ message: `${params.type === "sopimus" ? "Contract" : "Offer"} ${offerNumber} created`,
23378
+ offer_id: offerId,
23379
+ offer_number: offerNumber,
23380
+ type: params.type,
23381
+ title: params.title,
23382
+ subtotal,
23383
+ vat_total: vatTotal,
23384
+ total,
23385
+ lines: calculatedLines
23386
+ });
23387
+ }
23388
+ );
23389
+ server2.tool(
23390
+ "kirjapro_update_offer_status",
23391
+ "Update offer status with state machine validation. Valid transitions: draft\u2192sent, sent\u2192viewed/accepted/rejected/expired/draft, viewed\u2192accepted/rejected/expired/draft, accepted/rejected/expired\u2192draft.",
23392
+ {
23393
+ offerId: external_exports.string().uuid().describe("Offer UUID"),
23394
+ status: external_exports.enum(["draft", "sent", "viewed", "accepted", "rejected", "expired"]).describe("New status")
23395
+ },
23396
+ async (params) => {
23397
+ const supabase2 = getSupabase();
23398
+ const companyId2 = getCompanyId();
23399
+ const { data: offer, error: fetchError } = await supabase2.from("offers").select("id, offer_number, status, invoice_id").eq("id", params.offerId).eq("company_id", companyId2).single();
23400
+ if (fetchError || !offer) {
23401
+ return toErrorContent(
23402
+ `Offer ${params.offerId} not found. Use kirjapro_list_offers to find offer IDs.`
23403
+ );
23404
+ }
23405
+ const currentStatus = offer.status;
23406
+ const newStatus = params.status;
23407
+ if (currentStatus === newStatus) {
23408
+ return toErrorContent(
23409
+ `Offer ${offer.offer_number} is already in status "${currentStatus}".`
23410
+ );
23411
+ }
23412
+ const validNext = VALID_OFFER_TRANSITIONS[currentStatus] || [];
23413
+ if (!validNext.includes(newStatus)) {
23414
+ return toErrorContent(
23415
+ `Cannot transition offer ${offer.offer_number} from "${currentStatus}" to "${newStatus}". Valid next statuses: ${validNext.length > 0 ? validNext.join(", ") : "(none)"}.`
23416
+ );
23417
+ }
23418
+ if (newStatus === "draft" && offer.invoice_id) {
23419
+ return toErrorContent(
23420
+ `Cannot revert offer ${offer.offer_number} to draft \u2014 it has already been converted to an invoice.`
23421
+ );
23422
+ }
23423
+ const updateFields = { status: newStatus };
23424
+ const timestampMap = {
23425
+ sent: "sent_at",
23426
+ viewed: "viewed_at",
23427
+ accepted: "accepted_at",
23428
+ rejected: "rejected_at"
23429
+ };
23430
+ if (timestampMap[newStatus]) {
23431
+ updateFields[timestampMap[newStatus]] = (/* @__PURE__ */ new Date()).toISOString();
23432
+ }
23433
+ const { error: updateError } = await supabase2.from("offers").update(updateFields).eq("id", params.offerId).eq("company_id", companyId2);
23434
+ if (updateError) {
23435
+ return toErrorContent(`Failed to update offer status: ${updateError.message}`);
23436
+ }
23437
+ const { error: eventError } = await supabase2.from("offer_events").insert({
23438
+ offer_id: params.offerId,
23439
+ event_type: `status_${newStatus}`
23440
+ });
23441
+ if (eventError) {
23442
+ console.error("Failed to log offer event:", eventError);
23443
+ }
23444
+ return toTextContent({
23445
+ message: `Offer ${offer.offer_number} status changed from "${currentStatus}" to "${newStatus}"`,
23446
+ offer_number: offer.offer_number,
23447
+ previous_status: currentStatus,
23448
+ new_status: newStatus
23449
+ });
23450
+ }
23451
+ );
23452
+ server2.tool(
23453
+ "kirjapro_delete_offer",
23454
+ 'Delete a draft offer/contract. Only offers with status "draft" can be deleted.',
23455
+ {
23456
+ offerId: external_exports.string().uuid().describe("Offer UUID")
23457
+ },
23458
+ async (params) => {
23459
+ const supabase2 = getSupabase();
23460
+ const companyId2 = getCompanyId();
23461
+ const { data: offer, error: fetchError } = await supabase2.from("offers").select("id, offer_number, status, type").eq("id", params.offerId).eq("company_id", companyId2).single();
23462
+ if (fetchError || !offer) {
23463
+ return toErrorContent(
23464
+ `Offer ${params.offerId} not found. Use kirjapro_list_offers to find offer IDs.`
23465
+ );
23466
+ }
23467
+ if (offer.status !== "draft") {
23468
+ return toErrorContent(
23469
+ `Cannot delete offer ${offer.offer_number} \u2014 status is "${offer.status}". Only draft offers can be deleted.`
23470
+ );
23471
+ }
23472
+ const { error: deleteError } = await supabase2.from("offers").delete().eq("id", params.offerId).eq("company_id", companyId2);
23473
+ if (deleteError) {
23474
+ return toErrorContent(`Failed to delete offer: ${deleteError.message}`);
23475
+ }
23476
+ return toTextContent({
23477
+ message: `Draft ${offer.type === "sopimus" ? "contract" : "offer"} ${offer.offer_number} deleted`,
23478
+ offer_number: offer.offer_number
23479
+ });
23480
+ }
23481
+ );
23482
+ }
23483
+
22537
23484
  // src/tools/index.ts
22538
23485
  function registerTools(server2) {
22539
23486
  registerAccountTools(server2);
22540
23487
  registerTransactionTools(server2);
23488
+ registerTemplateTools(server2);
23489
+ registerBookingTools(server2);
22541
23490
  registerInvoiceTools(server2);
22542
23491
  registerCustomerTools(server2);
22543
23492
  registerReportTools(server2);
22544
23493
  registerBankingTools(server2);
22545
23494
  registerValidationTools(server2);
22546
23495
  registerFiscalYearTools(server2);
23496
+ registerOfferTools(server2);
22547
23497
  }
22548
23498
 
22549
23499
  // src/resources/schema.ts
@@ -22649,7 +23599,7 @@ function registerSchemaResources(server2) {
22649
23599
  contents: [{
22650
23600
  uri: uri.href,
22651
23601
  mimeType: "application/json",
22652
- text: JSON.stringify(CHART_OF_ACCOUNTS, null, 2)
23602
+ text: JSON.stringify(CHART_OF_ACCOUNTS)
22653
23603
  }]
22654
23604
  })
22655
23605
  );
@@ -22664,7 +23614,7 @@ function registerSchemaResources(server2) {
22664
23614
  contents: [{
22665
23615
  uri: uri.href,
22666
23616
  mimeType: "application/json",
22667
- text: JSON.stringify(VAT_RATES, null, 2)
23617
+ text: JSON.stringify(VAT_RATES)
22668
23618
  }]
22669
23619
  })
22670
23620
  );
@@ -22687,7 +23637,7 @@ function registerCompanyResources(server2) {
22687
23637
  contents: [{
22688
23638
  uri: uri.href,
22689
23639
  mimeType: "application/json",
22690
- text: JSON.stringify(company || {}, null, 2)
23640
+ text: JSON.stringify(company || {})
22691
23641
  }]
22692
23642
  };
22693
23643
  }
@@ -22703,11 +23653,16 @@ function registerCompanyResources(server2) {
22703
23653
  const supabase2 = getSupabase();
22704
23654
  const companyId2 = getCompanyId();
22705
23655
  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 });
23656
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
23657
+ const enriched = (fiscalYears || []).map((fy) => ({
23658
+ ...fy,
23659
+ is_current: !fy.is_closed && fy.start_date <= today && fy.end_date >= today
23660
+ }));
22706
23661
  return {
22707
23662
  contents: [{
22708
23663
  uri: uri.href,
22709
23664
  mimeType: "application/json",
22710
- text: JSON.stringify(fiscalYears || [], null, 2)
23665
+ text: JSON.stringify(enriched)
22711
23666
  }]
22712
23667
  };
22713
23668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirjapro/mcp-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for Finnish accounting — exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude Code, Claude Desktop, and any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {