@frihet/mcp-server 2.0.1 → 2.0.2
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 -6
- package/dist/canary/mcp-harness.d.ts +1 -1
- package/dist/canary/mcp-harness.js +3 -2
- package/dist/claude-profile.js +3 -1
- package/dist/client-interface.d.ts +7 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +16 -0
- package/dist/demo-client.d.ts +14 -16
- package/dist/demo-client.js +101 -30
- package/dist/demo-fixtures.d.ts +14 -4
- package/dist/demo-fixtures.js +84 -16
- package/dist/invoice-payment-link.d.ts +189 -0
- package/dist/invoice-payment-link.js +371 -0
- package/dist/mpp-sandbox.d.ts +176 -0
- package/dist/mpp-sandbox.js +612 -0
- package/dist/openai-103/openai-expansion-103.d.ts +1192 -0
- package/dist/openai-103/openai-expansion-103.js +1628 -0
- package/dist/openai-profile.d.ts +5 -1
- package/dist/openai-profile.js +14 -2
- package/dist/openai-review-contract.js +2 -1
- package/dist/server-composition.js +34 -0
- package/dist/tool-exposure.js +2 -1
- package/dist/tools/purchaseOrders.d.ts +30 -0
- package/dist/tools/purchaseOrders.js +102 -0
- package/dist/tools/register-all.js +2 -0
- package/dist/tools/shared.d.ts +42 -0
- package/dist/tools/shared.js +46 -0
- package/docs/agent-onboarding.json +2 -2
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<a href="https://www.npmjs.com/package/@frihet/mcp-server"><img src="https://img.shields.io/npm/v/@frihet/mcp-server?style=flat&color=18181b&labelColor=09090b" alt="npm version"></a>
|
|
20
20
|
<a href="https://www.npmjs.com/package/@frihet/mcp-server"><img src="https://img.shields.io/npm/dm/@frihet/mcp-server?style=flat&color=18181b&labelColor=09090b&label=downloads" alt="npm downloads"></a>
|
|
21
21
|
<a href="https://registry.modelcontextprotocol.io/?q=io.frihet"><img src="https://img.shields.io/badge/MCP_Registry-io.frihet%2Ferp-4A90D9?style=flat&logo=anthropic&logoColor=white" alt="MCP Registry"></a>
|
|
22
|
-
<img src="https://img.shields.io/badge/catalogue-
|
|
22
|
+
<img src="https://img.shields.io/badge/catalogue-158_operations-18181b?style=flat&labelColor=09090b" alt="158 canonical catalogue operations">
|
|
23
23
|
<a href="https://github.com/Frihet-io/frihet-mcp/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-18181b?style=flat&labelColor=09090b" alt="license"></a>
|
|
24
24
|
</p>
|
|
25
25
|
|
|
@@ -38,7 +38,7 @@ You: "Create an invoice for TechStart SL, 40 hours of consulting at 75 EUR/h
|
|
|
38
38
|
Claude: Done. Invoice INV-2026-089 created. Total: 3,000.00 EUR + 21% IVA = 3,630.00 EUR.
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
158 canonical operations. Five fiscal aliases. Ten prompts. The local package serves 11 resources; the hosted Worker deliberately serves the 7 static resources, while API-backed workspace resources remain local-profile only.
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
@@ -77,7 +77,7 @@ Local installs need Node 22.23.4+, 24.22.1+ or 26.12.1+. Other clients are cover
|
|
|
77
77
|
|
|
78
78
|
Also listed on [Glama](https://glama.ai/mcp/servers/Frihet-io/frihet-mcp), [Cursor Directory](https://cursor.directory/plugins/mcp-frihet), [mcp.so](https://mcp.so/servers/frihet-mcp), [mcpservers.org](https://mcpservers.org/servers/frihet-io/frihet-mcp) and the [ToolSDK registry](https://github.com/toolsdk-ai/toolsdk-mcp-registry/blob/main/packages/finance-fintech/frihet-mcp.json).
|
|
79
79
|
|
|
80
|
-
> **Surface truth:** the catalogue contains
|
|
80
|
+
> **Surface truth:** the catalogue contains 158 canonical operations. The local full profile serves 163 tool names, 11 resources, and 10 prompts (158 canonical operations plus 5 fiscal aliases). The hosted grouped profile serves 166 tool names, 7 resources, and 10 prompts (the same names plus 3 discovery tools, with API-backed resources kept local-only). The separately reviewed OpenAI profile serves 33 tool names, 0 resources, and 0 prompts. These counts describe the current source release; the hosted server reports the version it is running at `/health`. Catalogue membership is not a promise that a backing API is enabled for every workspace.
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
@@ -197,7 +197,7 @@ If you are an agent reading this repository, the contract is machine-readable an
|
|
|
197
197
|
|
|
198
198
|
## What to expect
|
|
199
199
|
|
|
200
|
-
This MCP is a **structured data interface** -- you describe what you want in natural language, and the AI creates, queries, or modifies business records in Frihet. Most of the
|
|
200
|
+
This MCP is a **structured data interface** -- you describe what you want in natural language, and the AI creates, queries, or modifies business records in Frihet. Most of the 158 canonical operations are CRUD operations over the REST API; the rest are read-only summaries and fiscal/e-invoice actions. Alias and discovery names are counted separately.
|
|
201
201
|
|
|
202
202
|
It does not do OCR, PDF scanning, file upload or image processing. To digitize a paper invoice, extract the data first (a vision model, an OCR service, or manual entry), then ask the assistant to create the record. One API key maps to one Frihet workspace, and you need a Frihet account at [app.frihet.io](https://app.frihet.io).
|
|
203
203
|
|
|
@@ -206,7 +206,7 @@ It does not do OCR, PDF scanning, file upload or image processing. To digitize a
|
|
|
206
206
|
## Full catalogue
|
|
207
207
|
|
|
208
208
|
<details>
|
|
209
|
-
<summary><strong>All
|
|
209
|
+
<summary><strong>All 158 canonical operations, resources and prompts</strong></summary>
|
|
210
210
|
|
|
211
211
|
### Invoices (12)
|
|
212
212
|
|
|
@@ -629,7 +629,7 @@ Test with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspect
|
|
|
629
629
|
|
|
630
630
|
| Package | What it is |
|
|
631
631
|
|---------|-----------|
|
|
632
|
-
| [`@frihet/mcp-server`](https://www.npmjs.com/package/@frihet/mcp-server) | This MCP server (
|
|
632
|
+
| [`@frihet/mcp-server`](https://www.npmjs.com/package/@frihet/mcp-server) | This MCP server (158 canonical operations + 5 alias names; 11 local resources; 10 prompts) |
|
|
633
633
|
| [`@frihet/sdk`](https://github.com/Frihet-io/frihet-sdk) | TypeScript SDK (`frihet.invoices.create()`) |
|
|
634
634
|
| [`frihet`](https://www.npmjs.com/package/frihet) | CLI (`frihet invoices list --status overdue`) |
|
|
635
635
|
| [`n8n-nodes-frihet`](https://www.npmjs.com/package/n8n-nodes-frihet) | n8n community node for workflow automation |
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const HARNESS_CONTRACT_VERSION = 1;
|
|
17
17
|
export declare const INSPECTOR_PINNED_VERSION = "2.3.0";
|
|
18
|
-
export declare const CANONICAL_OPERATIONS_COUNT =
|
|
18
|
+
export declare const CANONICAL_OPERATIONS_COUNT = 158;
|
|
19
19
|
export declare const FISCAL_ALIASES_COUNT: number;
|
|
20
20
|
export type CheckStatus = "PASS" | "FAIL" | "UNSUPPORTED" | "NOT_EXERCISED";
|
|
21
21
|
export type OverallStatus = "PASS" | "PASS_WITH_GAPS" | "FAIL";
|
|
@@ -24,8 +24,9 @@ export const HARNESS_CONTRACT_VERSION = 1;
|
|
|
24
24
|
export const INSPECTOR_PINNED_VERSION = "2.3.0";
|
|
25
25
|
// Updated 2026-10-10 by issue #124 (IGIC-pattern withdraw of dead onboarding_status /
|
|
26
26
|
// onboarding_persona_set — the Frihet-ERP /v1/onboarding/* family is absent, so the
|
|
27
|
-
// MCP catalogue
|
|
28
|
-
|
|
27
|
+
// MCP catalogue then declared 156 canonical operations, down from 158; the two read-only
|
|
28
|
+
// purchase-order tools (ADR 0027 S2) bring it to 158).
|
|
29
|
+
export const CANONICAL_OPERATIONS_COUNT = 158;
|
|
29
30
|
export const FISCAL_ALIASES_COUNT = Object.keys(FISCAL_MODELO_ALIASES).length;
|
|
30
31
|
/**
|
|
31
32
|
* Executes the complete compatibility harness in-process and returns
|
package/dist/claude-profile.js
CHANGED
|
@@ -170,7 +170,9 @@ export const CLAUDE_EXCLUDED_TOOLS = {
|
|
|
170
170
|
"period_reopen": "The handler deliberately defers execution; capability truth marks this deferred.",
|
|
171
171
|
"refund_sale": "Executes an actual Stripe money transfer; excluded under Directory policy section 4.A.",
|
|
172
172
|
"send_quote": "Previewed recipient is not atomically bound and delivery is not retry-safe.",
|
|
173
|
-
"sync_channel": "The handler deliberately defers execution; capability truth marks this deferred."
|
|
173
|
+
"sync_channel": "The handler deliberately defers execution; capability truth marks this deferred.",
|
|
174
|
+
"get_purchase_order": "Added after the 176de12 Claude operational candidate freeze; not yet admitted to the Claude profile.",
|
|
175
|
+
"list_purchase_orders": "Added after the 176de12 Claude operational candidate freeze; not yet admitted to the Claude profile."
|
|
174
176
|
};
|
|
175
177
|
export const CLAUDE_CANDIDATE_RESOURCES = new Set([
|
|
176
178
|
"frihet://config/currencies",
|
|
@@ -101,6 +101,13 @@ export interface IFrihetClient {
|
|
|
101
101
|
createVendor(data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
102
102
|
updateVendor(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
103
103
|
deleteVendor(id: string): Promise<void>;
|
|
104
|
+
listPurchaseOrders(params?: {
|
|
105
|
+
status?: string;
|
|
106
|
+
vendorId?: string;
|
|
107
|
+
limit?: number;
|
|
108
|
+
offset?: number;
|
|
109
|
+
}): Promise<PaginatedResponse<Record<string, unknown>>>;
|
|
110
|
+
getPurchaseOrder(id: string): Promise<Record<string, unknown>>;
|
|
104
111
|
sendInvoice(id: string, to?: string): Promise<Record<string, unknown>>;
|
|
105
112
|
markInvoicePaid(id: string, paidDate?: string): Promise<Record<string, unknown>>;
|
|
106
113
|
getInvoicePdf(id: string): Promise<import("./client.js").BinaryDocument>;
|
package/dist/client.d.ts
CHANGED
|
@@ -275,6 +275,13 @@ export declare class FrihetClient {
|
|
|
275
275
|
createVendor(data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
276
276
|
updateVendor(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
277
277
|
deleteVendor(id: string): Promise<void>;
|
|
278
|
+
listPurchaseOrders(params?: {
|
|
279
|
+
status?: string;
|
|
280
|
+
vendorId?: string;
|
|
281
|
+
limit?: number;
|
|
282
|
+
offset?: number;
|
|
283
|
+
}): Promise<PaginatedResponse<Record<string, unknown>>>;
|
|
284
|
+
getPurchaseOrder(id: string): Promise<Record<string, unknown>>;
|
|
278
285
|
sendInvoice(id: string, to?: string): Promise<Record<string, unknown>>;
|
|
279
286
|
markInvoicePaid(id: string, paidDate?: string): Promise<Record<string, unknown>>;
|
|
280
287
|
getInvoicePdf(id: string): Promise<BinaryDocument>;
|
package/dist/client.js
CHANGED
|
@@ -850,6 +850,22 @@ export class FrihetClient {
|
|
|
850
850
|
async deleteVendor(id) {
|
|
851
851
|
return this.request("DELETE", `/vendors/${pathSegment(id)}`);
|
|
852
852
|
}
|
|
853
|
+
// ---------------------------------------------------------------- Purchase Orders
|
|
854
|
+
// ----------------------------------------------------------------
|
|
855
|
+
// ADR 0027 S2, server-owned lifecycle. Routes are read-only here; the
|
|
856
|
+
// write surface (create draft, send, confirm, cancel, receive,
|
|
857
|
+
// reverse-receipt, bill) is deliberately not exposed by the MCP server.
|
|
858
|
+
async listPurchaseOrders(params) {
|
|
859
|
+
return this.requestPaginated("GET", "/purchase-orders", undefined, {
|
|
860
|
+
status: params?.status,
|
|
861
|
+
vendorId: params?.vendorId,
|
|
862
|
+
limit: params?.limit,
|
|
863
|
+
offset: params?.offset,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
async getPurchaseOrder(id) {
|
|
867
|
+
return this.requestUnwrapped("GET", `/purchase-orders/${pathSegment(id)}`);
|
|
868
|
+
}
|
|
853
869
|
// ---------------------------------------------------------------- Invoice Actions
|
|
854
870
|
// ----------------------------------------------------------------
|
|
855
871
|
async sendInvoice(id, to) {
|
package/dist/demo-client.d.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A drop-in IFrihetClient that makes ZERO network calls: it serves the canned
|
|
5
5
|
* PII-safe fixtures in demo-fixtures.ts. index.ts swaps this in for FrihetClient
|
|
6
|
-
* when demo mode is on
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* when demo mode is on. Single-record fixtures carry their own demo stamps;
|
|
7
|
+
* server-composition.ts stamps paginated MCP results after profile projection
|
|
8
|
+
* and declares those fields only on demo schemas, preserving live contracts.
|
|
9
9
|
*
|
|
10
10
|
* Response contract (see SPEC-mcp-demo-mode.md option B/C):
|
|
11
|
-
* - reads/lists → fixture data stamped `_demo:true` (READ notice).
|
|
11
|
+
* - reads/lists → MCP fixture data stamped `_demo:true` (READ notice).
|
|
12
12
|
* - lists without fixtures → EMPTY PaginatedResponse, still stamped.
|
|
13
13
|
* - writes → plausible fixture-shaped object with a `demo_`-prefixed id
|
|
14
14
|
* (WRITE notice "…Simulated — not persisted.").
|
|
@@ -23,10 +23,7 @@ import { type BinaryDocument, type EInvoiceDocument, type EInvoiceExportResult,
|
|
|
23
23
|
type Rec = Record<string, unknown>;
|
|
24
24
|
export declare class DemoFrihetClient implements IFrihetClient {
|
|
25
25
|
readonly isDemoClient: true;
|
|
26
|
-
listInvoices(params?:
|
|
27
|
-
limit?: number;
|
|
28
|
-
offset?: number;
|
|
29
|
-
}): Promise<PaginatedResponse<Rec>>;
|
|
26
|
+
listInvoices(params?: Parameters<IFrihetClient["listInvoices"]>[0]): Promise<PaginatedResponse<Rec>>;
|
|
30
27
|
getInvoice(id: string): Promise<Rec>;
|
|
31
28
|
createInvoice(data: Rec): Promise<Rec>;
|
|
32
29
|
updateInvoice(id: string, data: Rec): Promise<Rec>;
|
|
@@ -35,10 +32,7 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
35
32
|
limit?: number;
|
|
36
33
|
offset?: number;
|
|
37
34
|
}): Promise<PaginatedResponse<Rec>>;
|
|
38
|
-
listExpenses(params?:
|
|
39
|
-
limit?: number;
|
|
40
|
-
offset?: number;
|
|
41
|
-
}): Promise<PaginatedResponse<Rec>>;
|
|
35
|
+
listExpenses(params?: Parameters<IFrihetClient["listExpenses"]>[0]): Promise<PaginatedResponse<Rec>>;
|
|
42
36
|
getExpense(id: string): Promise<Rec>;
|
|
43
37
|
createExpense(data: Rec): Promise<Rec>;
|
|
44
38
|
updateExpense(id: string, data: Rec): Promise<Rec>;
|
|
@@ -75,6 +69,13 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
75
69
|
createVendor(data: Rec): Promise<Rec>;
|
|
76
70
|
updateVendor(id: string, data: Rec): Promise<Rec>;
|
|
77
71
|
deleteVendor(_id: string): Promise<void>;
|
|
72
|
+
listPurchaseOrders(params?: {
|
|
73
|
+
status?: string;
|
|
74
|
+
vendorId?: string;
|
|
75
|
+
limit?: number;
|
|
76
|
+
offset?: number;
|
|
77
|
+
}): Promise<PaginatedResponse<Rec>>;
|
|
78
|
+
getPurchaseOrder(id: string): Promise<Rec>;
|
|
78
79
|
sendInvoice(id: string, to?: string): Promise<Rec>;
|
|
79
80
|
markInvoicePaid(id: string, paidDate?: string): Promise<Rec>;
|
|
80
81
|
getInvoicePdf(id: string): Promise<BinaryDocument>;
|
|
@@ -271,10 +272,7 @@ export declare class DemoFrihetClient implements IFrihetClient {
|
|
|
271
272
|
offset?: number;
|
|
272
273
|
}): Promise<PaginatedResponse<Rec>>;
|
|
273
274
|
getBankAccount(id: string): Promise<Rec>;
|
|
274
|
-
listTransactions(params?:
|
|
275
|
-
limit?: number;
|
|
276
|
-
offset?: number;
|
|
277
|
-
}): Promise<PaginatedResponse<Rec>>;
|
|
275
|
+
listTransactions(params?: Parameters<IFrihetClient["listTransactions"]>[0]): Promise<PaginatedResponse<Rec>>;
|
|
278
276
|
categorizeTransaction(id: string, data: {
|
|
279
277
|
category: string;
|
|
280
278
|
notes?: string;
|
package/dist/demo-client.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A drop-in IFrihetClient that makes ZERO network calls: it serves the canned
|
|
5
5
|
* PII-safe fixtures in demo-fixtures.ts. index.ts swaps this in for FrihetClient
|
|
6
|
-
* when demo mode is on
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* when demo mode is on. Single-record fixtures carry their own demo stamps;
|
|
7
|
+
* server-composition.ts stamps paginated MCP results after profile projection
|
|
8
|
+
* and declares those fields only on demo schemas, preserving live contracts.
|
|
9
9
|
*
|
|
10
10
|
* Response contract (see SPEC-mcp-demo-mode.md option B/C):
|
|
11
|
-
* - reads/lists → fixture data stamped `_demo:true` (READ notice).
|
|
11
|
+
* - reads/lists → MCP fixture data stamped `_demo:true` (READ notice).
|
|
12
12
|
* - lists without fixtures → EMPTY PaginatedResponse, still stamped.
|
|
13
13
|
* - writes → plausible fixture-shaped object with a `demo_`-prefixed id
|
|
14
14
|
* (WRITE notice "…Simulated — not persisted.").
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { toApiEInvoiceExportFormat, } from "./client.js";
|
|
21
21
|
import { Buffer } from "node:buffer";
|
|
22
|
-
import { READ_STAMP, FISCAL_STAMP, DEMO_NOW, demoPage, demoEmptyPage, findOrStub, demoId, simulateWrite, simulateAction, demoClients, demoProducts, demoInvoices, demoExpenses, demoQuotes, demoVendors, demoBankAccounts, demoTransactions, } from "./demo-fixtures.js";
|
|
22
|
+
import { READ_STAMP, FISCAL_STAMP, DEMO_NOW, demoPage, demoEmptyPage, findOrStub, demoId, simulateWrite, simulateAction, demoClients, demoMoney, demoSum, demoTopClients, demoProducts, demoInvoices, demoExpenses, demoQuotes, demoVendors, demoPurchaseOrders, demoBankAccounts, demoTransactions, } from "./demo-fixtures.js";
|
|
23
23
|
const DEMO_RBAC_ROLES = ["owner", "admin", "manager", "sales", "accountant", "employee", "viewer"];
|
|
24
24
|
const DEMO_RBAC_RESOURCES = [
|
|
25
25
|
"workspace", "invoices", "quotes", "expenses", "clients", "products", "accounting",
|
|
@@ -78,7 +78,12 @@ export class DemoFrihetClient {
|
|
|
78
78
|
isDemoClient = true;
|
|
79
79
|
// ---------------------------------------------------------------- Invoices
|
|
80
80
|
async listInvoices(params) {
|
|
81
|
-
|
|
81
|
+
const filtered = demoInvoices.filter((row) => (!params?.from || (typeof row.issueDate === "string" && row.issueDate >= params.from))
|
|
82
|
+
&& (!params?.to || (typeof row.issueDate === "string" && row.issueDate <= params.to))
|
|
83
|
+
&& (!params?.status || row.status === params.status)
|
|
84
|
+
&& (!params?.clientId || row.clientId === params.clientId)
|
|
85
|
+
&& (!params?.seriesId || row.seriesId === params.seriesId));
|
|
86
|
+
return demoPage(filtered, params, ["issueDate", "documentNumber", "id"]);
|
|
82
87
|
}
|
|
83
88
|
async getInvoice(id) {
|
|
84
89
|
return findOrStub(demoInvoices, id);
|
|
@@ -109,7 +114,11 @@ export class DemoFrihetClient {
|
|
|
109
114
|
}
|
|
110
115
|
// ---------------------------------------------------------------- Expenses
|
|
111
116
|
async listExpenses(params) {
|
|
112
|
-
|
|
117
|
+
const filtered = demoExpenses.filter((row) => (!params?.from || (typeof row.date === "string" && row.date >= params.from))
|
|
118
|
+
&& (!params?.to || (typeof row.date === "string" && row.date <= params.to))
|
|
119
|
+
&& (!params?.vendorId || row.vendorId === params.vendorId)
|
|
120
|
+
&& (!params?.category || row.category === params.category));
|
|
121
|
+
return demoPage(filtered, params, ["date", "id"]);
|
|
113
122
|
}
|
|
114
123
|
async getExpense(id) {
|
|
115
124
|
return findOrStub(demoExpenses, id);
|
|
@@ -187,6 +196,21 @@ export class DemoFrihetClient {
|
|
|
187
196
|
async deleteVendor(_id) {
|
|
188
197
|
return;
|
|
189
198
|
}
|
|
199
|
+
// ---------------------------------------------------------------- Purchase Orders
|
|
200
|
+
// Read-only surface (ADR 0027 S2). Write tools are intentionally absent.
|
|
201
|
+
async listPurchaseOrders(params) {
|
|
202
|
+
const filtered = demoPurchaseOrders.filter((po) => {
|
|
203
|
+
if (params?.status && po.status !== params.status)
|
|
204
|
+
return false;
|
|
205
|
+
if (params?.vendorId && po.vendorId !== params.vendorId)
|
|
206
|
+
return false;
|
|
207
|
+
return true;
|
|
208
|
+
});
|
|
209
|
+
return demoPage(filtered, params);
|
|
210
|
+
}
|
|
211
|
+
async getPurchaseOrder(id) {
|
|
212
|
+
return findOrStub(demoPurchaseOrders, id);
|
|
213
|
+
}
|
|
190
214
|
// ---------------------------------------------------------------- Invoice actions
|
|
191
215
|
async sendInvoice(id, to) {
|
|
192
216
|
// Email dispatch — simulated, no email is actually sent.
|
|
@@ -371,6 +395,17 @@ export class DemoFrihetClient {
|
|
|
371
395
|
}
|
|
372
396
|
// ---------------------------------------------------------------- Intelligence
|
|
373
397
|
async getBusinessContext() {
|
|
398
|
+
const month = DEMO_NOW.slice(0, 7);
|
|
399
|
+
const today = DEMO_NOW.slice(0, 10);
|
|
400
|
+
const monthInvoices = demoInvoices.filter((invoice) => String(invoice.issueDate).startsWith(month));
|
|
401
|
+
const monthExpenses = demoExpenses.filter((expense) => String(expense.date).startsWith(month));
|
|
402
|
+
// Fixture invoices have no partial payments: paid means settled; drafts are not receivables.
|
|
403
|
+
const unpaid = demoInvoices.filter((invoice) => invoice.status === "sent" || invoice.status === "overdue");
|
|
404
|
+
const overdue = unpaid.filter((invoice) => String(invoice.dueDate) < today);
|
|
405
|
+
const lastInvoice = [...demoInvoices].sort((a, b) => String(b.issueDate).localeCompare(String(a.issueDate)))[0];
|
|
406
|
+
const lastExpense = [...demoExpenses].sort((a, b) => String(b.date).localeCompare(String(a.date)))[0];
|
|
407
|
+
const revenue = demoSum(monthInvoices.filter((invoice) => invoice.status !== "draft" && invoice.status !== "cancelled"), "total");
|
|
408
|
+
const expenses = demoSum(monthExpenses, "amount");
|
|
374
409
|
return {
|
|
375
410
|
business: {
|
|
376
411
|
name: "Demo Studio SL",
|
|
@@ -382,39 +417,70 @@ export class DemoFrihetClient {
|
|
|
382
417
|
defaults: { taxRate: 21, irpfRate: 15, dueDays: 30, currency: "EUR" },
|
|
383
418
|
plan: {
|
|
384
419
|
name: "free",
|
|
385
|
-
invoices: { used:
|
|
386
|
-
expenses: { used:
|
|
420
|
+
invoices: { used: monthInvoices.length, limit: 999 },
|
|
421
|
+
expenses: { used: monthExpenses.length, limit: 5 },
|
|
387
422
|
aiMessages: { used: 2, limit: 30 },
|
|
388
423
|
},
|
|
389
|
-
series: [{ id: "default", prefix: "F", current:
|
|
424
|
+
series: [{ id: "default", prefix: "F", current: demoInvoices.length, year: Number(DEMO_NOW.slice(0, 4)) }],
|
|
390
425
|
recentActivity: {
|
|
391
|
-
lastInvoice:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
426
|
+
lastInvoice: lastInvoice
|
|
427
|
+
? { number: lastInvoice.documentNumber, date: lastInvoice.issueDate, client: lastInvoice.clientName }
|
|
428
|
+
: null,
|
|
429
|
+
lastExpense: lastExpense
|
|
430
|
+
? { date: lastExpense.date, vendor: lastExpense.vendor, amount: lastExpense.amount }
|
|
431
|
+
: null,
|
|
432
|
+
overdueCount: overdue.length,
|
|
433
|
+
overdueAmount: demoSum(overdue, "total"),
|
|
434
|
+
unpaidCount: unpaid.length,
|
|
435
|
+
coverage: { totalAnalyzed: demoInvoices.length, sampleLimit: 500, isTruncated: false, asOf: today, currency: "EUR", excludedForeignInvoices: 0 },
|
|
396
436
|
},
|
|
397
|
-
topClients:
|
|
437
|
+
topClients: demoTopClients(demoInvoices.filter((invoice) => invoice.status === "paid")),
|
|
398
438
|
currentMonth: {
|
|
399
|
-
revenue
|
|
400
|
-
expenses
|
|
401
|
-
profit:
|
|
402
|
-
invoiceCount:
|
|
403
|
-
expenseCount:
|
|
439
|
+
revenue,
|
|
440
|
+
expenses,
|
|
441
|
+
profit: demoMoney(revenue - expenses),
|
|
442
|
+
invoiceCount: monthInvoices.length,
|
|
443
|
+
expenseCount: monthExpenses.length,
|
|
404
444
|
},
|
|
405
445
|
...READ_STAMP,
|
|
406
446
|
};
|
|
407
447
|
}
|
|
408
448
|
async getMonthlySummary(month) {
|
|
449
|
+
const period = month ?? DEMO_NOW.slice(0, 7);
|
|
450
|
+
const invoices = demoInvoices.filter((invoice) => String(invoice.issueDate).startsWith(`${period}-`));
|
|
451
|
+
const expenses = demoExpenses.filter((expense) => String(expense.date).startsWith(`${period}-`));
|
|
452
|
+
// Match /v1/monthly: drafts count as created, but do not contribute revenue or tax.
|
|
453
|
+
const issued = invoices.filter((invoice) => invoice.status !== "draft" && invoice.status !== "cancelled");
|
|
454
|
+
const deductible = expenses.filter((expense) => expense.taxDeductible !== false);
|
|
455
|
+
const revenue = {
|
|
456
|
+
total: demoSum(issued, "total"), taxBase: demoSum(issued, "subtotal"),
|
|
457
|
+
tax: demoSum(issued, "taxAmount"), irpf: demoSum(issued, "irpfAmount"),
|
|
458
|
+
};
|
|
459
|
+
const expenseTotal = demoSum(expenses, "amount");
|
|
460
|
+
const inputTax = demoSum(deductible, "tax");
|
|
461
|
+
const byCategory = {};
|
|
462
|
+
for (const expense of expenses) {
|
|
463
|
+
const category = String(expense.category ?? "other");
|
|
464
|
+
byCategory[category] = demoMoney((byCategory[category] ?? 0) + Number(expense.amount));
|
|
465
|
+
}
|
|
466
|
+
const vatPayable = demoMoney(revenue.tax - inputTax);
|
|
409
467
|
return {
|
|
410
|
-
period
|
|
411
|
-
revenue
|
|
412
|
-
expenses: { total:
|
|
413
|
-
profit: {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
468
|
+
period,
|
|
469
|
+
revenue,
|
|
470
|
+
expenses: { total: expenseTotal, deductible: demoSum(deductible, "amount"), tax: inputTax },
|
|
471
|
+
profit: {
|
|
472
|
+
gross: demoMoney(revenue.total - expenseTotal),
|
|
473
|
+
net: demoMoney(revenue.total - expenseTotal - revenue.tax + inputTax + revenue.irpf),
|
|
474
|
+
},
|
|
475
|
+
invoices: {
|
|
476
|
+
created: invoices.length,
|
|
477
|
+
sent: invoices.filter((invoice) => invoice.status === "sent" || invoice.status === "partial").length,
|
|
478
|
+
paid: invoices.filter((invoice) => invoice.status === "paid").length,
|
|
479
|
+
overdue: invoices.filter((invoice) => invoice.status === "overdue").length,
|
|
480
|
+
},
|
|
481
|
+
topClients: demoTopClients(issued),
|
|
482
|
+
byCategory,
|
|
483
|
+
taxLiability: { vatPayable, irpfRetained: revenue.irpf, estimatedModel303: vatPayable },
|
|
418
484
|
...READ_STAMP,
|
|
419
485
|
};
|
|
420
486
|
}
|
|
@@ -517,7 +583,12 @@ export class DemoFrihetClient {
|
|
|
517
583
|
return findOrStub(demoBankAccounts, id);
|
|
518
584
|
}
|
|
519
585
|
async listTransactions(params) {
|
|
520
|
-
|
|
586
|
+
const filtered = demoTransactions.filter((row) => (!params?.from || (typeof row.postedAt === "string" && row.postedAt >= params.from))
|
|
587
|
+
&& (!params?.to || (typeof row.postedAt === "string" && row.postedAt <= params.to))
|
|
588
|
+
&& (!params?.accountId || row.accountId === params.accountId)
|
|
589
|
+
&& (!params?.status || row.status === params.status)
|
|
590
|
+
&& (!params?.category || row.category === params.category));
|
|
591
|
+
return demoPage(filtered, params, ["postedAt", "id"]);
|
|
521
592
|
}
|
|
522
593
|
async categorizeTransaction(id, data) {
|
|
523
594
|
return simulateAction(id, { category: data.category, ...(data.notes ? { notes: data.notes } : {}) });
|
package/dist/demo-fixtures.d.ts
CHANGED
|
@@ -31,11 +31,11 @@ export declare const DEMO_NOW = "2026-07-15T10:00:00.000Z";
|
|
|
31
31
|
/** Test IBAN (ECBS/AEAT example — never a real account). */
|
|
32
32
|
export declare const DEMO_TEST_IBAN = "ES9121000418450200051332";
|
|
33
33
|
export type DemoPaginated = PaginatedResponse<Record<string, unknown>>;
|
|
34
|
-
/**
|
|
35
|
-
export declare function demoPage(data: Record<string, unknown>[], params?: {
|
|
34
|
+
/** Page fixtures after filtering; opt-in string keys sort descending on a copy. */
|
|
35
|
+
export declare function demoPage(data: readonly Record<string, unknown>[], params?: {
|
|
36
36
|
limit?: number;
|
|
37
37
|
offset?: number;
|
|
38
|
-
}): DemoPaginated;
|
|
38
|
+
}, descendingKeys?: readonly string[]): DemoPaginated;
|
|
39
39
|
/** Empty PaginatedResponse for surfaces without dedicated fixtures. */
|
|
40
40
|
export declare function demoEmptyPage(params?: {
|
|
41
41
|
limit?: number;
|
|
@@ -43,15 +43,25 @@ export declare function demoEmptyPage(params?: {
|
|
|
43
43
|
}): DemoPaginated;
|
|
44
44
|
export declare const demoClients: Record<string, unknown>[];
|
|
45
45
|
export declare const demoProducts: Record<string, unknown>[];
|
|
46
|
+
export declare function demoMoney(n: number): number;
|
|
46
47
|
export declare const demoInvoices: Record<string, unknown>[];
|
|
48
|
+
/** Aggregate stored fixture amounts; never recalculate invoice taxes here. */
|
|
49
|
+
export declare function demoSum(records: Record<string, unknown>[], field: string): number;
|
|
50
|
+
/** Rank only the invoices selected by the caller (paid or issued in a month). */
|
|
51
|
+
export declare function demoTopClients(invoices: Record<string, unknown>[]): {
|
|
52
|
+
name: string;
|
|
53
|
+
totalRevenue: number;
|
|
54
|
+
invoiceCount: number;
|
|
55
|
+
}[];
|
|
47
56
|
export declare const demoExpenses: Record<string, unknown>[];
|
|
48
57
|
export declare const demoQuotes: Record<string, unknown>[];
|
|
49
58
|
export declare const demoVendors: Record<string, unknown>[];
|
|
59
|
+
export declare const demoPurchaseOrders: Record<string, unknown>[];
|
|
50
60
|
export declare const demoBankAccounts: Record<string, unknown>[];
|
|
51
61
|
export declare const demoTransactions: Record<string, unknown>[];
|
|
52
62
|
/** Find a fixture by id, or synthesize a minimal stamped object with that id. */
|
|
53
63
|
export declare function findOrStub(collection: Record<string, unknown>[], id: string): Record<string, unknown> & DemoStamp;
|
|
54
|
-
/**
|
|
64
|
+
/** Simulated ids cannot alias read-only fixtures (no network or clock dependency). */
|
|
55
65
|
export declare function demoId(prefix: string): string;
|
|
56
66
|
/**
|
|
57
67
|
* Simulate a create/update: echo the input, stamp a fabricated demo id +
|
package/dist/demo-fixtures.js
CHANGED
|
@@ -36,12 +36,21 @@ export const EMPTY_STAMP = {
|
|
|
36
36
|
export const DEMO_NOW = "2026-07-15T10:00:00.000Z";
|
|
37
37
|
/** Test IBAN (ECBS/AEAT example — never a real account). */
|
|
38
38
|
export const DEMO_TEST_IBAN = "ES9121000418450200051332";
|
|
39
|
-
/**
|
|
40
|
-
export function demoPage(data, params) {
|
|
39
|
+
/** Page fixtures after filtering; opt-in string keys sort descending on a copy. */
|
|
40
|
+
export function demoPage(data, params, descendingKeys) {
|
|
41
41
|
const limit = params?.limit ?? 50;
|
|
42
42
|
const offset = params?.offset ?? 0;
|
|
43
|
+
const ordered = descendingKeys?.length ? [...data].sort((a, b) => {
|
|
44
|
+
for (const key of descendingKeys) {
|
|
45
|
+
const left = String(a[key] ?? "");
|
|
46
|
+
const right = String(b[key] ?? "");
|
|
47
|
+
if (left !== right)
|
|
48
|
+
return left < right ? 1 : -1;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}) : data;
|
|
43
52
|
return {
|
|
44
|
-
data:
|
|
53
|
+
data: ordered.slice(offset, offset + limit),
|
|
45
54
|
total: data.length,
|
|
46
55
|
limit,
|
|
47
56
|
offset,
|
|
@@ -124,16 +133,17 @@ export const demoProducts = [
|
|
|
124
133
|
{ id: "demo_prd_007", name: "Menú catering (por persona)", unitPrice: 18, taxRate: 10, description: "Servicio de catering, IVA reducido hostelería", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
125
134
|
{ id: "demo_prd_008", name: "Libro técnico impreso", unitPrice: 24, taxRate: 4, description: "Publicación impresa, IVA superreducido", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
126
135
|
];
|
|
127
|
-
function
|
|
136
|
+
export function demoMoney(n) {
|
|
128
137
|
return Math.round(n * 100) / 100;
|
|
129
138
|
}
|
|
130
139
|
function invoice(opts) {
|
|
131
|
-
const subtotal =
|
|
132
|
-
const taxAmount =
|
|
133
|
-
const irpfAmount = opts.irpfRate ?
|
|
134
|
-
const total =
|
|
140
|
+
const subtotal = demoMoney(opts.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0));
|
|
141
|
+
const taxAmount = demoMoney(subtotal * (opts.taxRate / 100));
|
|
142
|
+
const irpfAmount = opts.irpfRate ? demoMoney(subtotal * (opts.irpfRate / 100)) : 0;
|
|
143
|
+
const total = demoMoney(subtotal + taxAmount - irpfAmount);
|
|
135
144
|
return {
|
|
136
145
|
id: opts.id,
|
|
146
|
+
documentNumber: `F-${opts.issueDate.slice(0, 4)}-${opts.id.split("_").at(-1)}`,
|
|
137
147
|
clientId: opts.clientId,
|
|
138
148
|
clientName: opts.clientName,
|
|
139
149
|
items: opts.items,
|
|
@@ -163,16 +173,29 @@ export const demoInvoices = [
|
|
|
163
173
|
invoice({ id: "demo_inv_008", clientId: "demo_cli_003", clientName: "Servicios Insulares Canarias SL", items: [{ description: "Mantenimiento mensual", quantity: 3, unitPrice: 150 }], taxRate: 7, status: "overdue", issueDate: "2026-04-05", dueDate: "2026-05-05", notes: "IGIC 7% — factura vencida", clientLocation: "canarias" }),
|
|
164
174
|
invoice({ id: "demo_inv_009", clientId: "demo_cli_004", clientName: "Marina Delgado (autónoma)", items: [{ description: "Diseño de marca", quantity: 1, unitPrice: 1200 }, { description: "Auditoría SEO", quantity: 1, unitPrice: 600 }], taxRate: 21, status: "draft", issueDate: "2026-07-08", dueDate: "2026-08-08", irpfRate: 15, notes: "Autónoma con retención IRPF 15%", clientLocation: "peninsula" }),
|
|
165
175
|
];
|
|
176
|
+
/** Aggregate stored fixture amounts; never recalculate invoice taxes here. */
|
|
177
|
+
export function demoSum(records, field) {
|
|
178
|
+
return demoMoney(records.reduce((sum, record) => sum + Number(record[field] ?? 0), 0));
|
|
179
|
+
}
|
|
180
|
+
/** Rank only the invoices selected by the caller (paid or issued in a month). */
|
|
181
|
+
export function demoTopClients(invoices) {
|
|
182
|
+
return demoClients.map((client) => {
|
|
183
|
+
const matched = invoices.filter((invoice) => invoice.clientId === client.id);
|
|
184
|
+
return { name: String(client.name), totalRevenue: demoSum(matched, "total"), invoiceCount: matched.length };
|
|
185
|
+
}).filter((client) => client.invoiceCount > 0)
|
|
186
|
+
.sort((a, b) => b.totalRevenue - a.totalRevenue)
|
|
187
|
+
.slice(0, 5);
|
|
188
|
+
}
|
|
166
189
|
/* ------------------------------------------------------------------ */
|
|
167
190
|
/* Expenses (6) — categorized */
|
|
168
191
|
/* ------------------------------------------------------------------ */
|
|
169
192
|
export const demoExpenses = [
|
|
170
|
-
{ id: "demo_exp_001", description: "Suscripción software de diseño", amount: 15.4, category: "software", date: "2026-07-01", vendor: "Herramientas Digitales SL", taxDeductible: true, createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
171
|
-
{ id: "demo_exp_002", description: "Material de oficina", amount: 48.2, category: "office_supplies", date: "2026-06-18", vendor: "Papelería Central", taxDeductible: true, createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
172
|
-
{ id: "demo_exp_003", description: "Comida de trabajo con cliente", amount: 62.0, category: "meals", date: "2026-06-22", vendor: "Restaurante El Puerto", taxDeductible: true, createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
173
|
-
{ id: "demo_exp_004", description: "Combustible desplazamiento", amount: 70.0, category: "travel", date: "2026-06-25", vendor: "Estación de Servicio Norte", taxDeductible: true, createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
174
|
-
{ id: "demo_exp_005", description: "Cuota gestoría mensual", amount: 90.0, category: "professional_services", date: "2026-07-01", vendor: "Gestoría Ramírez", taxDeductible: true, createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
175
|
-
{ id: "demo_exp_006", description: "Hosting anual", amount: 120.0, category: "software", date: "2026-05-30", vendor: "Proveedor Cloud SL", taxDeductible: true, createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
193
|
+
{ id: "demo_exp_001", description: "Suscripción software de diseño", amount: 15.4, category: "software", date: "2026-07-01", vendor: "Herramientas Digitales SL", taxDeductible: true, currency: "EUR", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
194
|
+
{ id: "demo_exp_002", description: "Material de oficina", amount: 48.2, category: "office_supplies", date: "2026-06-18", vendor: "Papelería Central", taxDeductible: true, currency: "EUR", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
195
|
+
{ id: "demo_exp_003", description: "Comida de trabajo con cliente", amount: 62.0, category: "meals", date: "2026-06-22", vendor: "Restaurante El Puerto", taxDeductible: true, currency: "EUR", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
196
|
+
{ id: "demo_exp_004", description: "Combustible desplazamiento", amount: 70.0, category: "travel", date: "2026-06-25", vendor: "Estación de Servicio Norte", taxDeductible: true, currency: "EUR", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
197
|
+
{ id: "demo_exp_005", description: "Cuota gestoría mensual", amount: 90.0, category: "professional_services", date: "2026-07-01", vendor: "Gestoría Ramírez", taxDeductible: true, currency: "EUR", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
198
|
+
{ id: "demo_exp_006", description: "Hosting anual", amount: 120.0, category: "software", date: "2026-05-30", vendor: "Proveedor Cloud SL", taxDeductible: true, currency: "EUR", createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
176
199
|
];
|
|
177
200
|
/* ------------------------------------------------------------------ */
|
|
178
201
|
/* Quotes (2) */
|
|
@@ -218,6 +241,51 @@ export const demoVendors = [
|
|
|
218
241
|
{ id: "demo_ven_002", name: "Proveedor Cloud SL", email: "facturacion@example.com", phone: "+34 600 000 011", taxId: "B22222222", address: { city: "Barcelona", postalCode: "08007", country: "ES" }, createdAt: DEMO_NOW, updatedAt: DEMO_NOW },
|
|
219
242
|
];
|
|
220
243
|
/* ------------------------------------------------------------------ */
|
|
244
|
+
/* Purchase orders (2) — ADR 0027 S2 read-only surface. */
|
|
245
|
+
/* Same vendor ids as demoVendors so list+get read coherently. */
|
|
246
|
+
/* ------------------------------------------------------------------ */
|
|
247
|
+
export const demoPurchaseOrders = [
|
|
248
|
+
{
|
|
249
|
+
id: "demo_po_001",
|
|
250
|
+
poNumber: "PO-2026-0001",
|
|
251
|
+
vendorId: "demo_ven_001",
|
|
252
|
+
vendorName: "Gestoría Ramírez",
|
|
253
|
+
status: "sent",
|
|
254
|
+
items: [
|
|
255
|
+
{ description: "Asesoria contable mensual", quantity: 1, unitPrice: 350, productId: "demo_prod_001" },
|
|
256
|
+
],
|
|
257
|
+
issueDate: "2026-09-01",
|
|
258
|
+
expectedDate: "2026-10-01",
|
|
259
|
+
taxRate: 21,
|
|
260
|
+
subtotal: 350,
|
|
261
|
+
tax: 73.5,
|
|
262
|
+
total: 423.5,
|
|
263
|
+
currency: "EUR",
|
|
264
|
+
notes: "Renovacion contrato asesoria",
|
|
265
|
+
createdAt: DEMO_NOW,
|
|
266
|
+
updatedAt: DEMO_NOW,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "demo_po_002",
|
|
270
|
+
poNumber: "PO-2026-0002",
|
|
271
|
+
vendorId: "demo_ven_002",
|
|
272
|
+
vendorName: "Proveedor Cloud SL",
|
|
273
|
+
status: "draft",
|
|
274
|
+
items: [
|
|
275
|
+
{ description: "Hosting VPS 4 vCPU", quantity: 12, unitPrice: 45, productId: "demo_prod_002" },
|
|
276
|
+
{ description: "Backup storage 500 GB", quantity: 1, unitPrice: 80 },
|
|
277
|
+
],
|
|
278
|
+
issueDate: "2026-09-15",
|
|
279
|
+
taxRate: 21,
|
|
280
|
+
subtotal: 620,
|
|
281
|
+
tax: 130.2,
|
|
282
|
+
total: 750.2,
|
|
283
|
+
currency: "EUR",
|
|
284
|
+
createdAt: DEMO_NOW,
|
|
285
|
+
updatedAt: DEMO_NOW,
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
/* ------------------------------------------------------------------ */
|
|
221
289
|
/* Banking — 1 account + 4 transactions */
|
|
222
290
|
/* ------------------------------------------------------------------ */
|
|
223
291
|
export const demoBankAccounts = [
|
|
@@ -250,10 +318,10 @@ export function findOrStub(collection, id) {
|
|
|
250
318
|
return { id, ...READ_STAMP };
|
|
251
319
|
}
|
|
252
320
|
let demoIdCounter = 0;
|
|
253
|
-
/**
|
|
321
|
+
/** Simulated ids cannot alias read-only fixtures (no network or clock dependency). */
|
|
254
322
|
export function demoId(prefix) {
|
|
255
323
|
demoIdCounter += 1;
|
|
256
|
-
return `${prefix}
|
|
324
|
+
return `${prefix}_simulated_${String(demoIdCounter).padStart(3, "0")}`;
|
|
257
325
|
}
|
|
258
326
|
/**
|
|
259
327
|
* Simulate a create/update: echo the input, stamp a fabricated demo id +
|