@kirjapro/mcp-server 0.5.11 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/dist/accounts.d.ts +40 -0
- package/dist/accounts.js +45 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +245 -0
- package/dist/compliance/rates.d.ts +53 -0
- package/dist/compliance/rates.js +57 -0
- package/dist/database.types.d.ts +6525 -0
- package/dist/database.types.js +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +58 -52
- package/dist/lib/incomeStatementSynthesis.d.ts +47 -0
- package/dist/lib/incomeStatementSynthesis.js +140 -0
- package/dist/resources/company.d.ts +2 -0
- package/dist/resources/company.js +75 -0
- package/dist/resources/compliance.d.ts +2 -0
- package/dist/resources/compliance.js +338 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.js +10 -0
- package/dist/resources/schema.d.ts +2 -0
- package/dist/resources/schema.js +63 -0
- package/dist/resources/security.d.ts +2 -0
- package/dist/resources/security.js +33 -0
- package/dist/schemas.d.ts +12 -0
- package/dist/schemas.js +12 -0
- package/dist/tools/accounts.d.ts +2 -0
- package/dist/tools/accounts.js +48 -0
- package/dist/tools/banking.d.ts +2 -0
- package/dist/tools/banking.js +196 -0
- package/dist/tools/bookings.d.ts +2 -0
- package/dist/tools/bookings.js +294 -0
- package/dist/tools/customers.d.ts +2 -0
- package/dist/tools/customers.js +320 -0
- package/dist/tools/financialNotes.d.ts +2 -0
- package/dist/tools/financialNotes.js +255 -0
- package/dist/tools/fingerprints.d.ts +6 -0
- package/dist/tools/fingerprints.js +28 -0
- package/dist/tools/fiscal-years.d.ts +2 -0
- package/dist/tools/fiscal-years.js +35 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +105 -0
- package/dist/tools/invoices.d.ts +2 -0
- package/dist/tools/invoices.js +489 -0
- package/dist/tools/offers.d.ts +2 -0
- package/dist/tools/offers.js +443 -0
- package/dist/tools/received-invoices.d.ts +2 -0
- package/dist/tools/received-invoices.js +134 -0
- package/dist/tools/recurring-invoices.d.ts +2 -0
- package/dist/tools/recurring-invoices.js +348 -0
- package/dist/tools/reports.d.ts +2 -0
- package/dist/tools/reports.js +869 -0
- package/dist/tools/templates.d.ts +89 -0
- package/dist/tools/templates.js +636 -0
- package/dist/tools/transactions.d.ts +2 -0
- package/dist/tools/transactions.js +597 -0
- package/dist/tools/validation.d.ts +26 -0
- package/dist/tools/validation.js +127 -0
- package/dist/types.d.ts +388 -0
- package/dist/types.js +34 -0
- package/dist/utils.d.ts +276 -0
- package/dist/utils.js +637 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +3 -2
- package/server.json +5 -1
- package/tool-fingerprints.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const Constants = {
|
|
2
|
+
public: {
|
|
3
|
+
Enums: {
|
|
4
|
+
account_type: ["asset", "liability", "equity", "revenue", "expense"],
|
|
5
|
+
company_role: ["owner", "admin", "accountant", "viewer"],
|
|
6
|
+
company_size: ["micro", "small", "large"],
|
|
7
|
+
platform_role: ["admin", "superadmin"],
|
|
8
|
+
subscription_tier: ["free", "entrepreneur", "growth", "company", "custom"],
|
|
9
|
+
vat_category: [
|
|
10
|
+
"standard",
|
|
11
|
+
"zero_rate_export",
|
|
12
|
+
"zero_rate_eu",
|
|
13
|
+
"reverse_charge",
|
|
14
|
+
"exempt",
|
|
15
|
+
"margin_scheme",
|
|
16
|
+
],
|
|
17
|
+
vat_period: ["monthly", "quarterly", "yearly"],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -30869,18 +30869,27 @@ var companyId = null;
|
|
|
30869
30869
|
var refreshTimer = null;
|
|
30870
30870
|
async function authenticateWithApiKey(apiKey) {
|
|
30871
30871
|
const url2 = `${SUPABASE_PROJECT_URL}/functions/v1/mcp-auth`;
|
|
30872
|
-
|
|
30873
|
-
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30877
|
-
|
|
30878
|
-
|
|
30879
|
-
|
|
30880
|
-
|
|
30881
|
-
|
|
30872
|
+
try {
|
|
30873
|
+
const response = await fetch(url2, {
|
|
30874
|
+
method: "POST",
|
|
30875
|
+
headers: { "Content-Type": "application/json" },
|
|
30876
|
+
body: JSON.stringify({ api_key: apiKey }),
|
|
30877
|
+
signal: AbortSignal.timeout(1e4)
|
|
30878
|
+
});
|
|
30879
|
+
if (!response.ok) {
|
|
30880
|
+
const body = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
30881
|
+
throw new Error(
|
|
30882
|
+
`API key authentication failed (${response.status}): ${body.error || "Unknown error"}`
|
|
30883
|
+
);
|
|
30884
|
+
}
|
|
30885
|
+
return await response.json();
|
|
30886
|
+
} catch (err) {
|
|
30887
|
+
if (err instanceof Error && err.message.startsWith("API key authentication failed")) {
|
|
30888
|
+
throw err;
|
|
30889
|
+
}
|
|
30890
|
+
const reason = err?.name === "TimeoutError" ? "timed out after 10s" : `failed: ${err?.message ?? "network error"}`;
|
|
30891
|
+
throw new Error(`API key authentication request to mcp-auth ${reason}`);
|
|
30882
30892
|
}
|
|
30883
|
-
return response.json();
|
|
30884
30893
|
}
|
|
30885
30894
|
function createClientFromAuth(auth) {
|
|
30886
30895
|
return createClient(auth.supabase_url, auth.anon_key, {
|
|
@@ -30984,10 +30993,10 @@ async function initLegacyAuth() {
|
|
|
30984
30993
|
}
|
|
30985
30994
|
const { data: subscription } = await supabase.from("subscriptions").select("tier").eq("user_id", userData.user.id).single();
|
|
30986
30995
|
const tier = subscription?.tier || "free";
|
|
30987
|
-
const apiTiers = ["entrepreneur", "company", "custom"];
|
|
30996
|
+
const apiTiers = ["entrepreneur", "growth", "company", "custom"];
|
|
30988
30997
|
if (!apiTiers.includes(tier)) {
|
|
30989
30998
|
throw new Error(
|
|
30990
|
-
`API access requires
|
|
30999
|
+
`API access requires a Yritt\xE4j\xE4, Kasvu, Tilitoimisto, or R\xE4\xE4t\xE4l\xF6ity subscription. Current tier: ${tier}. Upgrade at https://kirjapro.fi/settings/billing`
|
|
30991
31000
|
);
|
|
30992
31001
|
}
|
|
30993
31002
|
companyId = kirjaproCompanyId;
|
|
@@ -32029,8 +32038,8 @@ function registerTransactionTools(server2) {
|
|
|
32029
32038
|
|
|
32030
32039
|
// src/types.ts
|
|
32031
32040
|
var VALID_OFFER_TRANSITIONS = {
|
|
32032
|
-
draft: ["
|
|
32033
|
-
sent: ["
|
|
32041
|
+
draft: ["expired"],
|
|
32042
|
+
sent: ["accepted", "rejected", "expired", "draft"],
|
|
32034
32043
|
viewed: ["accepted", "rejected", "expired", "draft"],
|
|
32035
32044
|
accepted: [],
|
|
32036
32045
|
rejected: ["draft"],
|
|
@@ -33235,7 +33244,7 @@ function registerReportTools(server2) {
|
|
|
33235
33244
|
if (error51) {
|
|
33236
33245
|
return toErrorContent(sanitizeDbError("Failed to generate income statement", error51));
|
|
33237
33246
|
}
|
|
33238
|
-
const { data: lineTypes, error: lineTypesError } = await supabase2.from("income_statement_line_types").select("
|
|
33247
|
+
const { data: lineTypes, error: lineTypesError } = await supabase2.from("income_statement_line_types").select("line_code, line_name_fi, line_type, indent_level, is_calculated, sign_convention, display_order").eq("template_id", templateId).order("display_order", { ascending: true });
|
|
33239
33248
|
if (lineTypesError || !lineTypes) {
|
|
33240
33249
|
return toErrorContent(
|
|
33241
33250
|
sanitizeDbError("Failed to load income statement template lines", lineTypesError)
|
|
@@ -35144,10 +35153,10 @@ function registerOfferTools(server2) {
|
|
|
35144
35153
|
server2.registerTool(
|
|
35145
35154
|
"kirjapro_update_offer_status",
|
|
35146
35155
|
{
|
|
35147
|
-
description: "Update offer status
|
|
35156
|
+
description: "Update offer status (sender-side). Valid transitions: draft\u2192expired, sent\u2192accepted/rejected/expired/draft, viewed\u2192accepted/rejected/expired/draft, rejected/expired\u2192draft. draft\u2192sent is NOT here \u2014 use the send-offer-email path (recipient gets the public link). Accepted is terminal (convert to invoice instead).",
|
|
35148
35157
|
inputSchema: {
|
|
35149
35158
|
offerId: external_exports.string().uuid().describe("Offer UUID"),
|
|
35150
|
-
status: external_exports.enum(["draft", "
|
|
35159
|
+
status: external_exports.enum(["draft", "accepted", "rejected", "expired"]).describe("New status"),
|
|
35151
35160
|
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
|
|
35152
35161
|
dryRun: DRY_RUN_SCHEMA
|
|
35153
35162
|
},
|
|
@@ -35195,43 +35204,32 @@ function registerOfferTools(server2) {
|
|
|
35195
35204
|
params.idempotencyKey,
|
|
35196
35205
|
params,
|
|
35197
35206
|
async () => {
|
|
35198
|
-
const
|
|
35199
|
-
|
|
35200
|
-
|
|
35201
|
-
|
|
35202
|
-
|
|
35203
|
-
|
|
35204
|
-
|
|
35205
|
-
|
|
35206
|
-
|
|
35207
|
-
|
|
35208
|
-
updateFields.accepted_at = nowIso;
|
|
35209
|
-
break;
|
|
35210
|
-
case "rejected":
|
|
35211
|
-
updateFields.rejected_at = nowIso;
|
|
35212
|
-
break;
|
|
35213
|
-
}
|
|
35214
|
-
const { data: updated, error: updateError } = await supabase2.from("offers").update(updateFields).eq("id", params.offerId).eq("company_id", companyId2).eq("status", currentStatus).select("id");
|
|
35215
|
-
if (updateError) {
|
|
35216
|
-
return toErrorContent(sanitizeDbError("Failed to update offer status", updateError));
|
|
35217
|
-
}
|
|
35218
|
-
if (!updated || updated.length === 0) {
|
|
35219
|
-
return toErrorContent(
|
|
35220
|
-
`Offer ${offer.offer_number} status changed concurrently. Please retry.`
|
|
35221
|
-
);
|
|
35207
|
+
const { data: rpcData, error: rpcError } = await supabase2.rpc(
|
|
35208
|
+
"record_offer_status_change",
|
|
35209
|
+
{
|
|
35210
|
+
p_offer_id: params.offerId,
|
|
35211
|
+
p_company_id: companyId2,
|
|
35212
|
+
p_to_status: newStatus
|
|
35213
|
+
}
|
|
35214
|
+
);
|
|
35215
|
+
if (rpcError) {
|
|
35216
|
+
return toErrorContent(sanitizeDbError("Failed to update offer status", rpcError));
|
|
35222
35217
|
}
|
|
35223
|
-
const
|
|
35224
|
-
|
|
35225
|
-
|
|
35226
|
-
|
|
35227
|
-
|
|
35228
|
-
|
|
35218
|
+
const result = rpcData;
|
|
35219
|
+
if (!result?.success) {
|
|
35220
|
+
if (result?.error === "not_claimable") {
|
|
35221
|
+
return toErrorContent(
|
|
35222
|
+
`Offer ${offer.offer_number} status changed concurrently (current: ${result.current_status ?? "unknown"}). Please re-check and retry.`
|
|
35223
|
+
);
|
|
35224
|
+
}
|
|
35225
|
+
return toErrorContent(result?.error ?? "Failed to update offer status");
|
|
35229
35226
|
}
|
|
35230
35227
|
return toResult({
|
|
35231
35228
|
message: `Offer ${offer.offer_number} status changed from "${currentStatus}" to "${newStatus}"`,
|
|
35232
35229
|
offer_number: offer.offer_number,
|
|
35233
35230
|
previous_status: currentStatus,
|
|
35234
|
-
new_status: newStatus
|
|
35231
|
+
new_status: newStatus,
|
|
35232
|
+
stamp: result.stamp
|
|
35235
35233
|
});
|
|
35236
35234
|
}
|
|
35237
35235
|
);
|
|
@@ -36637,7 +36635,7 @@ function registerResources(server2) {
|
|
|
36637
36635
|
}
|
|
36638
36636
|
|
|
36639
36637
|
// src/version.ts
|
|
36640
|
-
var VERSION = "0.
|
|
36638
|
+
var VERSION = "0.6.0";
|
|
36641
36639
|
|
|
36642
36640
|
// src/index.ts
|
|
36643
36641
|
var INSTRUCTIONS_COMMON = `
|
|
@@ -36801,8 +36799,9 @@ var server = new McpServer(
|
|
|
36801
36799
|
var TIER_LABELS = {
|
|
36802
36800
|
free: "Ilmainen",
|
|
36803
36801
|
entrepreneur: "Yritt\xE4j\xE4 (19 \u20AC/kk)",
|
|
36802
|
+
growth: "Kasvu (39 \u20AC/kk)",
|
|
36804
36803
|
company: "Tilitoimisto (97 \u20AC/kk)",
|
|
36805
|
-
custom: "
|
|
36804
|
+
custom: "R\xE4\xE4t\xE4l\xF6ity"
|
|
36806
36805
|
};
|
|
36807
36806
|
function getRegisteredToolCount(mcpServer) {
|
|
36808
36807
|
return Object.keys(
|
|
@@ -36822,7 +36821,14 @@ function registerPrompts(mcpServer) {
|
|
|
36822
36821
|
const [companyResult, fyResult, subResult] = await Promise.all([
|
|
36823
36822
|
supabase2.from("companies").select("name, business_id, industry, address, postal_code, city").eq("id", companyId2).single(),
|
|
36824
36823
|
supabase2.from("fiscal_years").select("start_date, end_date, is_closed").eq("company_id", companyId2).order("start_date", { ascending: false }),
|
|
36825
|
-
|
|
36824
|
+
// Tier is keyed by the subscription OWNER (user_id), not company_id.
|
|
36825
|
+
// `subscriptions.company_id` is nullable and never populated by billing
|
|
36826
|
+
// (stripe-webhook keys every write by user_id), so `.eq('company_id', …)`
|
|
36827
|
+
// matched 0 rows and silently showed every paying customer "Ilmainen".
|
|
36828
|
+
// The `subscriptions_select` RLS policy (user_id = auth.uid()) already
|
|
36829
|
+
// scopes this to the caller's own row — no explicit user filter needed.
|
|
36830
|
+
// Newest row wins if more than one exists.
|
|
36831
|
+
supabase2.from("subscriptions").select("tier").order("created_at", { ascending: false }).limit(1).maybeSingle()
|
|
36826
36832
|
]);
|
|
36827
36833
|
const company = companyResult.data;
|
|
36828
36834
|
let companyBlock;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Income statement synthesis — calculated subtotals from leaf-line amounts.
|
|
3
|
+
*
|
|
4
|
+
* DUPLICATED BY INTENT from `src/hooks/useIncomeStatementCalculation.ts`
|
|
5
|
+
* (`synthesizeIncomeStatementLines`). The mcp-server is published as a
|
|
6
|
+
* separate npm package and cannot import from `src/`. The contract is:
|
|
7
|
+
*
|
|
8
|
+
* Any change to the web hook's synthesis switch (line_code cases,
|
|
9
|
+
* addends, sign conventions) MUST be mirrored here, and vice versa.
|
|
10
|
+
*
|
|
11
|
+
* Both paths consume the same `get_income_statement` RPC, which returns
|
|
12
|
+
* ONLY leaf rows (range-based line_codes). Calculated subtotals
|
|
13
|
+
* (LIIKEVOITTO, RAHOITUS_TOTAL, TILIKAUDEN_TULOS, ...) are synthesized
|
|
14
|
+
* client-side.
|
|
15
|
+
*/
|
|
16
|
+
import type { Database } from "../database.types.js";
|
|
17
|
+
type IncomeStatementLineType = Database["public"]["Tables"]["income_statement_line_types"]["Row"];
|
|
18
|
+
export interface CalculatedIncomeStatementLine {
|
|
19
|
+
lineCode: string;
|
|
20
|
+
lineName: string;
|
|
21
|
+
lineType: string | null;
|
|
22
|
+
indentLevel: number;
|
|
23
|
+
currentAmount: number;
|
|
24
|
+
previousAmount: number;
|
|
25
|
+
variance: number;
|
|
26
|
+
variancePercentage: number | null;
|
|
27
|
+
isCalculated: boolean;
|
|
28
|
+
signConvention: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface CalculationTotals {
|
|
31
|
+
liikevaihto: number;
|
|
32
|
+
liikevoitto: number;
|
|
33
|
+
voittoEnnenVeroja: number;
|
|
34
|
+
tilikausiTulos: number;
|
|
35
|
+
}
|
|
36
|
+
export interface SynthesisResult {
|
|
37
|
+
lines: CalculatedIncomeStatementLine[];
|
|
38
|
+
totals: CalculationTotals;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Synthesize calculated subtotals (LIIKEVOITTO, MATERIAALIT_TOTAL, ...)
|
|
42
|
+
* from pre-computed leaf-line amounts. Calculated subtotals are processed
|
|
43
|
+
* in lineTypes order (display_order); each subtotal MUST appear after its
|
|
44
|
+
* dependencies in the template.
|
|
45
|
+
*/
|
|
46
|
+
export declare function synthesizeIncomeStatementLines(lineTypes: IncomeStatementLineType[], currentByLine: Map<string, number>, previousByLine: Map<string, number>): SynthesisResult;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesize calculated subtotals (LIIKEVOITTO, MATERIAALIT_TOTAL, ...)
|
|
3
|
+
* from pre-computed leaf-line amounts. Calculated subtotals are processed
|
|
4
|
+
* in lineTypes order (display_order); each subtotal MUST appear after its
|
|
5
|
+
* dependencies in the template.
|
|
6
|
+
*/
|
|
7
|
+
export function synthesizeIncomeStatementLines(lineTypes, currentByLine, previousByLine) {
|
|
8
|
+
// Defensive copy — we mutate during the calculated-subtotal pass below.
|
|
9
|
+
const lineAmounts = new Map(currentByLine);
|
|
10
|
+
const prevLineAmounts = new Map(previousByLine);
|
|
11
|
+
lineTypes.forEach((lineType) => {
|
|
12
|
+
if (!lineType.is_calculated)
|
|
13
|
+
return;
|
|
14
|
+
let currentAmount = 0;
|
|
15
|
+
let previousAmount = 0;
|
|
16
|
+
const get = (code) => lineAmounts.get(code) || 0;
|
|
17
|
+
const getPrev = (code) => prevLineAmounts.get(code) || 0;
|
|
18
|
+
switch (lineType.line_code) {
|
|
19
|
+
case "MATERIAALIT_TOTAL":
|
|
20
|
+
currentAmount =
|
|
21
|
+
get("OSTOT_TILIKAUDEN") +
|
|
22
|
+
get("VARASTOJEN_MUUTOS_OSTOT") +
|
|
23
|
+
get("ULKOPUOLISET_PALVELUT");
|
|
24
|
+
previousAmount =
|
|
25
|
+
getPrev("OSTOT_TILIKAUDEN") +
|
|
26
|
+
getPrev("VARASTOJEN_MUUTOS_OSTOT") +
|
|
27
|
+
getPrev("ULKOPUOLISET_PALVELUT");
|
|
28
|
+
break;
|
|
29
|
+
case "HENKILOSTO_TOTAL":
|
|
30
|
+
currentAmount =
|
|
31
|
+
get("PALKAT_PALKKIOT") + get("ELAKEKULUT") + get("MUUT_HENKILOSIVUKULUT");
|
|
32
|
+
previousAmount =
|
|
33
|
+
getPrev("PALKAT_PALKKIOT") +
|
|
34
|
+
getPrev("ELAKEKULUT") +
|
|
35
|
+
getPrev("MUUT_HENKILOSIVUKULUT");
|
|
36
|
+
break;
|
|
37
|
+
case "POISTOT_TOTAL":
|
|
38
|
+
currentAmount =
|
|
39
|
+
get("SUUNNITELMAN_POISTOT") +
|
|
40
|
+
get("ARVONALENT_PYSYVAT") +
|
|
41
|
+
get("ARVONALENT_VAIHTUVAT");
|
|
42
|
+
previousAmount =
|
|
43
|
+
getPrev("SUUNNITELMAN_POISTOT") +
|
|
44
|
+
getPrev("ARVONALENT_PYSYVAT") +
|
|
45
|
+
getPrev("ARVONALENT_VAIHTUVAT");
|
|
46
|
+
break;
|
|
47
|
+
case "LIIKEVOITTO":
|
|
48
|
+
currentAmount =
|
|
49
|
+
get("LIIKEVAIHTO") +
|
|
50
|
+
get("VARASTOJEN_MUUTOS") +
|
|
51
|
+
get("VALMISTUS_OMAAN") +
|
|
52
|
+
get("MUUT_TUOTOT") +
|
|
53
|
+
get("MATERIAALIT_TOTAL") +
|
|
54
|
+
get("HENKILOSTO_TOTAL") +
|
|
55
|
+
get("POISTOT_TOTAL") +
|
|
56
|
+
get("MUUT_KULUT");
|
|
57
|
+
previousAmount =
|
|
58
|
+
getPrev("LIIKEVAIHTO") +
|
|
59
|
+
getPrev("VARASTOJEN_MUUTOS") +
|
|
60
|
+
getPrev("VALMISTUS_OMAAN") +
|
|
61
|
+
getPrev("MUUT_TUOTOT") +
|
|
62
|
+
getPrev("MATERIAALIT_TOTAL") +
|
|
63
|
+
getPrev("HENKILOSTO_TOTAL") +
|
|
64
|
+
getPrev("POISTOT_TOTAL") +
|
|
65
|
+
getPrev("MUUT_KULUT");
|
|
66
|
+
break;
|
|
67
|
+
case "RAHOITUS_TOTAL":
|
|
68
|
+
currentAmount =
|
|
69
|
+
get("TUOTOT_KONSERNI") +
|
|
70
|
+
get("TUOTOT_OMISTUSYHTEYS") +
|
|
71
|
+
get("TUOTOT_SIJOITUKSET") +
|
|
72
|
+
get("MUUT_KORKOTUOTOT") +
|
|
73
|
+
get("ARVONALENT_SIJOITUKSET") +
|
|
74
|
+
get("ARVONALENT_RAHOITUS") +
|
|
75
|
+
get("KORKOKULUT");
|
|
76
|
+
previousAmount =
|
|
77
|
+
getPrev("TUOTOT_KONSERNI") +
|
|
78
|
+
getPrev("TUOTOT_OMISTUSYHTEYS") +
|
|
79
|
+
getPrev("TUOTOT_SIJOITUKSET") +
|
|
80
|
+
getPrev("MUUT_KORKOTUOTOT") +
|
|
81
|
+
getPrev("ARVONALENT_SIJOITUKSET") +
|
|
82
|
+
getPrev("ARVONALENT_RAHOITUS") +
|
|
83
|
+
getPrev("KORKOKULUT");
|
|
84
|
+
break;
|
|
85
|
+
case "VOITTO_ENNEN_SIIRTOJA":
|
|
86
|
+
currentAmount = get("LIIKEVOITTO") + get("RAHOITUS_TOTAL");
|
|
87
|
+
previousAmount = getPrev("LIIKEVOITTO") + getPrev("RAHOITUS_TOTAL");
|
|
88
|
+
break;
|
|
89
|
+
case "SIIRROT_TOTAL":
|
|
90
|
+
currentAmount =
|
|
91
|
+
get("POISTOERON_MUUTOS") + get("VARAUSTEN_MUUTOS") + get("KONSERNIAVUSTUS");
|
|
92
|
+
previousAmount =
|
|
93
|
+
getPrev("POISTOERON_MUUTOS") +
|
|
94
|
+
getPrev("VARAUSTEN_MUUTOS") +
|
|
95
|
+
getPrev("KONSERNIAVUSTUS");
|
|
96
|
+
break;
|
|
97
|
+
case "TILIKAUDEN_TULOS":
|
|
98
|
+
currentAmount =
|
|
99
|
+
get("VOITTO_ENNEN_SIIRTOJA") +
|
|
100
|
+
get("SIIRROT_TOTAL") +
|
|
101
|
+
get("TULOVEROT") +
|
|
102
|
+
get("MUUT_VEROT");
|
|
103
|
+
previousAmount =
|
|
104
|
+
getPrev("VOITTO_ENNEN_SIIRTOJA") +
|
|
105
|
+
getPrev("SIIRROT_TOTAL") +
|
|
106
|
+
getPrev("TULOVEROT") +
|
|
107
|
+
getPrev("MUUT_VEROT");
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
lineAmounts.set(lineType.line_code, currentAmount);
|
|
111
|
+
prevLineAmounts.set(lineType.line_code, previousAmount);
|
|
112
|
+
});
|
|
113
|
+
const calculatedLines = lineTypes.map((lineType) => {
|
|
114
|
+
const currentAmount = lineAmounts.get(lineType.line_code) || 0;
|
|
115
|
+
const previousAmount = prevLineAmounts.get(lineType.line_code) || 0;
|
|
116
|
+
const variance = currentAmount - previousAmount;
|
|
117
|
+
const variancePercentage = previousAmount !== 0 ? (variance / Math.abs(previousAmount)) * 100 : null;
|
|
118
|
+
return {
|
|
119
|
+
lineCode: lineType.line_code,
|
|
120
|
+
lineName: lineType.line_name_fi,
|
|
121
|
+
lineType: lineType.line_type,
|
|
122
|
+
indentLevel: lineType.indent_level ?? 0,
|
|
123
|
+
currentAmount,
|
|
124
|
+
previousAmount,
|
|
125
|
+
variance,
|
|
126
|
+
variancePercentage,
|
|
127
|
+
isCalculated: lineType.is_calculated ?? false,
|
|
128
|
+
signConvention: lineType.sign_convention,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
// `??` not `||` so a legitimate break-even 0 is preserved.
|
|
132
|
+
// Skill: fallback-chain-zero-respect.
|
|
133
|
+
const totals = {
|
|
134
|
+
liikevaihto: lineAmounts.get("LIIKEVAIHTO") ?? 0,
|
|
135
|
+
liikevoitto: lineAmounts.get("LIIKEVOITTO") ?? 0,
|
|
136
|
+
voittoEnnenVeroja: lineAmounts.get("VOITTO_ENNEN_SIIRTOJA") ?? 0,
|
|
137
|
+
tilikausiTulos: lineAmounts.get("TILIKAUDEN_TULOS") ?? 0,
|
|
138
|
+
};
|
|
139
|
+
return { lines: calculatedLines, totals };
|
|
140
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getSupabase, getCompanyId } from '../auth.js';
|
|
2
|
+
import { helsinkiDateString, sanitizeDbError } from '../utils.js';
|
|
3
|
+
export function registerCompanyResources(server) {
|
|
4
|
+
server.registerResource('company-info', 'kirjapro://company/info', {
|
|
5
|
+
description: 'Current company details: name, business ID (Y-tunnus), address, and settings.',
|
|
6
|
+
mimeType: 'application/json',
|
|
7
|
+
}, async (uri) => {
|
|
8
|
+
const supabase = getSupabase();
|
|
9
|
+
const companyId = getCompanyId();
|
|
10
|
+
const { data: company, error } = await supabase
|
|
11
|
+
.from('companies')
|
|
12
|
+
.select('id, name, business_id, address, postal_code, city, country, created_at')
|
|
13
|
+
.eq('id', companyId)
|
|
14
|
+
.single();
|
|
15
|
+
// Don't swallow the error. An empty company object is indistinguishable
|
|
16
|
+
// from auth expiry, RLS denial, or PostgREST failure — and the agent
|
|
17
|
+
// would build decisions on a blank Y-tunnus. Surface the sanitized
|
|
18
|
+
// error so the agent can tell the user to re-auth or retry.
|
|
19
|
+
if (error || !company) {
|
|
20
|
+
return {
|
|
21
|
+
contents: [{
|
|
22
|
+
uri: uri.href,
|
|
23
|
+
mimeType: 'application/json',
|
|
24
|
+
text: JSON.stringify({
|
|
25
|
+
error: error
|
|
26
|
+
? sanitizeDbError('Failed to read company', error)
|
|
27
|
+
: 'Company not found for current context.',
|
|
28
|
+
}),
|
|
29
|
+
}],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
contents: [{
|
|
34
|
+
uri: uri.href,
|
|
35
|
+
mimeType: 'application/json',
|
|
36
|
+
text: JSON.stringify(company),
|
|
37
|
+
}],
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
server.registerResource('fiscal-years', 'kirjapro://fiscal-years', {
|
|
41
|
+
description: 'List of fiscal years (tilikaudet) for the company. Shows which are open/closed and their date ranges.',
|
|
42
|
+
mimeType: 'application/json',
|
|
43
|
+
}, async (uri) => {
|
|
44
|
+
const supabase = getSupabase();
|
|
45
|
+
const companyId = getCompanyId();
|
|
46
|
+
const { data: fiscalYears, error } = await supabase
|
|
47
|
+
.from('fiscal_years')
|
|
48
|
+
.select('id, start_date, end_date, is_closed, created_at')
|
|
49
|
+
.eq('company_id', companyId)
|
|
50
|
+
.order('start_date', { ascending: false });
|
|
51
|
+
if (error) {
|
|
52
|
+
return {
|
|
53
|
+
contents: [{
|
|
54
|
+
uri: uri.href,
|
|
55
|
+
mimeType: 'application/json',
|
|
56
|
+
text: JSON.stringify({
|
|
57
|
+
error: sanitizeDbError('Failed to read fiscal years', error),
|
|
58
|
+
}),
|
|
59
|
+
}],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const today = helsinkiDateString();
|
|
63
|
+
const enriched = (fiscalYears || []).map((fy) => ({
|
|
64
|
+
...fy,
|
|
65
|
+
is_current: !fy.is_closed && fy.start_date <= today && fy.end_date >= today,
|
|
66
|
+
}));
|
|
67
|
+
return {
|
|
68
|
+
contents: [{
|
|
69
|
+
uri: uri.href,
|
|
70
|
+
mimeType: 'application/json',
|
|
71
|
+
text: JSON.stringify(enriched),
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|